From 23ba57a9c731b332e1e349d623f438d81b04994b Mon Sep 17 00:00:00 2001
From: Bruno Lavit <bruno.lavit@forgerock.com>
Date: Thu, 07 Apr 2016 13:45:10 +0000
Subject: [PATCH] Merge branch 'opendj-sdk'

---
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java                                 |   63 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/first.names                                                   | 8605 ++
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequest.java                                        |   41 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestImpl.java                            |  135 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultReferenceImpl.java               |   40 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/LDAPTestCase.java                                              |   30 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityResponseControl.java                 |  180 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiredResponseControl.java                       |  155 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractApproximateMatchingRuleImpl.java                    |   48 
 opendj-sdk/opendj-grizzly/src/main/resources/com/forgerock/opendj/grizzly/grizzly.properties                                      |   24 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDigestMD5SASLBindRequestImpl.java             |  141 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java                                    |  210 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java                                 |   71 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ResultImpl.java                                          |   57 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRule.java                                           |  558 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/cities                                                        |  232 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate.bat                                                                  |   22 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connection.java                                                    | 1453 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindClientImpl.java                                   |  194 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequest.java                                      |  102 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransportTestCase.java                          |   67 
 opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/GrizzlyTransportProvider.java                                |   52 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java                                |   93 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RegexSyntaxTestCase.java                                    |   59 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java                                              |  440 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleTest.java              |   44 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Filter.java                                                        | 1653 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_TW.properties                          |   19 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPPasswordModify.java                              |  288 
 opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java                                      |  195 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequest.java                                |  105 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestTestCase.java                      |  125 
 opendj-sdk/opendj-core/src/test/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider                       |   15 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java                                                 |  124 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV1RequestControl.java                          |  220 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleImpl.java                     |   44 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Search.java                                       |  125 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/FileBasedArgument.java                                               |  169 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequest.java                               |   99 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResult.java                        |  131 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java                            |  508 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetInfo.java                                      |  135 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseBuilderTestCase.java                         |  147 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPCompareITCase.java                               |   94 
 opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/CliTestCase.java                                                     |   28 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_deselected.jpg                                               |    0 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequest.java                             |  466 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java                                             |   90 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationContainer.java              |   56 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListSubtypes.ftl                                                          |   45 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java                                               |  669 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericIntermediateResponseImpl.java         |   39 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java                                               |  412 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ReferenceCountedObject.java                                        |  156 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java                                     |  149 
 opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandTestCase.java                                          |  249 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FaxSyntaxImpl.java                                          |   60 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleImpl.java              |   68 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRule.java                                       |  505 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java                                |   69 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java                                        |  510 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleImpl.java  |   90 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/RequestsTestCase.java                                     |  214 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResult.java                              |   95 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationSyntaxImpl.java                          |   68 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaOptions.java                                          |  135 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SearchScopeTestCase.java                                           |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCancelExtendedRequestImpl.java                |   41 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResult.java                                |  151 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java                                        |  740 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/package-info.java                                       |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiablePasswordModifyExtendedResultImpl.java        |   44 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameForm.java                                               |  637 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/IntermediateResponseHandler.java                                   |   58 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java                                     |  171 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/package-info.java                                 |   22 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GuideSyntaxTest.java                                        |   40 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java                                                          |  848 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutChecker.java                                                |  170 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyRequestImpl.java                        |   92 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/KeywordEqualityMatchingRuleImpl.java                        |  134 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleTest.java                        |   51 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxImpl.java                                         |  122 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryReader.java                                         |  394 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCRAMMD5SASLBindRequestImpl.java               |   57 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/setcp.bat                                                                 |   29 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/TemplateTagTestcase.java                                           |  283 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java                                        |  435 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleTest.java                           |   51 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestTestCase.java                       |  104 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleTest.java                 |  100 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleImpl.java                  |   44 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControlTestCase.java              |   81 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/duration-syntax.html                                             |   24 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java |   82 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapsearch.bat                                                               |   22 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/last.names                                                    | 13419 +++
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedRequest.java                     |  169 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSyntaxImpl.java                                  |   59 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java                                                        |  522 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/authrate.bat                                                                 |   22 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericBindRequestTestCase.java                           |  105 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequenceReader.java                                            |  523 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entry.java                                                         |  522 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SSLContextBuilder.java                                             |  212 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryNotFoundException.java                                        |   44 
 opendj-sdk/opendj-rest2ldap/src/site/xdoc/index.xml.vm                                                                            |   99 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AddressMask.java                                                   |  512 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NameFormTestCase.java                                       | 1044 
 opendj-sdk/opendj-ldap-toolkit/src/site/xdoc/index.xml.vm                                                                         |   63 
 opendj-sdk/opendj-ldap-toolkit/README                                                                                             |   11 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/FilterVisitor.java                                                 |  192 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java                              |  194 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthorizationException.java                                        |   46 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedResult.java                  |  103 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractEqualityMatchingRuleImpl.java                       |   53 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GSERParserTestCase.java                                            |  291 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestTestCase.java                        |  102 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Reader.java                                              |  154 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableExtendedRequest.java                  |   57 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/TransactionIdControl.java                                 |  131 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponse.java                         |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReferenceImpl.java                           |   70 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxTest.java                              |   45 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java                                                  | 2823 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TrustManagers.java                                                 |  373 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaUtils.java                                            |  776 
 opendj-sdk/opendj-ldap-toolkit/legal-notices/THIRDPARTYREADME.txt                                                                 |  338 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ManageDsaITRequestControl.java                            |  160 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java                                             | 1608 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ReaderTestCase.java                                              |  789 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadResponseControl.java                               |  209 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitor.java                                           |   87 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate                                                                      |   25 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Responses.java                                           |  537 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedResult.java                      |  124 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Response.java                                            |   89 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedHashMapEntry.java                                            |  164 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBindAsync.java                              |  173 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/AbstractLDIFTestCase.java                                          |   33 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java                               |  196 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java                                         |  673 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestLoadBalancer.java                                           |  251 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java                                               |  184 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractIntermediateResponse.java                        |   81 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ResultCodeTestCase.java                                            |   63 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutResultException.java                                        |   31 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/states                                                        |   51 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxTest.java                                    |   35 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConstraintViolationException.java                                  |   67 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java                          |  185 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVarListEntry.ftl                                                          |   21 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleTest.java                     |   59 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DoubleMetaphoneApproximateMatchingRuleImpl.java             |  942 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleImpl.java                 |   38 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaElement.java                                          |  299 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/people_and_groups.template                                    |   62 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateRefEntriesMojo.java                       |  275 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestImpl.java                    |  284 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutEventListener.java                                          |   47 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleTest.java                     |   54 
 opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/package-info.java                                            |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiringResponseControl.java                      |  177 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java                                   |   30 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java                                        |  878 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java               |   76 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExtendedRequestTestCase.java                              |   35 
 opendj-sdk/opendj-rest2ldap/README                                                                                                |   11 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java                                | 2489 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchAsync.java                                  |  241 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Indexer.java                                                   |   68 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ApplicationKeyManager.java                                           |  261 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ConnectionState.java                                           |  371 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameFormSyntaxImpl.java                                     |  154 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchRequestControl.java                       |  371 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java                                             |  124 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java                    |  112 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ReturnCode.java                                                      |  218 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListSubstringMatchingRuleImpl.java                |   49 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_TW.properties                                         |   86 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFSearch.java                                      |  280 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestImpl.java                            |  164 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequest.java                             |  113 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java                              |  263 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java                            |  385 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/package-info.java                                              |   21 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java                                                 |  139 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestTestCase.java                     |  203 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequest.java                                       |  176 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/example.template                                              |   38 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties                                            |   90 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelRequestListener.java                                         |   48 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java                              |   73 
 opendj-sdk/opendj-grizzly/src/site/xdoc/index.xml.vm                                                                              |   99 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleTest.java                |   72 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/package-info.java                                         |   21 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriterTestCase.java                          |  388 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ASCIICharPropTestCase.java                                         |  257 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateSchemaDocMojo.java                        |  252 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_fr.properties                             |   19 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringOrderingMatchingRuleImpl.java                    |   40 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgReference.ftl                                                             |   35 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ApproximateMatchingRuleTest.java                            |  140 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java                                                  |  275 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSimpleBindRequestImpl.java                    |   51 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateListSyntaxImpl.java                              |   65 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentException.java                                               |   61 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributesTestCase.java                                            |  114 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtils.java                                                 |  244 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPReader.java                                                      |  722 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GenericControl.java                                       |  155 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AuthRateITCase.java                                  |   82 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_script-util.bat                                                          |  145 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_es.properties                                            |   86 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java                                                 |  623 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/package-info.java                                         |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OIDSyntaxImpl.java                                          |   68 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_de.properties                                            |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancerEventListener.java                                     |   91 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleTest.java                  |   55 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java                                     |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestImpl.java                         |  395 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java                                                  | 2068 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleTest.java            |   47 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/descriptor.xml                                                                   |  110 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConditionResult.java                                               |  235 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequest.java                                |  433 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiColumnPrinter.java                                              |  519 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BinarySyntaxImpl.java                                       |   58 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetADChangeNotifications.java                     |  134 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionsTestCase.java                                           |  152 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaConstants.java                                        |  761 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanMatchingRuleTest.java                   |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubentriesRequestControl.java                             |  228 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SyntaxTestCase.java                                         |  598 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxTest.java                                      |   47 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxImpl.java                                     |   53 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxImpl.java                                         |   89 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java                                     |  197 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateTag.java                                                   | 1462 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/UtilTestCase.java                                                  |   29 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxImpl.java                                      |   65 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleTest.java                        |  145 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequestImpl.java                                    |   80 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EnumSyntaxTestCase.java                                     |   80 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java                                           |  267 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControlTestCase.java               |   58 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyWarningType.java                            |   65 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressSyntaxImpl.java                          |   67 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryWriterTestCase.java                                 |  110 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/package-info.java                                    |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResult.java                                       |  112 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaTestCase.java                                         |  114 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableExternalSASLBindRequestImpl.java              |   39 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryReaderTestCase.java                                 |  265 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/package-info.java                                                    |   19 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java                            |  372 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java                                                           |  740 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnection.java                                |   97 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Functions.java                                                     |  438 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRule.java                                         |  868 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationType.java                   |  186 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumOrderingMatchingRule.java                               |   53 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleImpl.java                    |   50 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericBindRequestImpl.java                   |   51 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleTest.java                           |   53 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/RegexSyntaxImpl.java                                        |   91 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaException.java                                        |   60 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactAssertionSyntaxImpl.java                    |   53 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryWriterTestCase.java                                       |  826 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxTest.java                                 |   37 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAP.java                                                            |  833 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnectionFactory.java                                       |   55 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelledResultException.java                                      |   38 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java                                     |  387 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResultImpl.java                      |   98 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StaticUtils.java                                                   |  789 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyResponseControl.java                        |  354 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TabSeparatedTablePrinter.java                                        |  137 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstringMatchingRuleTest.java                              |  217 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_pl.properties                                               |   17 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TeletexTerminalIdentifierSyntaxImpl.java                    |  188 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java                                |  438 
 opendj-sdk/opendj-sdk-bom/pom.xml                                                                                                 |  101 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitorWriter.java                                     |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequest.java                              |  121 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java                                            |  228 
 opendj-sdk/opendj-ldap-toolkit/src/test/resources/valid_test_template.ldif                                                        |   28 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaValidationPolicy.java                                 |  425 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MultipleEntriesFoundException.java                                 |   35 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFactory.java                                              |   40 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java                             |  588 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeTest.java                                      |  547 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractLDAPMessageHandler.java                                      |  244 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandler.java                                                |  218 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUse.java                                        |  490 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnhancedGuideSyntaxImpl.java                                |  121 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryWriter.java                                         |  112 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java                                                           |  744 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java                                          | 1300 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java                                         | 1768 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequest.java                                   |  126 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractFilterVisitor.java                                         |  173 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGSSAPISASLBindRequestImpl.java                |  152 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/WordEqualityMatchingRuleTest.java                           |   57 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/package-info.java                                                  |   22 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldappasswordmodify.bat                                                       |   22 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Matcher.java                                                       |  567 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java                                               |  992 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferWriterTestCase.java                                |   61 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestImpl.java                                  |  142 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSyntaxImpl.java                                |   88 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchChangeType.java                           |   83 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SubstringReader.java                                               |  151 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRuleSyntaxImpl.java                             |  141 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/package-info.java                                                  |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java                                        |  324 
 opendj-sdk/opendj-ldap-sdk-examples/pom.xml                                                                                       |  100 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleTest.java                  |   47 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java                                              |  767 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_TW.properties                                            |   17 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableCompareResultImpl.java                       |   32 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeBuilderTestCase.java                           |  176 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryReaderTestCase.java                                       | 1662 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj-config.css                                                |  101 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ConflictingSchemaElementException.java                      |   39 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Base64.java                                                        |  356 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticationException.java                                       |   44 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResultDecoder.java                               |  111 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleTest.java                   |   53 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java                                                   |  835 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestTestCase.java                     |   81 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_de.properties                                               |   17 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultReferenceIOException.java                              |   59 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Controls.java                                     | 1076 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java                                       |  352 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/CommandLineTool.java                              |  167 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_client-script.bat                                                        |   42 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java                                   |  314 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java                                         |  244 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapmodify                                                                   |   28 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordSyntaxImpl.java                                 |  154 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifsearch.bat                                                               |   21 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/OperatingSystemTestCase.java                                       |  133 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SyntaxImpl.java                                             |  112 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatTest.java                                       |  299 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPListener.java                                         |   75 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java                                                  | 1678 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/IntermediateResponse.java                                |   84 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImplTest.java                  |  281 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPSearchITCase.java                                |   87 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/PromptingTrustManager.java                                           |  388 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleImpl.java                  |   46 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java                                          | 2664 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ca_ES.properties                                            |   17 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImplTest.java                   |  203 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractExtendedRequest.java                              |   89 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1OutputStreamWriter.java                                          |  336 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java                                              |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java                               |  284 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/responses/ResponsesTestCase.java                                   |   31 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxImpl.java                                |  200 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldappasswordmodify                                                           |   27 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordReader.java                                            |   72 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateConfigurationReferenceMojo.java           |  154 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java                                                   |   29 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java                                                   |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GuideSyntaxImpl.java                                        |  314 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ja.properties                                            |   86 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelexNumberSyntaxImpl.java                                  |  150 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxImpl.java                              |  142 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponseImpl.java                     |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindRequest.java                                          |   96 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RDNTestCase.java                                                   |  512 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java                        |   70 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ja.properties                                               |   17 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaElementTestCase.java                          |  140 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedAttribute.java                                               |  694 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ResultCode.java                                                    | 1000 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CollationMatchingRulesImpl.java                             |  333 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxTest.java                              |   44 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ConnectionSecurityLayerFilter.java                           |  120 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstitutionSyntaxTestCase.java                             |  102 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapmodify.bat                                                               |   22 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequest.java                                       |   64 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java                        |  131 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassSyntaxImpl.java                                  |  149 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java                                                 |  260 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties                                                  |  463 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java                                   |  859 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java                    |  447 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxImpl.java                                    |   84 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CompareRequestTestCase.java                               |  109 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java                                                |  449 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AddRequestTestCase.java                                   |  147 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleImpl.java                                       |  132 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Request.java                                              |   89 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListItem.ftl                                                              |   25 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java                                                    |   40 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordExactEqualityMatchingRuleImpl.java              |   46 
 opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/UtilsTestCase.java                                                   |   84 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/MatchedValuesRequestControl.java                          |  353 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntry.java                                   |  119 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java                               |   90 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLife.java                                    |  163 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindRequest.java                                      |   79 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxTest.java                                |  114 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/Control.java                                              |   90 
 opendj-sdk/opendj-grizzly/src/main/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider                    |   15 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Writer.java                                                      |  356 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java                                       |  139 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties                                |  372 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java                                      |  842 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ko.properties                             |   19 
 opendj-sdk/opendj-core/clirr-ignored-api-changes.xml                                                                              |   35 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapcompare                                                                  |   27 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleImpl.java                           |   44 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaTestCase.java                                 |   27 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java                            |  167 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestImpl.java                          |  136 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java                                         | 1224 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestImpl.java                           |  153 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java                                              |   43 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableUnbindRequestImpl.java                        |   27 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ClientException.java                                                 |   98 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ControlDecoder.java                                       |   50 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClientImpl.java                                       |  104 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/TransportProvider.java                                         |   70 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/KeyManagers.java                                                   |  283 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java                                             | 1152 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestImpl.java                             |  182 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java                |   58 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java                                        |  203 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxTest.java                                 |   47 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterators.java                                                     |  341 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/sec-locales-subtypes.ftl                                          |   62 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DeliveryMethodSyntaxImpl.java                               |  118 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentGroup.java                                                   |  154 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchemaAsync.java                               |  173 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancer.java                                                  |  297 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PrintableStringSyntaxImpl.java                              |  189 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java                                                    |   31 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableIntermediateResponseImpl.java        |   48 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanOrEqualMatchingRuleTest.java            |   87 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/ConnectionStateTest.java                                       |  269 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferReader.java                                        |  572 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/package-info.java                                           |   21 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyUtilsTestCase.java                                    |  103 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/package-info.java                                                    |   29 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ASCIICharProp.java                                                 |  316 
 opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java                                   |  776 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTest.java                                       |  106 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequest.java                                    |  163 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResult.java                                          |  132 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DereferenceAliasesPolicy.java                                      |  157 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_CN.properties                          |   19 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnection.java                                            |  186 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequestImpl.java                                   |   63 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java                             |   70 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractBindRequest.java                                  |   46 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTestCase.java                                   |  744 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectClassBuilderTestCase.java                             |  198 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java                                         |  542 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroup.java                                  |  178 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPBaseFilter.java                                          |  121 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleTest.java                        |   68 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EntrySchemaCheckingTestCase.java                            | 1137 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequest.java                                      |  220 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubtreeDeleteRequestControl.java                          |  143 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/authrate                                                                     |   27 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/makeldif                                                                     |   27 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumSyntaxImpl.java                                         |  112 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ModificationType.java                                              |  193 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GetEffectiveRightsRequestControl.java                     |  367 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicTransportProvider.java                                    |   56 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/pageaction.gif                                                   |    0 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java                                                     |  701 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConditionResultTestCase.java                                       |  175 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedChangeRecordListener.java                                  |  219 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsTestCase.java                                   |   30 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/package-info.java                                            |   38 
 opendj-sdk/opendj-core/src/site/xdoc/index.xml.vm                                                                                 |  180 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestTestCase.java                         |  110 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_es.properties                             |   19 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ReferralException.java                                             |   32 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxImpl.java                              |  145 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaSupportedLocales.java                             |  250 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceTestCase.java                                          |  160 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSubstringMatchingRuleImpl.java               |   55 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java                                        |  193 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attributes.java                                                    |  592 
 opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output.ldif                                                            |   11 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImpl.java                                 |  253 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleImpl.java                      |   44 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java                               |  339 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableBindResultImpl.java                          |   45 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_selected.gif                                                 |    0 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java                                            |  440 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/package-info.java                                                  |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PostalAddressSyntaxImpl.java                                |   62 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockConnectionEventListener.java                                   |   93 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java                                               |  476 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationEqualityMatchingRuleTest.java                      |  106 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanOrEqualMatchingRuleTest.java               |   87 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_client-script.sh                                                         |   54 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedRequest.java                 |  696 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCompareRequestImpl.java                       |   77 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SubstringAssertionSyntaxImpl.java                           |   94 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocDescriptionSupplement.java                                        |   32 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/controls/ControlsTestCase.java                                     |   44 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java                                      |  463 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleImpl.java                           |  163 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequest.java                        |  212 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/man-pages.xml                                                                    |   94 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java                                      |  317 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleTest.java                      |   50 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleImpl.java                        |  140 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResultImpl.java                            |  100 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/PackedLongTestCase.java                                            |   59 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java                         |  390 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPWriter.java                                                      |  689 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java                                            |  373 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java                                |  176 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/modrate.bat                                                                  |   22 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableWhoAmIExtendedResultImpl.java                |   39 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleTest.java            |   47 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ca_ES.properties                          |   20 
 opendj-sdk/opendj-cli/pom.xml                                                                                                     |  145 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxImpl.java                                      |  170 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java                                                    |   69 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_fr.properties                                               |   17 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAbandonRequestImpl.java                       |   37 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java                            |  254 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java                               |  182 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java                |   40 
 opendj-sdk/opendj-core/src/main/javadoc/overview.html                                                                             |  116 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleTest.java                |   76 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java                   |  177 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResultImpl.java                                  |  126 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CoreSchemaTest.java                                         |   31 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetSymmetricKeyExtendedRequest.java                     |  228 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleImpl.java                 |   52 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionSecurityLayer.java                                       |   65 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImplTest.java                             |   76 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTestCase.java                                  | 2340 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java                                        |  290 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java                                  |   95 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java                                        |   99 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java                                          |  136 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyDNRequestImpl.java                      |   92 
 opendj-sdk/opendj-ldap-toolkit/src/test/resources/invalid_test_template.ldif                                                      |   29 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleTest.java                  |   52 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java                                  |  153 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryReader.java                                                   |   70 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleTest.java                 |  112 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableSASLBindRequest.java                  |   37 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java                                               |  420 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferWriter.java                                        |  445 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Modification.java                                                  |   86 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortRequestControl.java                         |  323 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockScheduler.java                                                 |  260 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TreeMapEntry.java                                                  |  166 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_script-util.sh                                                           |  119 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ModificationTypeTestCase.java                                      |   46 
 opendj-sdk/opendj-cli/src/main/resources/templates/refEntry.ftl                                                                   |   86 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxTest.java                                      |   52 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriterTestCase.java                                | 1405 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AddressMaskTestCase.java                                           |  242 
 opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output_80_column.ldif                                                  |   14 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapcompare.bat                                                              |   22 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleImpl.java                   |   63 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequest.java                                 |  202 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnection.java                                              |   88 
 opendj-sdk/opendj-cli/src/main/resources/templates/optionsRefSect1.ftl                                                            |   51 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassType.java                                        |   64 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifdiff.bat                                                                 |   21 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/log-message-reference.ftl                                         |   57 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Writer.java                                              |   95 
 opendj-sdk/opendj-core/pom.xml                                                                                                    |  312 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClient.java                                           |   78 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java                                                 | 2211 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequest.java                               |  163 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1OutputStreamWriterTestCase.java                                  |   45 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/javadoc/overview.html                                                                |    7 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapsearch                                                                   |   27 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java                                 |   42 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java                                   |   79 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java                                                            |   38 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITStructureRuleTestCase.java                               |  171 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberEqualityMatchingRuleImpl.java                |   60 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleTest.java                      |   44 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java                                       |  841 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequest.java                                |   94 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableBindRequest.java                      |   50 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java                 |  180 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferReaderTestCase.java                                |   40 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                                               |  223 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeOptions.java                                                 |  181 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewRequestControl.java                        |  483 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java              |   77 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_de.properties                             |   19 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleTest.java                   |   52 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapPromise.java                                                   |   66 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1WriterTestCase.java                                              |  595 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                                           | 1126 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StringPrepProfile.java                                             |  508 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultHandler.java                                           |   63 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java                                       |  198 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java                      |  378 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestTestCase.java                       |  108 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDeleteRequestImpl.java                        |   51 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/assembly/examples.xml                                                                |   39 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnectionFactory.java                                     |   88 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java                   |   63 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java                                     |  152 
 opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java                            |  160 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/OperatingSystem.java                                               |  247 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OrderingMatchingRuleTest.java                               |  101 
 opendj-sdk/opendj-rest2ldap/pom.xml                                                                                               |  100 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SchemaResolver.java                                                |   60 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseImpl.java                                           |   82 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxTestCase.java                                 |   58 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ModifyAsync.java                                  |  167 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java                                              |  123 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapException.java                                                 |  213 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequest.java                                        |  160 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyErrorType.java                              |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerFirstComponentEqualityMatchingRuleImpl.java          |   89 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java                                                 |  812 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericExtendedResultImpl.java               |   39 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringBuilderTestCase.java                                     |  444 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AbandonRequestTestCase.java                               |   77 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Collections2.java                                                  |  260 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ParseAttributes.java                              |  153 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DataProviderIterator.java                                          |   51 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java                                        |  496 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryFactory.java                                                  |   40 
 opendj-sdk/opendj-ldap-toolkit/pom.xml                                                                                            |  327 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResultImpl.java                           |   91 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java                                                  |  366 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestTestCase.java                        |   96 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroupAsync.java                             |  218 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/package-info.java                                        |   21 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleTestCase.java                                 |  161 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameSyntaxImpl.java                            |   67 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnectionWrapper.java                                     |  635 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java                       |  202 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriter.java                                        |  392 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java                      | 1054 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionPoolTestCase.java                                        |  539 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestImpl.java                         |   83 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadResponseControl.java                              |  210 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAnonymousSASLBindRequestImpl.java             |   39 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxTest.java                                  |  143 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaUtilsTest.java                                        |  252 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestTestCase.java                      |   75 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java                                                       |  743 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/EntryChangeNotificationResponseControl.java               |  344 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java                                                            | 1046 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSubstringMatchingRuleImpl.java                   |   44 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedLDIFListener.java                                          |  164 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResultImpl.java                                      |   80 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java                                     |  163 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestImpl.java                           |  181 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestImpl.java                            |  503 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateResultCodeDocMojo.java                    |  174 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java                             |  709 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java                         |  151 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java                   |  116 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxTest.java                                         |  116 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractSASLBindRequest.java                              |   50 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifsearch                                                                   |   26 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableExtendedResultImpl.java              |   48 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StaticUtilsTestCase.java                                           |  332 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AssertionFailureException.java                                     |   33 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperation.java                       |   38 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequestImpl.java                                    |  118 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/package-info.java                                         |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractOrderingMatchingRuleImpl.java                       |  105 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequestDecoder.java                               |   51 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GSERParser.java                                                    |  396 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityRequestControl.java                  |  158 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPListenerImpl.java                                          |   48 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StringPrepProfileTestCase.java                                     |   93 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/searchrate.bat                                                               |   22 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_pl.properties                             |   20 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLifeAsync.java                               |  217 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifmodify                                                                   |   27 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResultDecoder.java                       |   93 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequestImpl.java                               |  121 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/EntryGeneratorTestCase.java                                        |  591 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SortKey.java                                                       |  538 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Assertion.java                                                     |   70 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortResponseControl.java                        |  310 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxTest.java                                      |  172 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringSyntaxImpl.java                              |   73 
 opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ReferenceCountedObjectTestCase.java                                |  151 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java                                                 | 1138 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleImpl.java                     |   50 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxDescriptionSyntaxImpl.java                        |  163 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResultImpl.java                    |  122 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SimplePagedResultsControl.java                            |  315 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ProxyBackend.java                                 |  260 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java              |   52 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequest.java                                           |  119 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBind.java                                   |  103 
 opendj-sdk/opendj-cli/src/main/resources/templates/refSect1.ftl                                                                   |   32 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java                          |  109 
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ja.properties                             |   19 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanMatchingRuleTest.java                      |   87 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleImpl.java                     |   40 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxImpl.java                                    |   83 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolRefDocContainer.java                                             |   80 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Result.java                                              |  188 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionPool.java                                                |   81 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandUsageHandler.java                                          |   46 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AVATestCase.java                                                   |   83 
 opendj-sdk/opendj-ldap-sdk-examples/src/test/bin/checkRewriterProxy.sh                                                            |  100 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SupportedAlgorithmSyntaxImpl.java                           |   66 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgSubcommand.ftl                                                            |   97 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java                         |   51 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java              |  208 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java                      |   84 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LinkedAttributeTestCase.java                                       |  421 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java                                                |  329 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResult.java                               |  120 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyRequestTestCase.java                                |  109 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionEventListener.java                                       |   70 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleImpl.java                  |   54 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleImpl.java                        |   49 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java                                        |  314 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/StringArgument.java                                                  |   61 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameAndOptionalUIDSyntaxImpl.java                           |  103 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequestImpl.java                                    |   47 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListEqualityMatchingRuleImpl.java                 |   41 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java                                                    |  569 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java                                                      |  355 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxImpl.java                                  |  200 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java                                             |  300 
 opendj-sdk/opendj-doc-maven-plugin/pom.xml                                                                                        |   95 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_CN.properties                                         |   86 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UnknownSchemaElementException.java                          |   37 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxTest.java                                         |   48 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequest.java                                        |  105 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractMatchingRuleImpl.java                               |  115 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java                               |  503 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Reader.java                                                      |  421 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ByteSequenceReaderTestCase.java                                  |   32 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ko.properties                                               |   17 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/RealAttributesOnlyRequestControl.java                     |  130 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePlainSASLBindRequestImpl.java                 |   66 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionFactory.java                                             |   93 
 opendj-sdk/opendj-grizzly/pom.xml                                                                                                 |  146 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControl.java                       |  132 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java                                |  285 
 opendj-sdk/opendj-cli/src/main/resources/templates/refSect2.ftl                                                                   |   65 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java                                            |  698 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleImpl.java                      |   40 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVariableList.ftl                                                          |   40 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Modify.java                                       |  122 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java                                        |  516 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_es.properties                                               |   17 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPMessageHandler.java                                              |  389 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateGlobalAcisTableMojo.java                  |  188 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java                                             | 2362 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleTest.java                      |   50 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java                                          |   85 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/DataSource.java                                      |  416 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxImpl.java                                |   84 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResult.java                                      |  110 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java                        |  462 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java                                  |  544 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequestImpl.java                                   |  125 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleTest.java                  |   99 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiChoiceArgument.java                                             |  150 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LoadBalancerTestCase.java                                          |  179 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleTest.java                     |   97 
 opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java                                           |   96 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchema.java                                    |  140 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties                                               | 1649 
 opendj-sdk/opendj-cli/src/main/resources/templates/dscfgAppendProps.ftl                                                           |   24 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringEqualityMatchingRuleImpl.java                    |   39 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java                                     |  328 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactory.java                                         |   46 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxTest.java                              |   48 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceReaderTest.java                                        |  369 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java                                           |  550 
 opendj-sdk/opendj-ldap-toolkit/src/test/resources/dummy-truststore                                                                |    0 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java                                                |  316 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template                                              |   32 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFModify.java                                      |  290 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java                               |  703 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxTest.java                                    |   44 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/VersionHandler.java                                                  |   27 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/PasswordResetForAD.java                           |  171 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java                                           |  220 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/searchrate                                                                   |   27 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterables.java                                                     |  325 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionLoadBalancer.java                                        |   61 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestTestCase.java                        |  201 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java                                             |  127 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationSubstringMatchingRuleTest.java                     |  156 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CachedConnectionPool.java                                          |  925 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java                                                    |  319 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexQueryFactory.java                                         |  107 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java                                   |  124 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ReadSchema.java                                   |  117 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapResultHandler.java                                             |   61 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java                                            |  148 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV2RequestControl.java                          |  231 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxImpl.java                               |  141 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/PackedLong.java                                                    |  235 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java                                                  |  309 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericExtendedRequestImpl.java               |   41 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1ByteSequenceReader.java                                          |  400 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java                                                    |   41 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/BooleanArgument.java                                                 |   89 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleImpl.java                      |   66 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexingOptions.java                                           |   33 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Provider.java                                                  |   35 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPClientContext.java                                             |  162 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AssertionRequestControl.java                              |  193 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClass.java                                            |  961 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractSynchronousConnection.java                                 |  153 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionException.java                                           |   31 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java                                                       | 1103 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestImpl.java                                |   97 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DelayedSchema.java                                          |   31 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/makeldif.bat                                                                 |   21 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java                           |  119 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnUpdatePolicy.java                                  |   48 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj_logo_sm.png                                               |    0 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FacsimileNumberSyntaxImpl.java                              |  168 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestTestCase.java                |  140 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RootDSE.java                                                       |  425 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleImpl.java                   |   46 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyRequestControl.java                         |  171 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxTest.java                                         |   77 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseGenericControl.java                            |  162 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/org/forgerock/opendj/maven/doc/docs.properties                              |   68 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriter.java                                  |  210 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleTest.java                    |  110 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/UnbindRequestTestCase.java                                |   57 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java                                        |   21 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchScope.java                                                   |  204 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java                                            |  363 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/appendix-ldap-result-codes.ftl                                    |   74 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/modrate                                                                      |   27 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReference.java                               |   67 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificatePairSyntaxImpl.java                              |   64 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java                                                           |  576 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/StatsThread.java                                     |  418 
 opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java                                 |  133 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestTestCase.java                            |   98 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java                         |  158 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifdiff                                                                     |   26 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1.java                                                            |  327 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryWriter.java                                               |  293 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleImpl.java                        |   43 
 opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/table-global-acis.ftl                                             |   60 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFITCase.java                                  |  168 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactoryAdapter.java                                  |  682 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxImpl.java                                 |  116 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleImpl.java            |   41 
 opendj-sdk/opendj-rest2ldap/src/main/resources/META-INF/services/org.forgerock.http.HttpApplication                               |   16 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentConstants.java                                               |  390 
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/streets                                                       |   74 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attribute.java                                                     |  438 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java                                            |  101 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java                                              |  135 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Syntax.java                                                 |  473 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java                                                    |  958 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePasswordModifyExtendedRequestImpl.java        |   81 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxImpl.java                                      |  695 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java                                        | 1219 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java                                        |  239 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java                                                    | 1396 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifmodify.bat                                                               |   21 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Predicate.java                                                     |   43 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java                |  116 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java                           |  180 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaOptionsTestCase.java                                  |   75 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ko.properties                                            |   86 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/BindRequestTestCase.java                                  |   61 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxImpl.java                                 |  141 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewResponseControl.java                       |  322 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableResultImpl.java                              |   26 
 opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_CN.properties                                            |   17 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControl.java                      |  451 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java                                                  |   99 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxTest.java                                |   47 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelexSyntaxTest.java                                        |   40 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxTest.java                               |   65 
 opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/package-info.java                                 |   20 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestContext.java                                                |   96 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImpl.java                      |  532 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleTest.java  |   47 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PermissiveModifyRequestControl.java                       |  158 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResultImpl.java                                   |   63 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleImpl.java                  |   39 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleImpl.java            |   41 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtilsTestCase.java                                         |   43 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeException.java                                               |  110 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ProviderNotFoundException.java                                     |   66 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableWhoAmIExtendedRequestImpl.java                |   30 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ExtendedOperations.java                           |  197 
 opendj-sdk/opendj-ldap-sdk-examples/src/site/xdoc/index.xml.vm                                                                    |  188 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFDiff.java                                        |  197 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java                                                      |  253 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeUsage.java                                         |   92 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DeleteRequestTestCase.java                                |   87 
 opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/VirtualAttributesOnlyRequestControl.java                  |  130 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestTestCase.java                              |  125 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RequestLoadBalancerTestCase.java                                   |  950 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java                                          | 1094 
 915 files changed, 226,109 insertions(+), 0 deletions(-)

diff --git a/opendj-sdk/opendj-cli/pom.xml b/opendj-sdk/opendj-cli/pom.xml
new file mode 100644
index 0000000..cc2362f
--- /dev/null
+++ b/opendj-sdk/opendj-cli/pom.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2014-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-cli</artifactId>
+    <name>OpenDJ CLI API</name>
+    <description>This module includes CLI API for implementing CLI applications.</description>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock</groupId>
+            <artifactId>forgerock-build-tools</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>2.3.21</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+
+    <properties>
+        <opendj.osgi.import.additional>
+            org.forgerock.opendj.*;provide:=true
+        </opendj.osgi.import.additional>
+    </properties>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate-messages</goal>
+                        </goals>
+                        <configuration>
+                            <messageFiles>
+                                <messageFile>com/forgerock/opendj/cli/cli.properties</messageFile>
+                            </messageFiles>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>cobertura-maven-plugin</artifactId>
+            </plugin>
+
+            <!-- Creates opendj-cli bundle -->
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>com.forgerock.opendj.cli</Export-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <reportSets>
+                    <reportSet>
+                        <reports>
+                            <report>mailing-list</report>
+                            <report>issue-tracking</report>
+                            <report>license</report>
+                            <report>cim</report>
+                            <report>distribution-management</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <links>
+                        <link>http://commons.forgerock.org/i18n-framework/i18n-core/apidocs</link>
+                    </links>
+                </configuration>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ApplicationKeyManager.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ApplicationKeyManager.java
new file mode 100644
index 0000000..160870d
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ApplicationKeyManager.java
@@ -0,0 +1,261 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2009 Parametric Technology Corporation (PTC)
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509KeyManager;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+
+/**
+ * This class is in charge of checking whether the certificates that are
+ * presented are trusted or not. This implementation tries to check also that
+ * the subject DN of the certificate corresponds to the host passed using the
+ * setHostName method.
+ * <p>
+ * The constructor tries to use a default TrustManager from the system and if it
+ * cannot be retrieved this class will only accept the certificates explicitly
+ * accepted by the user (and specified by calling acceptCertificate).
+ * <p>
+ * NOTE: this class is not aimed to be used when we have connections in
+ * parallel.
+ */
+final class ApplicationKeyManager implements X509KeyManager {
+    private static final LocalizedLogger LOG = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * The default keyManager.
+     */
+    private X509KeyManager keyManager;
+
+    /**
+     * The default constructor.
+     *
+     * @param keystore
+     *            The key store to use for this key manager.
+     * @param password
+     *            The key store password to use for this key manager.
+     */
+    ApplicationKeyManager(final KeyStore keystore, final char[] password) {
+        KeyManagerFactory kmf = null;
+        String userSpecifiedAlgo = System.getProperty("org.opendj.admin.keymanageralgo");
+        String userSpecifiedProvider = System.getProperty("org.opendj.admin.keymanagerprovider");
+
+        // Handle IBM specific cases if the user did not specify a algorithm
+        // and/or provider.
+        final String vendor = System.getProperty("java.vendor");
+        if (vendor.startsWith("IBM")) {
+            if (userSpecifiedAlgo == null) {
+                userSpecifiedAlgo = "IbmX509";
+            }
+            if (userSpecifiedProvider == null) {
+                userSpecifiedProvider = "IBMJSSE2";
+            }
+        }
+
+        // Have some fall backs to choose the provider and algorithm of the
+        // key manager. First see if the user wanted to use something
+        // specific, then try with the SunJSSE provider and SunX509
+        // algorithm. Finally, fall back to the default algorithm of the JVM.
+        final String[] preferredProvider = { userSpecifiedProvider, "SunJSSE", null, null };
+        final String[] preferredAlgo = { userSpecifiedAlgo, "SunX509", "SunX509",
+                    TrustManagerFactory.getDefaultAlgorithm() };
+        for (int i = 0; i < preferredProvider.length && keyManager == null; i++) {
+            final String provider = preferredProvider[i];
+            final String algo = preferredAlgo[i];
+            if (algo == null) {
+                continue;
+            }
+            try {
+                if (provider != null) {
+                    kmf = KeyManagerFactory.getInstance(algo, provider);
+                } else {
+                    kmf = KeyManagerFactory.getInstance(algo);
+                }
+                kmf.init(keystore, password);
+                final KeyManager[] kms = kmf.getKeyManagers();
+                /*
+                 * Iterate over the returned key managers, look for an instance
+                 * of X509KeyManager. If found, use that as our "default" key
+                 * manager.
+                 */
+                for (final KeyManager km : kms) {
+                    if (km instanceof X509KeyManager) {
+                        keyManager = (X509KeyManager) km;
+                        break;
+                    }
+                }
+            } catch (final NoSuchAlgorithmException e) {
+                // Nothing to do. Maybe we should avoid this and be strict, but
+                // we are in a best effort mode.
+                LOG.warn(LocalizableMessage.raw("Error with the algorithm", e));
+            } catch (final KeyStoreException e) {
+                // Nothing to do. Maybe we should avoid this and be strict, but
+                // we are in a best effort mode..
+                LOG.warn(LocalizableMessage.raw("Error with the keystore", e));
+            } catch (final UnrecoverableKeyException e) {
+                // Nothing to do. Maybe we should avoid this and be strict, but
+                // we are in a best effort mode.
+                LOG.warn(LocalizableMessage.raw("Error with the key", e));
+            } catch (final NoSuchProviderException e) {
+                // Nothing to do. Maybe we should avoid this and be strict, but
+                // we are in a best effort mode.
+                LOG.warn(LocalizableMessage.raw("Error with the provider", e));
+            }
+        }
+    }
+
+    /**
+     * Choose an alias to authenticate the client side of a secure socket given
+     * the public key type and the list of certificate issuer authorities
+     * recognized by the peer (if any).
+     *
+     * @param keyType
+     *            the key algorithm type name(s), ordered with the
+     *            most-preferred key type first.
+     * @param issuers
+     *            the list of acceptable CA issuer subject names or null if it
+     *            does not matter which issuers are used.
+     * @param socket
+     *            the socket to be used for this connection. This parameter can
+     *            be null, in which case this method will return the most
+     *            generic alias to use.
+     * @return the alias name for the desired key, or null if there are no
+     *         matches.
+     */
+    @Override
+    public String chooseClientAlias(final String[] keyType, final Principal[] issuers,
+            final Socket socket) {
+        if (keyManager != null) {
+            return keyManager.chooseClientAlias(keyType, issuers, socket);
+        }
+        return null;
+    }
+
+    /**
+     * Choose an alias to authenticate the client side of a secure socket given
+     * the public key type and the list of certificate issuer authorities
+     * recognized by the peer (if any).
+     *
+     * @param keyType
+     *            the key algorithm type name(s), ordered with the
+     *            most-preferred key type first.
+     * @param issuers
+     *            the list of acceptable CA issuer subject names or null if it
+     *            does not matter which issuers are used.
+     * @param socket
+     *            the socket to be used for this connection. This parameter can
+     *            be null, in which case this method will return the most
+     *            generic alias to use.
+     * @return the alias name for the desired key, or null if there are no
+     *         matches.
+     */
+    @Override
+    public String chooseServerAlias(final String keyType, final Principal[] issuers, final Socket socket) {
+        if (keyManager != null) {
+            return keyManager.chooseServerAlias(keyType, issuers, socket);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the certificate chain associated with the given alias.
+     *
+     * @param alias
+     *            the alias name
+     * @return the certificate chain (ordered with the user's certificate first
+     *         and the root certificate authority last), or null if the alias
+     *         can't be found.
+     */
+    @Override
+    public X509Certificate[] getCertificateChain(final String alias) {
+        if (keyManager != null) {
+            return keyManager.getCertificateChain(alias);
+        }
+        return null;
+    }
+
+    /**
+     * Get the matching aliases for authenticating the server side of a secure
+     * socket given the public key type and the list of certificate issuer
+     * authorities recognized by the peer (if any).
+     *
+     * @param keyType
+     *            the key algorithm type name
+     * @param issuers
+     *            the list of acceptable CA issuer subject names or null if it
+     *            does not matter which issuers are used.
+     * @return an array of the matching alias names, or null if there were no
+     *         matches.
+     */
+    @Override
+    public String[] getClientAliases(final String keyType, final Principal[] issuers) {
+        if (keyManager != null) {
+            return keyManager.getClientAliases(keyType, issuers);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the key associated with the given alias.
+     *
+     * @param alias
+     *            the alias name
+     * @return the requested key, or null if the alias can't be found.
+     */
+    @Override
+    public PrivateKey getPrivateKey(final String alias) {
+        if (keyManager != null) {
+            return keyManager.getPrivateKey(alias);
+        }
+        return null;
+    }
+
+    /**
+     * Get the matching aliases for authenticating the server side of a secure
+     * socket given the public key type and the list of certificate issuer
+     * authorities recognized by the peer (if any).
+     *
+     * @param keyType
+     *            the key algorithm type name
+     * @param issuers
+     *            the list of acceptable CA issuer subject names or null if it
+     *            does not matter which issuers are used.
+     * @return an array of the matching alias names, or null if there were no
+     *         matches.
+     */
+    @Override
+    public String[] getServerAliases(final String keyType, final Principal[] issuers) {
+        if (keyManager != null) {
+            return keyManager.getServerAliases(keyType, issuers);
+        }
+        return null;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java
new file mode 100644
index 0000000..db3da96
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Argument.java
@@ -0,0 +1,522 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines a generic argument that may be used in the argument list
+ * for an application. This is an abstract class that must be subclassed in
+ * order to provide specific functionality.
+ */
+public abstract class Argument implements DocDescriptionSupplement {
+
+    /**
+     * An abstract base class to build a generic {@link Argument}.
+     *
+     * @param <B>
+     *         The concrete {@link ArgumentBuilder} subclass.
+     * @param <T>
+     *         The default value type of the {@link Argument}.
+     * @param <A>
+     *         The concrete {@link Argument} type to build.
+     */
+    static abstract class ArgumentBuilder<B extends ArgumentBuilder<B, T, A>, T, A extends Argument> {
+        T defaultValue;
+        LocalizableMessage description;
+        LocalizableMessage docDescriptionSupplement;
+        boolean hidden;
+        final String longIdentifier;
+        boolean multiValued;
+        boolean needsValue = true;
+        boolean required;
+        Character shortIdentifier;
+        LocalizableMessage valuePlaceholder;
+
+        ArgumentBuilder(final String longIdentifier) {
+            Reject.ifNull(longIdentifier, "An argument must have a long identifier");
+            this.longIdentifier = longIdentifier;
+        }
+
+        abstract B getThis();
+
+        /**
+         * Build the argument.
+         *
+         * @return The argument built.
+         * @throws ArgumentException
+         *         If there is a problem with any of the parameters used to
+         *         create this argument.
+         */
+        public abstract A buildArgument() throws ArgumentException;
+
+        /**
+         * Build the argument and add it to the provided {@link ArgumentParser}.
+         *
+         * @param parser
+         *         The argument parser.
+         * @return The argument built.
+         * @throws ArgumentException
+         *         If there is a problem with any of the parameters used to
+         *         create this argument.
+         */
+        public A buildAndAddToParser(final ArgumentParser parser) throws ArgumentException {
+            final A arg = buildArgument();
+            parser.addArgument(arg);
+            return arg;
+        }
+
+        /**
+         * Build the argument and add it to the provided {@link SubCommand}.
+         *
+         * @param subCommand
+         *         The sub command.
+         * @return The argument built.
+         * @throws ArgumentException
+         *         If there is a problem with any of the parameters used to
+         *         create this argument.
+         */
+        public A buildAndAddToSubCommand(final SubCommand subCommand) throws ArgumentException {
+            final A arg = buildArgument();
+            subCommand.addArgument(arg);
+            return arg;
+        }
+
+        /**
+         * Sets this argument default value.
+         *
+         * @param defaultValue
+         *         The default value.
+         * @return This builder.
+         */
+        public B defaultValue(final T defaultValue) {
+            this.defaultValue = defaultValue;
+            return getThis();
+        }
+
+        /**
+         * Sets this argument description.
+         *
+         * @param description
+         *         The localized description.
+         * @return This builder.
+         */
+        public B description(final LocalizableMessage description) {
+            this.description = description;
+            return getThis();
+        }
+
+        /**
+         * Sets a supplement to the description intended for use in generated reference documentation.
+         *
+         * @param docDescriptionSupplement
+         *         The supplement to the description for use in generated reference documentation.
+         * @return This builder.
+         */
+        public B docDescriptionSupplement(final LocalizableMessage docDescriptionSupplement) {
+            this.docDescriptionSupplement = docDescriptionSupplement;
+            return getThis();
+        }
+
+        /**
+         * Specifies that this argument is hidden.
+         *
+         * @return This builder.
+         */
+        public B hidden() {
+            this.hidden = true;
+            return getThis();
+        }
+
+        /**
+         * Specifies that this argument may have multiple values.
+         *
+         * @return This builder.
+         */
+        public B multiValued() {
+            this.multiValued = true;
+            return getThis();
+        }
+
+        /**
+         * Specifies that this argument is required.
+         *
+         * @return This builder.
+         */
+        public B required() {
+            this.required = true;
+            return getThis();
+        }
+
+        /**
+         * Sets this argument single-character identifier.
+         *
+         * @param shortIdentifier
+         *         The single-character identifier.
+         * @return This builder.
+         */
+        public B shortIdentifier(final Character shortIdentifier) {
+            this.shortIdentifier = shortIdentifier;
+            return getThis();
+        }
+
+        /**
+         * Sets this argument value placeholder, which will be used in usage information.
+         *
+         * @param valuePlaceholder
+         *         The localized value placeholder.
+         * @return This builder.
+         */
+        public B valuePlaceholder(final LocalizableMessage valuePlaceholder) {
+            this.valuePlaceholder = valuePlaceholder;
+            return getThis();
+        }
+    }
+
+    /** The long identifier for this argument. */
+    final String longIdentifier;
+
+    /** The single-character identifier for this argument. */
+    private final Character shortIdentifier;
+    /** The unique ID of the description for this argument. */
+    private final LocalizableMessage description;
+    /** Indicates whether this argument should be hidden in the usage information. */
+    private final boolean isHidden;
+    /** Indicates whether this argument may be specified more than once for multiple values. */
+    private final boolean isMultiValued;
+    /** Indicates whether this argument is required to have a value. */
+    private final boolean isRequired;
+    /** Indicates whether this argument requires a value. */
+    private final boolean needsValue;
+    /** The default value for the argument if none other is provided. */
+    private final String defaultValue;
+    /** The value placeholder for this argument, which will be used in usage information. */
+    private final LocalizableMessage valuePlaceholder;
+
+    /** The set of values for this argument. */
+    private final LinkedList<String> values = new LinkedList<>();
+
+    /** Indicates whether this argument was provided in the set of command-line arguments. */
+    private boolean isPresent;
+
+    /**
+     * Indicates whether this argument was provided in the set of
+     * properties found in a properties file.
+     */
+    private boolean isValueSetByProperty;
+
+    <B extends ArgumentBuilder<B, T, A>, T, A extends Argument> Argument(final ArgumentBuilder<B, T, A> builder)
+            throws ArgumentException {
+        this.shortIdentifier = builder.shortIdentifier;
+        this.longIdentifier = builder.longIdentifier;
+        this.isRequired = builder.required;
+        this.isMultiValued = builder.multiValued;
+        this.needsValue = builder.needsValue;
+        this.valuePlaceholder = builder.valuePlaceholder;
+        this.defaultValue = builder.defaultValue != null ? String.valueOf(builder.defaultValue) : null;
+        this.description = builder.description;
+        this.isHidden = builder.hidden;
+        this.docDescriptionSupplement = builder.docDescriptionSupplement;
+
+        if (needsValue && valuePlaceholder == null) {
+            throw new ArgumentException(ERR_ARG_NO_VALUE_PLACEHOLDER.get(longIdentifier));
+        }
+
+        isPresent = false;
+    }
+
+    /**
+     * Adds a value to the set of values for this argument. This should only be
+     * called if the value is allowed by the <CODE>valueIsAcceptable</CODE>
+     * method.
+     *
+     * @param valueString
+     *            The string representation of the value to add to this
+     *            argument.
+     */
+    public void addValue(final String valueString) {
+        if (!isMultiValued) {
+            clearValues();
+        }
+        values.add(valueString);
+    }
+
+    /**
+     * Clears the set of values assigned to this argument.
+     */
+    public void clearValues() {
+        values.clear();
+    }
+
+    /**
+     * Retrieves the default value that will be used for this argument if it is
+     * not specified on the command line and it is not set from a properties
+     * file.
+     *
+     * @return The default value that will be used for this argument if it is
+     *         not specified on the command line and it is not set from a
+     *         properties file, or <CODE>null</CODE> if there is no default
+     *         value.
+     */
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    /**
+     * Retrieves the human-readable description for this argument.
+     *
+     * @return The human-readable description for this argument.
+     */
+    public LocalizableMessage getDescription() {
+        return description != null ? description : LocalizableMessage.EMPTY;
+    }
+
+    /** A supplement to the description intended for use in generated reference documentation. */
+    private LocalizableMessage docDescriptionSupplement;
+
+    @Override
+    public LocalizableMessage getDocDescriptionSupplement() {
+        return docDescriptionSupplement != null ? docDescriptionSupplement : LocalizableMessage.EMPTY;
+    }
+
+    /**
+     * Retrieves the value of this argument as an integer.
+     *
+     * @return The value of this argument as an integer.
+     * @throws ArgumentException
+     *             If there are multiple values, or the value cannot be parsed
+     *             as an integer.
+     */
+    public int getIntValue() throws ArgumentException {
+        if (values.isEmpty()) {
+            throw new ArgumentException(ERR_ARG_NO_INT_VALUE.get(longIdentifier));
+        }
+
+        final Iterator<String> iterator = values.iterator();
+        final String valueString = iterator.next();
+        if (iterator.hasNext()) {
+            throw new ArgumentException(ERR_ARG_INT_MULTIPLE_VALUES.get(longIdentifier));
+        }
+
+        try {
+            return Integer.parseInt(valueString);
+        } catch (final Exception e) {
+            throw new ArgumentException(ERR_ARG_CANNOT_DECODE_AS_INT.get(valueString, longIdentifier), e);
+        }
+    }
+
+    /**
+     * Retrieves the long (multi-character) identifier that may be used to
+     * specify the value of this argument.
+     *
+     * @return The long (multi-character) identifier that may be used to specify
+     *         the value of this argument.
+     */
+    public String getLongIdentifier() {
+        return longIdentifier;
+    }
+
+    /**
+     * Retrieves the single-character identifier that may be used to specify the
+     * value of this argument.
+     *
+     * @return The single-character identifier that may be used to specify the
+     *         value of this argument, or <CODE>null</CODE> if there is none.
+     */
+    public Character getShortIdentifier() {
+        return shortIdentifier;
+    }
+
+    /**
+     * Retrieves the string vale for this argument. If it has multiple values,
+     * then the first will be returned. If it does not have any values, then the
+     * default value will be returned.
+     *
+     * @return The string value for this argument, or <CODE>null</CODE> if there
+     *         are no values and no default value has been given.
+     */
+    public String getValue() {
+        return !values.isEmpty() ? values.getFirst() : defaultValue;
+    }
+
+    /**
+     * Retrieves the value placeholder that will be displayed for this argument
+     * in the generated usage information.
+     *
+     * @return The value placeholder that will be displayed for this argument in
+     *         the generated usage information, or <CODE>null</CODE> if there is none.
+     */
+    public LocalizableMessage getValuePlaceholder() {
+        return valuePlaceholder;
+    }
+
+    /**
+     * Retrieves the set of string values for this argument.
+     *
+     * @return The set of string values for this argument.
+     */
+    public List<String> getValues() {
+        return values;
+    }
+
+    /**
+     * Indicates whether this argument has at least one value.
+     *
+     * @return <CODE>true</CODE> if this argument has at least one value, or
+     *         <CODE>false</CODE> if it does not have any values.
+     */
+    public boolean hasValue() {
+        return !values.isEmpty();
+    }
+
+    /**
+     * Indicates whether this argument should be hidden from the usage
+     * information.
+     *
+     * @return <CODE>true</CODE> if this argument should be hidden from the
+     *         usage information, or <CODE>false</CODE> if not.
+     */
+    public boolean isHidden() {
+        return isHidden;
+    }
+
+    /**
+     * Indicates whether this argument may be provided more than once on the
+     * command line to specify multiple values.
+     *
+     * @return <CODE>true</CODE> if this argument may be provided more than once
+     *         on the command line to specify multiple values, or
+     *         <CODE>false</CODE> if it may have at most one value.
+     */
+    public boolean isMultiValued() {
+        return isMultiValued;
+    }
+
+    /**
+     * Indicates whether this argument is present in the parsed set of
+     * command-line arguments.
+     *
+     * @return <CODE>true</CODE> if this argument is present in the parsed set
+     *         of command-line arguments, or <CODE>false</CODE> if not.
+     */
+    public boolean isPresent() {
+        return isPresent;
+    }
+
+    /**
+     * Indicates whether this argument is required to have at least one value.
+     *
+     * @return <CODE>true</CODE> if this argument is required to have at least
+     *         one value, or <CODE>false</CODE> if it does not need to have a value.
+     */
+    public boolean isRequired() {
+        return isRequired;
+    }
+
+    /**
+     * Indicates whether this argument was provided in the set of properties
+     * found is a properties file.
+     *
+     * @return <CODE>true</CODE> if this argument was provided in the set of
+     *         properties found is a properties file, or <CODE>false</CODE> if not.
+     */
+    public boolean isValueSetByProperty() {
+        return isValueSetByProperty;
+    }
+
+    /**
+     * Indicates whether a value must be provided with this argument if it is
+     * present.
+     *
+     * @return <CODE>true</CODE> if a value must be provided with the argument
+     *         if it is present, or <CODE>false</CODE> if the argument does not
+     *         take a value and the presence of the argument identifier itself
+     *         is sufficient to convey the necessary information.
+     */
+    public boolean needsValue() {
+        return needsValue;
+    }
+
+    /**
+     * Specifies whether this argument is present in the parsed set of
+     * command-line arguments.
+     *
+     * @param isPresent
+     *            Indicates whether this argument is present in the set of
+     *            command-line arguments.
+     */
+    public void setPresent(final boolean isPresent) {
+        this.isPresent = isPresent;
+    }
+
+    void valueSetByProperty() {
+        isValueSetByProperty = true;
+    }
+
+    /**
+     * Indicates whether the provided value is acceptable for use in this
+     * argument.
+     *
+     * @param valueString
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            A buffer into which the invalid reason may be written if the
+     *            value is not acceptable.
+     * @return <CODE>true</CODE> if the value is acceptable, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    public abstract boolean valueIsAcceptable(String valueString, LocalizableMessageBuilder invalidReason);
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append("(");
+        if (longIdentifier != null) {
+            sb.append("longID=");
+            sb.append(longIdentifier);
+        }
+        if (shortIdentifier != null) {
+            if (longIdentifier != null) {
+                sb.append(", ");
+            }
+            sb.append("shortID=");
+            sb.append(shortIdentifier);
+        }
+        sb.append(", values=").append(values);
+        sb.append(")");
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object arg) {
+        return this == arg || (arg instanceof Argument && ((Argument) arg).longIdentifier.equals(this.longIdentifier));
+    }
+
+    @Override
+    public int hashCode() {
+        return longIdentifier.hashCode();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentConstants.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentConstants.java
new file mode 100644
index 0000000..049c7cc
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentConstants.java
@@ -0,0 +1,390 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * This class contains short and long options used by the argument in CLI.
+ */
+public final class ArgumentConstants {
+
+    /**
+     * The name of the SASL property that can be used to provide the
+     * authentication ID for the bind.
+     */
+    public static final String SASL_PROPERTY_AUTHID = "authid";
+    /**
+     * The name of the SASL property that can be used to provide the
+     * authorization ID for the bind.
+     */
+    public static final String SASL_PROPERTY_AUTHZID = "authzid";
+    /**
+     * The name of the SASL property that can be used to provide the digest URI
+     * for the bind.
+     */
+    public static final String SASL_PROPERTY_DIGEST_URI = "digest-uri";
+    /**
+     * The name of the SASL property that can be used to provide the KDC for use
+     * in Kerberos authentication.
+     */
+    public static final String SASL_PROPERTY_KDC = "kdc";
+    /**
+     * The name of the SASL property that can be used to provide the quality of
+     * protection for the bind.
+     */
+    public static final String SASL_PROPERTY_QOP = "qop";
+    /**
+     * The name of the SASL property that can be used to provide the realm for
+     * the bind.
+     */
+    public static final String SASL_PROPERTY_REALM = "realm";
+    /**
+     * The name of the SASL property that can be used to provide the SASL
+     * mechanism to use.
+     */
+    public static final String SASL_PROPERTY_MECH = "mech";
+
+    /** The value for the long option batch. */
+    public static final String OPTION_LONG_BATCH = "batch";
+    /** The value for the short option batchFilePath. */
+    public static final char OPTION_SHORT_BATCH_FILE_PATH = 'F';
+    /** The value for the long option batchFilePath. */
+    public static final String OPTION_LONG_BATCH_FILE_PATH = "batchFilePath";
+
+    /** The value for the short option host name. */
+    public static final char OPTION_SHORT_HOST = 'h';
+    /** The value for the long option host name. */
+    public static final String OPTION_LONG_HOST = "hostname";
+
+    /** The value for the short option port. */
+    public static final char OPTION_SHORT_PORT = 'p';
+    /** The value for the long option port. */
+    public static final String OPTION_LONG_PORT = "port";
+
+    /** The value for the short option useSSL. */
+    public static final char OPTION_SHORT_USE_SSL = 'Z';
+    /** The value for the long option useSSL. */
+    public static final String OPTION_LONG_USE_SSL = "useSSL";
+
+    /** The value for the short option baseDN. */
+    public static final char OPTION_SHORT_BASEDN = 'b';
+    /** The value for the long option baseDN. */
+    public static final String OPTION_LONG_BASEDN = "baseDN";
+
+    /** The value for the short option bindDN. */
+    public static final char OPTION_SHORT_BINDDN = 'D';
+    /** The value for the long option bindDN. */
+    public static final String OPTION_LONG_BINDDN = "bindDN";
+
+    /** The value for the short option bindPassword. */
+    public static final char OPTION_SHORT_BINDPWD = 'w';
+    /** The value for the long option bindPassword. */
+    public static final String OPTION_LONG_BINDPWD = "bindPassword";
+
+    /** The value for the short option bindPasswordFile. */
+    public static final char OPTION_SHORT_BINDPWD_FILE = 'j';
+    /** The value for the long option bindPasswordFile. */
+    public static final String OPTION_LONG_BINDPWD_FILE = "bindPasswordFile";
+
+    /** The value for the short option compress. */
+    public static final char OPTION_SHORT_COMPRESS = 'c';
+    /** The value for the long option compress. */
+    public static final String OPTION_LONG_COMPRESS = "compress";
+
+    /** The value for the short option filename. */
+    public static final char OPTION_SHORT_FILENAME = 'f';
+    /** The value for the long option filename. */
+    public static final String OPTION_LONG_FILENAME = "filename";
+
+    /** The value for the short option ldifFile. */
+    public static final char OPTION_SHORT_LDIF_FILE = 'l';
+    /** The value for the long option ldifFile. */
+    public static final String OPTION_LONG_LDIF_FILE = "ldifFile";
+
+    /** The value for the short option useStartTLS. */
+    public static final char OPTION_SHORT_START_TLS = 'q';
+    /** The value for the long option useStartTLS. */
+    public static final String OPTION_LONG_START_TLS = "useStartTLS";
+
+    /** The value for the short option randomSeed. */
+    public static final char OPTION_SHORT_RANDOM_SEED = 's';
+    /** The value for the long option randomSeed. */
+    public static final String OPTION_LONG_RANDOM_SEED = "randomSeed";
+
+    /** The value for the short option keyStorePath. */
+    public static final char OPTION_SHORT_KEYSTOREPATH = 'K';
+    /** The value for the long option keyStorePath. */
+    public static final String OPTION_LONG_KEYSTOREPATH = "keyStorePath";
+
+    /** The value for the short option trustStorePath. */
+    public static final char OPTION_SHORT_TRUSTSTOREPATH = 'P';
+    /** The value for the long option trustStorePath. */
+    public static final String OPTION_LONG_TRUSTSTOREPATH = "trustStorePath";
+
+    /** The value for the short option keyStorePassword. */
+    public static final char OPTION_SHORT_KEYSTORE_PWD = 'W';
+    /** The value for the long option keyStorePassword. */
+    public static final String OPTION_LONG_KEYSTORE_PWD = "keyStorePassword";
+
+    /** The value for the short option trustStorePassword. */
+    public static final char OPTION_SHORT_TRUSTSTORE_PWD = 'T';
+    /** The value for the long option trustStorePassword. */
+    public static final String OPTION_LONG_TRUSTSTORE_PWD = "trustStorePassword";
+
+    /** The value for the short option keyStorePasswordFile . */
+    public static final char OPTION_SHORT_KEYSTORE_PWD_FILE = 'u';
+    /** The value for the long option keyStorePasswordFile . */
+    public static final String OPTION_LONG_KEYSTORE_PWD_FILE = "keyStorePasswordFile";
+
+    /** The value for the short option keyStorePasswordFile . */
+    public static final char OPTION_SHORT_TRUSTSTORE_PWD_FILE = 'U';
+    /** The value for the long option keyStorePasswordFile . */
+    public static final String OPTION_LONG_TRUSTSTORE_PWD_FILE = "trustStorePasswordFile";
+    /** The value for the short option trustAll . */
+    public static final char OPTION_SHORT_TRUSTALL = 'X';
+    /** The value for the long option trustAll . */
+    public static final String OPTION_LONG_TRUSTALL = "trustAll";
+
+    /** The value for the short option certNickname . */
+    public static final char OPTION_SHORT_CERT_NICKNAME = 'N';
+    /** The value for the long option certNickname . */
+    public static final String OPTION_LONG_CERT_NICKNAME = "certNickname";
+
+    /** The value for the long option assertionFilter . */
+    public static final String OPTION_LONG_ASSERTION_FILE = "assertionFilter";
+
+    /** The value for the short option dry-run. */
+    public static final char OPTION_SHORT_DRYRUN = 'n';
+    /** The value for the long option dry-run. */
+    public static final String OPTION_LONG_DRYRUN = "dry-run";
+
+    /** The value for the short option help. */
+    public static final char OPTION_SHORT_HELP = 'H';
+    /** The value for the long option help. */
+    public static final String OPTION_LONG_HELP = "help";
+
+    /** The value for the long option cli. */
+    public static final String OPTION_LONG_CLI = "cli";
+    /** The value for the short option cli. */
+    public static final char OPTION_SHORT_CLI = 'i';
+
+    /** The value for the short option proxyAs. */
+    public static final char OPTION_SHORT_PROXYAUTHID = 'Y';
+    /** The value for the long option proxyAs. */
+    public static final String OPTION_LONG_PROXYAUTHID = "proxyAs";
+
+    /** The value for the short option saslOption. */
+    public static final char OPTION_SHORT_SASLOPTION = 'o';
+    /** The value for the long option saslOption. */
+    public static final String OPTION_LONG_SASLOPTION = "saslOption";
+
+    /** The value for the short option searchScope. */
+    public static final  char OPTION_SHORT_SEARCHSCOPE = 's';
+    /** The value for the long option searchScope. */
+    public static final String OPTION_LONG_SEARCHSCOPE = "searchScope";
+
+    /** The value for the short option geteffectiverights control authzid. */
+    public static final char OPTION_SHORT_EFFECTIVERIGHTSUSER = 'g';
+    /** The value for the long option geteffectiverights control authzid. */
+    public static final String OPTION_LONG_EFFECTIVERIGHTSUSER = "getEffectiveRightsAuthzid";
+    /** The value for the short option geteffectiveights control attributes. */
+    public static final char OPTION_SHORT_EFFECTIVERIGHTSATTR = 'e';
+    /**
+     * The value for the long option geteffectiverights control specific
+     * attribute list.
+     */
+    public static final String OPTION_LONG_EFFECTIVERIGHTSATTR = "getEffectiveRightsAttribute";
+
+    /** The value for the short option protocol version attributes. */
+    public static final char OPTION_SHORT_PROTOCOL_VERSION = 'V';
+    /** The value for the long option protocol version attribute. */
+    public static final String OPTION_LONG_PROTOCOL_VERSION = "ldapVersion";
+
+    /** The value for the long option version. */
+    public static final char OPTION_SHORT_PRODUCT_VERSION = 'V';
+    /** The value for the long option version. */
+    public static final String OPTION_LONG_PRODUCT_VERSION = "version";
+
+    /** The value for the long "checkStoppability" {@link Argument}. */
+    public static final String OPTION_LONG_CHECK_STOPPABILITY = "checkStoppability";
+    /** The value for the long "windowsNetStop" {@link Argument}. */
+    public static final String OPTION_LONG_WINDOWS_NET_STOP = "windowsNetStop";
+
+    /** Value for the quiet option short form. */
+    public static final Character OPTION_SHORT_QUIET = 'Q';
+    /** Value for the quiet option long form. */
+    public static final String OPTION_LONG_QUIET = "quiet";
+
+    /** Value for non-interactive session short form. */
+    public static final Character OPTION_SHORT_NO_PROMPT = 'n';
+    /** Value for non-interactive session long form. */
+    public static final String OPTION_LONG_NO_PROMPT = "no-prompt";
+
+    /** Long form of script friendly option. */
+    public static final String OPTION_LONG_SCRIPT_FRIENDLY = "script-friendly";
+    /** Short form of script friendly option. */
+    public static final Character OPTION_SHORT_SCRIPT_FRIENDLY = 's';
+
+    /** Value for verbose option short form. */
+    public static final Character OPTION_SHORT_VERBOSE = 'v';
+    /** Value for verbose option long form. */
+    public static final String OPTION_LONG_VERBOSE = "verbose";
+
+    /** The value for the long option propertiesFilePAth . */
+    public static final String OPTION_LONG_PROP_FILE_PATH = "propertiesFilePath";
+    /** The value for the long option propertiesFilePAth . */
+    public static final String OPTION_LONG_NO_PROP_FILE = "noPropertiesFile";
+
+    /** Long form of referenced host name. */
+    public static final String OPTION_LONG_REFERENCED_HOST_NAME = "referencedHostName";
+
+    /** Long form of admin UID. */
+    public static final String OPTION_LONG_ADMIN_UID = "adminUID";
+
+    /** Long form of report authorization ID connection option. */
+    public static final String OPTION_LONG_REPORT_AUTHZ_ID = "reportAuthzID";
+
+    /** Long form of use password policy control connection option. */
+    public static final String OPTION_LONG_USE_PW_POLICY_CTL = "usePasswordPolicyControl";
+
+    /** Long form of use SASL external connection option. */
+    public static final String OPTION_LONG_USE_SASL_EXTERNAL = "useSASLExternal";
+
+    /** Long form of option for the command-line encoding option. */
+    public static final String OPTION_LONG_ENCODING = "encoding";
+
+    /** Long form of option specifying no wrapping of the command-line. */
+    public static final String OPTION_LONG_DONT_WRAP = "dontWrap";
+
+    /** The value for the long option targetDN. */
+    public static final String OPTION_LONG_TARGETDN = "targetDN";
+
+    /** Long form of email notification upon completion option. */
+    public static final String OPTION_LONG_COMPLETION_NOTIFICATION_EMAIL = "completionNotify";
+    /** Short form of email notification upon completion option. */
+    public static final Character OPTION_SHORT_COMPLETION_NOTIFICATION_EMAIL = null;
+
+    /** Long form of email notification upon error option. */
+    public static final String OPTION_LONG_ERROR_NOTIFICATION_EMAIL = "errorNotify";
+    /** Short form of email notification upon error option. */
+    public static final Character OPTION_SHORT_ERROR_NOTIFICATION_EMAIL = null;
+
+    /** Long form of dependency option. */
+    public static final String OPTION_LONG_DEPENDENCY = "dependency";
+    /** Short form of dependency option. */
+    public static final Character OPTION_SHORT_DEPENDENCY = null;
+
+    /** Long form of failed dependency action option. */
+    public static final String OPTION_LONG_FAILED_DEPENDENCY_ACTION = "failedDependencyAction";
+    /** Short form of failed dependency action option. */
+    public static final Character OPTION_SHORT_FAILED_DEPENDENCY_ACTION = null;
+
+    /** The default separator to be used in tables. */
+    public static final String LIST_TABLE_SEPARATOR = ":";
+
+    /** The value for the short option output LDIF filename. */
+    public static final char OPTION_SHORT_OUTPUT_LDIF_FILENAME = 'o';
+    /** The value for the long option output LDIF filename. */
+    public static final String OPTION_LONG_OUTPUT_LDIF_FILENAME = "outputLDIF";
+
+    /** The value for the long option to automatically accept the license if present. */
+    public static final String OPTION_LONG_ACCEPT_LICENSE = "acceptLicense";
+
+    /** The value for the short option rootUserDN. */
+    public static final char OPTION_SHORT_ROOT_USER_DN = 'D';
+    /** The value for the long option rootUserDN. */
+    public static final String OPTION_LONG_ROOT_USER_DN = "rootUserDN";
+
+    /** The value for the long option connect timeout attribute. */
+    public static final String OPTION_LONG_CONNECT_TIMEOUT = "connectTimeout";
+
+    /** The value for the long option advanced. */
+    public static final String OPTION_LONG_ADVANCED = "advanced";
+
+    /** Display the equivalent non-interactive command. */
+    public static final String OPTION_LONG_DISPLAY_EQUIVALENT = "displayCommand";
+
+    /** The path where we write the equivalent non-interactive command. */
+    public static final String OPTION_LONG_EQUIVALENT_COMMAND_FILE_PATH = "commandFilePath";
+
+    /** The value for the short option remote . */
+    public static final char OPTION_SHORT_REMOTE = 'r';
+    /** The value for the long option remote . */
+    public static final String OPTION_LONG_REMOTE = "remote";
+
+    /** The value for the short option configClass. */
+    public static final char OPTION_SHORT_CONFIG_CLASS = 'C';
+    /** The value for the long option configClass. */
+    public static final String OPTION_LONG_CONFIG_CLASS = "configClass";
+
+    /** Value for the server root option short form. */
+    public static final Character OPTION_SHORT_SERVER_ROOT = 'R';
+
+    /** Value for the server root option long form. */
+    public static final String OPTION_LONG_SERVER_ROOT = "serverRoot";
+
+    /** Value for the control option short form. */
+    public static final Character OPTION_SHORT_CONTROL = 'J';
+    /** Value for the control option long form. */
+    public static final String OPTION_LONG_CONTROL = "control";
+
+    /** Recurring task option long form. */
+    public static final String OPTION_LONG_RECURRING_TASK = "recurringTask";
+    /** Recurring task option short form. */
+    public static final Character OPTION_SHORT_RECURRING_TASK = null;
+
+    /** Subentries control option long form. */
+    public static final String OPTION_LONG_SUBENTRIES = "subEntries";
+    /** Subentries control option short form. */
+    public static final Character OPTION_SHORT_SUBENTRIES = null;
+
+    /** Scheduled start date/time option long form. */
+    public static final String OPTION_LONG_START_DATETIME = "start";
+    /** Scheduled start date/time option short form. */
+    public static final Character OPTION_SHORT_START_DATETIME = 't';
+
+    /** Long form of admin password. */
+    public static final String OPTION_LONG_ADMIN_PWD = "adminPassword";
+    /** Long form of admin password file. */
+    public static final String OPTION_LONG_ADMIN_PWD_FILE = "adminPasswordFile";
+
+    /**
+     * The name of the SASL property that can be used to provide trace information
+     * for a SASL ANONYMOUS request.
+     */
+    public static final String SASL_PROPERTY_TRACE = "trace";
+
+    /** The value for the long option force upgrade. */
+    public static final String OPTION_LONG_FORCE_UPGRADE = "force";
+
+    /** The value for the long option ignore errors. */
+    public static final String OPTION_LONG_IGNORE_ERRORS = "ignoreErrors";
+
+    /** Value for the restart option long form. */
+    public static final String OPTION_LONG_RESTART = "restart";
+
+    /** The value for the hidden testonly argument. */
+    public static final String OPTION_LONG_TESTONLY_ARGUMENT = "testOnly";
+
+    /** The value for the backend type long option. */
+    public static final String OPTION_LONG_BACKEND_TYPE = "backendType";
+
+    /** The value for the backend type short option. */
+    public static final Character OPTION_SHORT_BACKEND_TYPE = 't';
+
+    /** Prevent instantiation. */
+    private ArgumentConstants() {
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentException.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentException.java
new file mode 100644
index 0000000..6fbea18
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentException.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * This class defines an exception that may be thrown if there is a problem with
+ * an argument definition.
+ */
+@SuppressWarnings("serial")
+public final class ArgumentException extends Exception implements LocalizableException {
+    /** The I18N message associated with this exception. */
+    private final LocalizableMessage message;
+
+    /**
+     * Creates a new argument exception with the provided message.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     */
+    public ArgumentException(final LocalizableMessage message) {
+        super(String.valueOf(message));
+        this.message = message;
+    }
+
+    /**
+     * Creates a new argument exception with the provided message and root
+     * cause.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @param cause
+     *            The exception that was caught to trigger this exception.
+     */
+    public ArgumentException(final LocalizableMessage message, final Throwable cause) {
+        super(String.valueOf(message), cause);
+        this.message = message;
+    }
+
+    @Override
+    public LocalizableMessage getMessageObject() {
+        return this.message;
+    }
+
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentGroup.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentGroup.java
new file mode 100644
index 0000000..4a61751
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentGroup.java
@@ -0,0 +1,154 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * Class for organizing options into logical groups when argument usage is printed. To use an argument group, create an
+ * instance and use {@code ArgumentParser#addArgument(Argument, ArgumentGroup)} when adding arguments for to the parser.
+ */
+public final class ArgumentGroup implements Comparable<ArgumentGroup> {
+
+    /** Description for this group of arguments. */
+    private LocalizableMessage description;
+    /** List of arguments belonging to this group. */
+    private final List<Argument> args = new LinkedList<>();
+    /** Governs groups position within usage statement. */
+    private final Integer priority;
+
+    /**
+     * Creates a parameterized instance.
+     *
+     * @param description
+     *            for options in this group that is printed before argument descriptions in usage output
+     * @param priority
+     *            number governing the position of this group within the usage statement. Groups with higher priority
+     *            values appear before groups with lower priority.
+     */
+    public ArgumentGroup(final LocalizableMessage description, final int priority) {
+        this.description = description;
+        this.priority = priority;
+    }
+
+    @Override
+    public int compareTo(final ArgumentGroup o) {
+        // Groups with higher priority numbers appear before
+        // those with lower priority in the usage output
+        return -1 * priority.compareTo(o.priority);
+    }
+
+    /**
+     * Adds an argument to this group.
+     *
+     * @param arg
+     *            to add
+     * @return boolean where true indicates the add was successful
+     */
+    public boolean addArgument(final Argument arg) {
+        if (arg != null) {
+            final Character newShort = arg.getShortIdentifier();
+            final String newLong = arg.getLongIdentifier();
+
+            // See if there is already an argument in this group that the
+            // new argument should replace
+            for (final Iterator<Argument> it = this.args.iterator(); it.hasNext();) {
+                final Argument a = it.next();
+                if ((newShort != null && newShort.equals(a.getShortIdentifier()))
+                        || (newLong != null && newLong.equals(a.getLongIdentifier()))) {
+                    it.remove();
+                    break;
+                }
+            }
+
+            return this.args.add(arg);
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this group contains any members.
+     *
+     * @return boolean where true means this group contains members
+     */
+    boolean containsArguments() {
+        return !args.isEmpty();
+    }
+
+    /**
+     * Indicates whether this group contains any non-hidden members.
+     *
+     * @return boolean where true means this group contains non-hidden members
+     */
+    boolean containsNonHiddenArguments() {
+        for (final Argument arg : args) {
+            if (!arg.isHidden()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the list of arguments associated with this group.
+     *
+     * @return list of associated arguments
+     */
+    List<Argument> getArguments() {
+        return Collections.unmodifiableList(args);
+    }
+
+    /**
+     * Gets the description for this group of arguments.
+     *
+     * @return description for this argument group
+     */
+    LocalizableMessage getDescription() {
+        return this.description;
+    }
+
+    /**
+     * Removes an argument from this group.
+     *
+     * @param arg
+     *            to remove
+     * @return boolean where true indicates the remove was successful
+     */
+    boolean removeArgument(final Argument arg) {
+        return this.args.remove(arg);
+    }
+
+    /**
+     * Sets the description for this group of arguments.
+     *
+     * @param description
+     *            for this argument group
+     */
+    void setDescription(final LocalizableMessage description) {
+        this.description = description;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(description=" + description + ")";
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java
new file mode 100644
index 0000000..96e570d
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ArgumentParser.java
@@ -0,0 +1,1678 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.DocGenerationHelper.*;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+
+/**
+ * This class defines a utility that can be used to deal with command-line
+ * arguments for applications in a CLIP-compliant manner using either short
+ * one-character or longer word-based arguments. It is also integrated with the
+ * Directory Server message catalog so that it can display messages in an
+ * internationalizable format, can automatically generate usage information,
+ * can detect conflicts between arguments, and can interact with a properties
+ * file to obtain default values for arguments there if they are not specified
+ * on the command-line.
+ */
+public class ArgumentParser implements ToolRefDocContainer {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private static final Set<String> HOST_LONG_IDENTIFIERS = new HashSet<>(Arrays.asList(
+            OPTION_LONG_HOST,
+            OPTION_LONG_REFERENCED_HOST_NAME,
+            "host1",
+            "host2",
+            "hostSource",
+            "hostDestination"));
+
+    /**
+     * The name of the OpenDJ configuration direction in the user home
+     * directory.
+     */
+    public static final String DEFAULT_OPENDJ_CONFIG_DIR = ".opendj";
+    /** The default properties file name. */
+    public static final String DEFAULT_OPENDJ_PROPERTIES_FILE_NAME = "tools";
+    /** The default properties file extension. */
+    public static final String DEFAULT_OPENDJ_PROPERTIES_FILE_EXTENSION = ".properties";
+    /** The name of a command-line script used to launch a tool. */
+    public static final String PROPERTY_SCRIPT_NAME = "com.forgerock.opendj.ldap.tools.scriptName";
+    /** The legacy name of a command-line script used to launch a tool. */
+    public static final String PROPERTY_SCRIPT_NAME_LEGACY = "org.opends.server.scriptName";
+
+    /** The argument that will be used to indicate the file properties. */
+    private StringArgument filePropertiesPathArgument;
+    /** The argument that will be used to indicate that we'll not look for default properties file. */
+    private BooleanArgument noPropertiesFileArgument;
+    /** The argument that will be used to trigger the display of usage information. */
+    private Argument usageArgument;
+    /** The argument that will be used to trigger the display of the OpenDJ version. */
+    private Argument versionArgument;
+
+    /** The set of unnamed trailing arguments that were provided for this parser. */
+    private final ArrayList<String> trailingArguments = new ArrayList<>();
+
+    /**
+     * Indicates whether this parser will allow additional unnamed arguments at
+     * the end of the list.
+     */
+    private final boolean allowsTrailingArguments;
+    /** Indicates whether long arguments should be treated in a case-sensitive manner. */
+    private final boolean longArgumentsCaseSensitive;
+    /** Indicates whether the usage or version information has been displayed. */
+    private boolean usageOrVersionDisplayed;
+    /** Indicates whether the version argument was provided. */
+    private boolean versionPresent;
+
+    /** The handler to call to print the product version. */
+    private VersionHandler versionHandler = new VersionHandler() {
+        @Override
+        public void printVersion() {
+            // display nothing at all
+        }
+
+        @Override
+        public String toString() {
+            return "<no version displayed>";
+        }
+    };
+
+    /** The set of arguments defined for this parser, referenced by short ID. */
+    private final Map<Character, Argument> shortIDMap = new HashMap<>();
+    /** The set of arguments defined for this parser, referenced by long ID. */
+    private final Map<String, Argument> longIDMap = new HashMap<>();
+    /** The total set of arguments defined for this parser. */
+    private final List<Argument> argumentList = new LinkedList<>();
+
+    /** The maximum number of unnamed trailing arguments that may be provided. */
+    private final int maxTrailingArguments;
+    /** The minimum number of unnamed trailing arguments that may be provided. */
+    private final int minTrailingArguments;
+
+    /** The output stream to which usage information should be printed. */
+    private OutputStream usageOutputStream = System.out;
+
+    /**
+     * The fully-qualified name of the Java class that should be invoked to
+     * launch the program with which this argument parser is associated.
+     */
+    private final String mainClassName;
+
+    /**
+     * A human-readable description for the tool, which will be included when
+     * displaying usage information.
+     */
+    private final LocalizableMessage toolDescription;
+    /** A short description for this tool, suitable in a man page summary line. */
+    private LocalizableMessage shortToolDescription;
+
+    /** The display name that will be used for the trailing arguments in the usage information. */
+    private final String trailingArgsDisplayName;
+
+    /** Set of argument groups. */
+    protected final Set<ArgumentGroup> argumentGroups = new TreeSet<>();
+
+    /**
+     * Group for arguments that have not been explicitly grouped. These will
+     * appear at the top of the usage statement without a header.
+     */
+    private final ArgumentGroup defaultArgGroup = new ArgumentGroup(
+            LocalizableMessage.EMPTY, Integer.MAX_VALUE);
+
+    /**
+     * Group for arguments that are related to connection through LDAP. This
+     * includes options like the bind DN, the port, etc.
+     */
+    final ArgumentGroup ldapArgGroup = new ArgumentGroup(
+            INFO_DESCRIPTION_LDAP_CONNECTION_ARGS.get(), Integer.MIN_VALUE + 2);
+
+    /**
+     * Group for arguments that are related to utility input/output like
+     * properties file, no-prompt etc. These will appear toward the bottom of
+     * the usage statement.
+     */
+    protected final ArgumentGroup ioArgGroup = new ArgumentGroup(
+            INFO_DESCRIPTION_IO_ARGS.get(), Integer.MIN_VALUE + 1);
+
+    /**
+     * Group for arguments that are general like help, version etc. These will
+     * appear at the end of the usage statement.
+     */
+    private final ArgumentGroup generalArgGroup = new ArgumentGroup(
+            INFO_DESCRIPTION_GENERAL_ARGS.get(), Integer.MIN_VALUE);
+
+    private static final String INDENT = "    ";
+
+    /**
+     * Creates a new instance of this argument parser with no arguments. Unnamed
+     * trailing arguments will not be allowed.
+     *
+     * @param mainClassName
+     *            The fully-qualified name of the Java class that should be
+     *            invoked to launch the program with which this argument parser
+     *            is associated.
+     * @param toolDescription
+     *            A human-readable description for the tool, which will be
+     *            included when displaying usage information.
+     * @param longArgumentsCaseSensitive
+     *            Indicates whether long arguments should be treated in a
+     *            case-sensitive manner.
+     */
+    public ArgumentParser(final String mainClassName, final LocalizableMessage toolDescription,
+            final boolean longArgumentsCaseSensitive) {
+        this.mainClassName = mainClassName;
+        this.toolDescription = toolDescription;
+        this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
+
+        allowsTrailingArguments = false;
+        trailingArgsDisplayName = null;
+        maxTrailingArguments = 0;
+        minTrailingArguments = 0;
+        initGroups();
+    }
+
+    /**
+     * Creates a new instance of this argument parser with no arguments that may
+     * or may not be allowed to have unnamed trailing arguments.
+     *
+     * @param mainClassName
+     *            The fully-qualified name of the Java class that should be
+     *            invoked to launch the program with which this argument parser
+     *            is associated.
+     * @param toolDescription
+     *            A human-readable description for the tool, which will be
+     *            included when displaying usage information.
+     * @param longArgumentsCaseSensitive
+     *            Indicates whether long arguments should be treated in a
+     *            case-sensitive manner.
+     * @param allowsTrailingArguments
+     *            Indicates whether this parser allows unnamed trailing
+     *            arguments to be provided.
+     * @param minTrailingArguments
+     *            The minimum number of unnamed trailing arguments that must be
+     *            provided. A value less than or equal to zero indicates that no
+     *            minimum will be enforced.
+     * @param maxTrailingArguments
+     *            The maximum number of unnamed trailing arguments that may be
+     *            provided. A value less than or equal to zero indicates that no
+     *            maximum will be enforced.
+     * @param trailingArgsDisplayName
+     *            The display name that should be used as a placeholder for
+     *            unnamed trailing arguments in the generated usage information.
+     */
+    public ArgumentParser(final String mainClassName, final LocalizableMessage toolDescription,
+            final boolean longArgumentsCaseSensitive, final boolean allowsTrailingArguments,
+            final int minTrailingArguments, final int maxTrailingArguments,
+            final String trailingArgsDisplayName) {
+        this.mainClassName = mainClassName;
+        this.toolDescription = toolDescription;
+        this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
+        this.allowsTrailingArguments = allowsTrailingArguments;
+        this.minTrailingArguments = minTrailingArguments;
+        this.maxTrailingArguments = maxTrailingArguments;
+        this.trailingArgsDisplayName = trailingArgsDisplayName;
+
+        initGroups();
+    }
+
+    /**
+     * Adds the provided argument to the set of arguments handled by this
+     * parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another argument that
+     *             has already been defined.
+     */
+    public void addArgument(final Argument argument) throws ArgumentException {
+        addArgument(argument, null);
+    }
+
+    /**
+     * Adds the provided argument to the set of arguments handled by this
+     * parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @param group
+     *            The argument group to which the argument belongs.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another argument that
+     *             has already been defined.
+     */
+    public void addArgument(final Argument argument, ArgumentGroup group) throws ArgumentException {
+        final Character shortID = argument.getShortIdentifier();
+        if (shortID != null && shortIDMap.containsKey(shortID)) {
+            final String conflictingID = shortIDMap.get(shortID).getLongIdentifier();
+            throw new ArgumentException(
+                    ERR_ARGPARSER_DUPLICATE_SHORT_ID.get(argument.getLongIdentifier(), shortID, conflictingID));
+        }
+
+        // JNR: what is the requirement for the following code?
+        if (versionArgument != null
+                && shortID != null
+                && shortID.equals(versionArgument.getShortIdentifier())) {
+            // Update the version argument to not display its short identifier.
+            try {
+                versionArgument = getVersionArgument(false);
+                // JNR: why not call addGeneralArgument(versionArgument) here?
+                this.generalArgGroup.addArgument(versionArgument);
+            } catch (final ArgumentException e) {
+                // ignore
+            }
+        }
+
+        final String longID = formatLongIdentifier(argument.getLongIdentifier());
+        if (longIDMap.containsKey(longID)) {
+            throw new ArgumentException(ERR_ARGPARSER_DUPLICATE_LONG_ID.get(argument.getLongIdentifier()));
+        }
+
+        if (shortID != null) {
+            shortIDMap.put(shortID, argument);
+        }
+
+        if (longID != null) {
+            longIDMap.put(longID, argument);
+        }
+
+        argumentList.add(argument);
+
+        if (group == null) {
+            group = getStandardGroup(argument);
+        }
+        group.addArgument(argument);
+        argumentGroups.add(group);
+    }
+
+    private BooleanArgument getVersionArgument(final boolean displayShortIdentifier) throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_PRODUCT_VERSION)
+                .shortIdentifier(displayShortIdentifier ? OPTION_SHORT_PRODUCT_VERSION : null)
+                .description(INFO_DESCRIPTION_PRODUCT_VERSION.get())
+                .buildArgument();
+    }
+
+    /**
+     * Adds the provided argument to the set of arguments handled by this parser
+     * and puts the argument in the default group.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another argument that
+     *             has already been defined.
+     */
+    protected void addDefaultArgument(final Argument argument) throws ArgumentException {
+        addArgument(argument, defaultArgGroup);
+    }
+
+    /**
+     * Adds the provided argument to the set of arguments handled by this parser
+     * and puts the argument in the LDAP connection group.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another argument that
+     *             has already been defined.
+     */
+    public void addLdapConnectionArgument(final Argument argument) throws ArgumentException {
+        addArgument(argument, ldapArgGroup);
+    }
+
+    /**
+     * Indicates whether this parser will allow unnamed trailing arguments.
+     * These will be arguments at the end of the list that are not preceded by
+     * either a long or short identifier and will need to be manually parsed by
+     * the application using this parser. Note that once an unnamed trailing
+     * argument has been identified, all remaining arguments will be classified
+     * as such.
+     *
+     * @return <CODE>true</CODE> if this parser allows unnamed trailing
+     *         arguments, or <CODE>false</CODE> if it does not.
+     */
+    boolean allowsTrailingArguments() {
+        return allowsTrailingArguments;
+    }
+
+    /**
+     * Check if we have a properties file.
+     *
+     * @return The properties found in the properties file or null.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided
+     *             arguments.
+     */
+    Properties checkExternalProperties() throws ArgumentException {
+        // We don't look for properties file.
+        if (noPropertiesFileArgument != null && noPropertiesFileArgument.isPresent()) {
+            return null;
+        }
+
+        // Check if we have a properties file argument
+        if (filePropertiesPathArgument == null) {
+            return null;
+        }
+
+        // check if the properties file argument has been set. If not look for default location.
+        String propertiesFilePath;
+        if (filePropertiesPathArgument.isPresent()) {
+            propertiesFilePath = filePropertiesPathArgument.getValue();
+        } else {
+            // Check in "user home"/.opendj directory
+            final String userDir = System.getProperty("user.home");
+            propertiesFilePath =
+                    findPropertiesFile(userDir + File.separator + DEFAULT_OPENDJ_CONFIG_DIR);
+        }
+
+        // We don't have a properties file location
+        if (propertiesFilePath == null) {
+            return null;
+        }
+
+        // We have a location for the properties file.
+        try {
+            final Properties argumentProperties = new Properties();
+            final String scriptName = getScriptName();
+            final Properties p = new Properties();
+            try (final FileInputStream fis = new FileInputStream(propertiesFilePath)) {
+                p.load(fis);
+            }
+
+            for (final Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {
+                final String currentPropertyName = (String) e.nextElement();
+                String propertyName = currentPropertyName;
+
+                // Property name form <script name>.<property name> has the
+                // precedence to <property name>
+                if (scriptName != null) {
+                    if (currentPropertyName.startsWith(scriptName)) {
+                        propertyName = currentPropertyName.substring(scriptName.length() + 1);
+                    } else if (p.containsKey(scriptName + "." + currentPropertyName)) {
+                        continue;
+                    }
+                }
+                argumentProperties.setProperty(propertyName.toLowerCase(), p
+                        .getProperty(currentPropertyName));
+            }
+            return argumentProperties;
+        } catch (final Exception e) {
+            final LocalizableMessage message =
+                    ERR_ARGPARSER_CANNOT_READ_PROPERTIES_FILE.get(propertiesFilePath, getExceptionMessage(e));
+            throw new ArgumentException(message, e);
+        }
+    }
+
+    /**
+     * Retrieves the argument with the specified long identifier.
+     *
+     * @param longID
+     *            The long identifier of the argument to retrieve.
+     * @return The argument with the specified long identifier, or
+     *         <CODE>null</CODE> if there is no such argument.
+     */
+    public Argument getArgumentForLongID(final String longID) {
+        return longIDMap.get(formatLongIdentifier(longID));
+    }
+
+    /**
+     * Retrieves the list of all arguments that have been defined for this
+     * argument parser.
+     *
+     * @return The list of all arguments that have been defined for this
+     *         argument parser.
+     */
+    public List<Argument> getArgumentList() {
+        return argumentList;
+    }
+
+    /**
+     * Retrieves the fully-qualified name of the Java class that should be
+     * invoked to launch the program with which this argument parser is
+     * associated.
+     *
+     * @return The fully-qualified name of the Java class that should be invoked
+     *         to launch the program with which this argument parser is
+     *         associated.
+     */
+    String getMainClassName() {
+        return mainClassName;
+    }
+
+    /**
+     * Given an argument, returns an appropriate group. Arguments may be part of
+     * one of the special groups or the default group.
+     *
+     * @param argument
+     *            for which a group is requested
+     * @return argument group appropriate for <code>argument</code>
+     */
+    ArgumentGroup getStandardGroup(final Argument argument) {
+        if (isInputOutputArgument(argument)) {
+            return ioArgGroup;
+        } else if (isGeneralArgument(argument)) {
+            return generalArgGroup;
+        } else if (isLdapConnectionArgument(argument)) {
+            return ldapArgGroup;
+        } else {
+            return defaultArgGroup;
+        }
+    }
+
+    /**
+     * Retrieves a human-readable description for this tool, which should be
+     * included at the top of the command-line usage information.
+     *
+     * @return A human-readable description for this tool, or {@code null} if
+     *         none is available.
+     */
+    LocalizableMessage getToolDescription() {
+        return toolDescription;
+    }
+
+    @Override
+    public LocalizableMessage getShortToolDescription() {
+        return shortToolDescription != null ? shortToolDescription : LocalizableMessage.EMPTY;
+    }
+
+    @Override
+    public void setShortToolDescription(final LocalizableMessage shortDescription) {
+        this.shortToolDescription = shortDescription;
+    }
+
+    /**
+     * A supplement to the description for this tool
+     * intended for use in generated reference documentation.
+     */
+    private DocSubcommandDescriptionSupplement docToolDescriptionSupplement;
+
+    @Override
+    public LocalizableMessage getDocToolDescriptionSupplement() {
+        this.docToolDescriptionSupplement =
+                constructIfNull(this.docToolDescriptionSupplement);
+        return this.docToolDescriptionSupplement.getDocDescriptionSupplement();
+    }
+
+    @Override
+    public void setDocToolDescriptionSupplement(final LocalizableMessage supplement) {
+        this.docToolDescriptionSupplement =
+                constructIfNull(this.docToolDescriptionSupplement);
+        this.docToolDescriptionSupplement.setDocDescriptionSupplement(supplement);
+    }
+
+    /**
+     * A supplement to the description for all subcommands of this tool,
+     * intended for use in generated reference documentation.
+     */
+    private class DocSubcommandDescriptionSupplement implements DocDescriptionSupplement {
+        /** A supplement to the description intended for use in generated reference documentation. */
+        private LocalizableMessage docDescriptionSupplement;
+
+        @Override
+        public LocalizableMessage getDocDescriptionSupplement() {
+            return docDescriptionSupplement != null ? docDescriptionSupplement : LocalizableMessage.EMPTY;
+        }
+
+        private void setDocDescriptionSupplement(final LocalizableMessage docDescriptionSupplement) {
+            this.docDescriptionSupplement = docDescriptionSupplement;
+        }
+    }
+
+    private DocSubcommandDescriptionSupplement docSubcommandsDescriptionSupplement;
+
+    @Override
+    public LocalizableMessage getDocSubcommandsDescriptionSupplement() {
+        this.docSubcommandsDescriptionSupplement =
+                constructIfNull(this.docSubcommandsDescriptionSupplement);
+        return this.docSubcommandsDescriptionSupplement.getDocDescriptionSupplement();
+    }
+
+    @Override
+    public void setDocSubcommandsDescriptionSupplement(final LocalizableMessage supplement) {
+        this.docSubcommandsDescriptionSupplement =
+                constructIfNull(this.docSubcommandsDescriptionSupplement);
+        this.docSubcommandsDescriptionSupplement.setDocDescriptionSupplement(supplement);
+    }
+
+    private DocSubcommandDescriptionSupplement constructIfNull(DocSubcommandDescriptionSupplement supplement) {
+        if (supplement != null) {
+            return supplement;
+        }
+        return new DocSubcommandDescriptionSupplement();
+    }
+
+    /**
+     * Retrieves the set of unnamed trailing arguments that were provided on the
+     * command line.
+     *
+     * @return The set of unnamed trailing arguments that were provided on the
+     *         command line.
+     */
+    public ArrayList<String> getTrailingArguments() {
+        return trailingArguments;
+    }
+
+    /**
+     * Retrieves a string containing usage information based on the defined
+     * arguments.
+     *
+     * @return A string containing usage information based on the defined
+     *         arguments.
+     */
+    public String getUsage() {
+        final StringBuilder buffer = new StringBuilder();
+        usageOrVersionDisplayed = true;
+        if (System.getProperty("org.forgerock.opendj.gendoc") != null) {
+            toRefEntry(buffer, getSynopsisArgs(), argumentList);
+        } else {
+            getUsage(buffer);
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Return the list of arguments for the generated reference documentation.
+     *
+     * @return  The list of arguments for the generated reference documentation.
+     */
+    String getSynopsisArgs() {
+        if (allowsTrailingArguments()) {
+            if (trailingArgsDisplayName != null) {
+                return trailingArgsDisplayName;
+            } else {
+                return INFO_ARGPARSER_USAGE_TRAILINGARGS.get().toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Appends a generated DocBook XML RefEntry (man page) to the StringBuilder.
+     *
+     * @param builder       Append the RefEntry element to this.
+     * @param synopsisArgs  List of arguments for the command synopsis.
+     * @param argList       List of (global) arguments for this tool.
+     */
+    void toRefEntry(StringBuilder builder, String synopsisArgs, List<Argument> argList) {
+        final String scriptName = getScriptName();
+        if (scriptName == null) {
+            throw new RuntimeException("The script name should have been set via the environment property '"
+                    + PROPERTY_SCRIPT_NAME + "'.");
+        }
+
+        final Map<String, Object> map = new HashMap<>();
+        map.put("locale", Locale.getDefault().getLanguage());
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+        map.put("name", scriptName);
+        map.put("shortDesc", getShortToolDescription());
+        map.put("descTitle", REF_TITLE_DESCRIPTION.get());
+        map.put("args", synopsisArgs);
+        map.put("description", eolToNewPara(getToolDescription()));
+        map.put("info", getDocToolDescriptionSupplement());
+        if (!argList.isEmpty()) {
+            map.put("optionSection", getOptionsRefSect1(scriptName));
+        }
+        map.put("subcommands", null);
+        map.put("trailingSectionString", System.getProperty("org.forgerock.opendj.gendoc.trailing"));
+        applyTemplate(builder, "refEntry.ftl", map);
+    }
+
+    /**
+     * Returns a String with line separators replaced by {@code &lt;/para>&lt;para>}.
+     * @param input String in which to replace line separators.
+     * @return A String with line separators replaced by {@code &lt;/para>&lt;para>}.
+     */
+    String eolToNewPara(final LocalizableMessage input) {
+        return input.toString().replaceAll(EOL, "</para><para>");
+    }
+
+    /**
+     * Returns a generated DocBook XML RefSect1 element for all command options.
+     * @param scriptName    The name of this script.
+     * @return              The RefSect1 element as a String.
+     */
+    protected String getOptionsRefSect1(String scriptName) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("name", scriptName);
+        map.put("title", REF_TITLE_OPTIONS.get());
+        map.put("intro", REF_INTRO_OPTIONS.get(scriptName));
+
+        Argument helpArgument = null;
+        final boolean printHeaders = printUsageGroupHeaders();
+        List<Map<String, Object>> groups = new LinkedList<>();
+        for (final ArgumentGroup argGroup : argumentGroups) {
+            Map<String, Object> group = new HashMap<>();
+
+            // Add the group's description if any
+            if (argGroup.containsArguments() && printHeaders) {
+                LocalizableMessage description = argGroup.getDescription();
+                if (description != LocalizableMessage.EMPTY) {
+                    group.put("description", eolToNewPara(description));
+                } else {
+                    group.put("description", INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
+                }
+            }
+
+            List<Map<String, Object>> options = new LinkedList<>();
+            final SortedSet<Argument> args = sortArguments(argGroup.getArguments());
+            for (final Argument a : args) {
+                if (a.isHidden()) {
+                    continue;
+                }
+
+                final Map<String, Object> argumentMap = getArgumentMap(a);
+                // Return a generic FQDN for localhost as the default hostname in reference documentation.
+                if (isHostNameArgument(a)) {
+                    argumentMap.put("default", REF_DEFAULT.get("localhost.localdomain"));
+                }
+
+                // Return a generic message as default backend type depends on the server distribution.
+                if (a.getLongIdentifier().equals(OPTION_LONG_BACKEND_TYPE)) {
+                    argumentMap.put("default", REF_DEFAULT_BACKEND_TYPE.get().toString());
+                }
+
+                // The help argument should be added at the end.
+                if (isUsageArgument(a)) {
+                    helpArgument = a;
+                    continue;
+                }
+
+                options.add(argumentMap);
+            }
+            group.put("options", options);
+            if (!options.isEmpty()) {
+                groups.add(group);
+            }
+        }
+        if (helpArgument != null) {
+            Map<String, Object> helpGroup = new HashMap<>();
+            helpGroup.put("description", null);
+            List<Map<String, Object>> options = new LinkedList<>();
+            options.add(getArgumentMap(helpArgument));
+            helpGroup.put("options", options);
+            groups.add(helpGroup);
+        }
+        map.put("groups", groups);
+
+        StringBuilder sb = new StringBuilder();
+        applyTemplate(sb, "optionsRefSect1.ftl", map);
+        return sb.toString();
+    }
+
+    /**
+     * Returns true if this argument is for setting a hostname.
+     * @param a The argument.
+     * @return true if this argument is for setting a hostname.
+     */
+    boolean isHostNameArgument(final Argument a) {
+        return HOST_LONG_IDENTIFIERS.contains(a.getLongIdentifier());
+    }
+
+    /**
+     * Returns a map containing information about an argument option.
+     * @param   a   The argument
+     * @return      A map containing information about an argument option
+     */
+    private Map<String, Object> getArgumentMap(final Argument a) {
+        Map<String, Object> option = new HashMap<>();
+        option.put("synopsis", getOptionSynopsis(a));
+        option.put("description", eolToNewPara(a.getDescription()));
+        String dv = a.getDefaultValue();
+        option.put("default", dv != null ? REF_DEFAULT.get(dv) : null);
+        option.put("info", a.getDocDescriptionSupplement());
+        return option;
+    }
+
+    /**
+     * Writes message to the usage output stream.
+     *
+     * @param message the message to write
+     */
+    void writeToUsageOutputStream(CharSequence message) {
+        try {
+            usageOutputStream.write(getBytes(message.toString()));
+        } catch (final Exception e) {
+            logger.traceException(e);
+        }
+    }
+
+    /**
+     * Appends usage information based on the defined arguments to the provided
+     * buffer.
+     *
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     */
+    private void getUsage(final StringBuilder buffer) {
+        buffer.append(getLocalizableScriptName());
+        if (allowsTrailingArguments) {
+            buffer.append(" ");
+            if (trailingArgsDisplayName != null) {
+                buffer.append(trailingArgsDisplayName);
+            } else {
+                buffer.append(INFO_ARGPARSER_USAGE_TRAILINGARGS.get());
+            }
+        }
+        buffer.append(EOL);
+        buffer.append(EOL);
+        if (toolDescription != null && toolDescription.length() > 0) {
+            buffer.append(wrapText(toolDescription.toString(), MAX_LINE_WIDTH - 1));
+            buffer.append(EOL);
+            buffer.append(EOL);
+        }
+        buffer.append(INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
+        buffer.append(EOL);
+        buffer.append(EOL);
+
+        Argument helpArgument = null;
+
+        final boolean printHeaders = printUsageGroupHeaders();
+        for (final ArgumentGroup argGroup : argumentGroups) {
+            if (argGroup.containsArguments() && printHeaders) {
+                // Print the groups description if any
+                final LocalizableMessage groupDesc = argGroup.getDescription();
+                if (groupDesc != null && !LocalizableMessage.EMPTY.equals(groupDesc)) {
+                    buffer.append(EOL);
+                    buffer.append(wrapText(groupDesc.toString(), MAX_LINE_WIDTH - 1));
+                    buffer.append(EOL);
+                    buffer.append(EOL);
+                }
+            }
+
+            final SortedSet<Argument> args = sortArguments(argGroup.getArguments());
+            for (final Argument a : args) {
+                if (a.isHidden()) {
+                    continue;
+                }
+
+                // Help argument should be printed at the end
+                if (isUsageArgument(a)) {
+                    helpArgument = a;
+                    continue;
+                }
+                printArgumentUsage(a, buffer);
+            }
+        }
+        if (helpArgument != null) {
+            printArgumentUsage(helpArgument, buffer);
+        } else {
+            buffer.append(EOL);
+            buffer.append("-?");
+            buffer.append(EOL);
+        }
+    }
+
+    /**
+     * Sorts arguments by identifier, lowercase options first then uppercase.
+     *
+     * @param arguments     The arguments to sort.
+     * @return              The set of arguments in sorted order.
+     */
+    SortedSet<Argument> sortArguments(final List<Argument> arguments) {
+        final SortedSet<Argument> result = new TreeSet<>(new Comparator<Argument>() {
+
+            @Override
+            public int compare(final Argument o1, final Argument o2) {
+                final String s1 = getIdentifier(o1);
+                final String s2 = getIdentifier(o2);
+                final int res = s1.compareToIgnoreCase(s2);
+                if (res != 0) {
+                    return res;
+                }
+                // Lowercase options first then uppercase.
+                return -s1.compareTo(s2);
+            }
+
+            private String getIdentifier(final Argument o1) {
+                if (o1.getShortIdentifier() != null) {
+                    return o1.getShortIdentifier().toString();
+                }
+                return o1.getLongIdentifier();
+            }
+
+        });
+        result.addAll(arguments);
+        return result;
+    }
+
+    /**
+     * Returns the script name or a Java equivalent command-line string.
+     *
+     * @return the script name or a Java equivalent command-line string
+     */
+    String getScriptNameOrJava() {
+        final String scriptName = getScriptName();
+        if (scriptName != null && scriptName.length() != 0) {
+            return scriptName;
+        }
+        return "java " + getMainClassName();
+    }
+
+    /**
+     * Returns the script name as a {@link LocalizableMessage}.
+     *
+     * @return the script name as a {@link LocalizableMessage}
+     */
+    LocalizableMessage getLocalizableScriptName() {
+        final String scriptName = getScriptName();
+        if (scriptName == null || scriptName.length() == 0) {
+            return INFO_ARGPARSER_USAGE_JAVA_CLASSNAME.get(mainClassName);
+        }
+        return INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME.get(scriptName);
+    }
+
+    /**
+     * Returns the script name if set.
+     *
+     * @return the script name, or {@code null} if not set
+     */
+    String getScriptName() {
+        final String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
+        if (scriptName != null && scriptName.length() != 0) {
+            return scriptName;
+        }
+        final String legacyScriptName = System.getProperty(PROPERTY_SCRIPT_NAME_LEGACY);
+        if (legacyScriptName != null && legacyScriptName.length() != 0) {
+            return legacyScriptName;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the usage argument.
+     *
+     * @return the usageArgument
+     */
+    Argument getUsageArgument() {
+        return usageArgument;
+    }
+
+    /**
+     * Returns whether the provided argument is the usage argument.
+     *
+     * @param a the argument to test
+     * @return true if the provided argument is the usage argument, false otherwise
+     */
+    boolean isUsageArgument(final Argument a) {
+        return usageArgument != null && usageArgument.getLongIdentifier().equals(a.getLongIdentifier());
+    }
+
+    /** Prints the version. */
+    void printVersion() {
+        versionPresent = true;
+        usageOrVersionDisplayed = true;
+        versionHandler.printVersion();
+    }
+
+    /**
+     * Returns whether the usage argument was provided or not. This method
+     * should be called after a call to parseArguments.
+     *
+     * @return <CODE>true</CODE> if the usage argument was provided and
+     *         <CODE>false</CODE> otherwise.
+     */
+    public boolean isUsageArgumentPresent() {
+        return usageArgument != null && usageArgument.isPresent();
+    }
+
+    /**
+     * Returns whether the version argument was provided or not. This method
+     * should be called after a call to parseArguments.
+     *
+     * @return <CODE>true</CODE> if the version argument was provided and
+     *         <CODE>false</CODE> otherwise.
+     */
+    public boolean isVersionArgumentPresent() {
+        return versionPresent;
+    }
+
+    /**
+     * Indicates whether subcommand names and long argument strings should be treated in a case-sensitive manner.
+     *
+     * @return <CODE>true</CODE> if subcommand names and long argument strings should be treated in a case-sensitive
+     *         manner, or <CODE>false</CODE> if they should not.
+     */
+    boolean longArgumentsCaseSensitive() {
+        return longArgumentsCaseSensitive;
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information
+     * associated with this parser accordingly.
+     *
+     * @param rawArguments
+     *            The raw set of arguments to parse.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided
+     *             arguments.
+     */
+    public void parseArguments(final String[] rawArguments) throws ArgumentException {
+        parseArguments(rawArguments, null);
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information
+     * associated with this parser accordingly. Default values for unspecified
+     * arguments may be read from the specified properties if any are provided.
+     *
+     * @param rawArguments
+     *            The set of raw arguments to parse.
+     * @param argumentProperties
+     *            A set of properties that may be used to provide default values
+     *            for arguments not included in the given raw arguments.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided
+     *             arguments.
+     */
+    public void parseArguments(final String[] rawArguments, Properties argumentProperties) throws ArgumentException {
+        boolean inTrailingArgs = false;
+
+        final int numArguments = rawArguments.length;
+        for (int i = 0; i < numArguments; i++) {
+            final String arg = rawArguments[i];
+
+            if (inTrailingArgs) {
+                trailingArguments.add(arg);
+                if (maxTrailingArguments > 0 && trailingArguments.size() > maxTrailingArguments) {
+                    final LocalizableMessage message =
+                            ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS.get(maxTrailingArguments);
+                    throw new ArgumentException(message);
+                }
+
+                continue;
+            }
+
+            if (arg.equals("--")) {
+                // This is a special indicator that we have reached the end of
+                // the named arguments and that everything that follows after
+                // this should be considered trailing arguments.
+                inTrailingArgs = true;
+            } else if (arg.startsWith("--")) {
+                // This indicates that we are using the long name to reference
+                // the argument. It may be in any of the following forms:
+                // --name
+                // --name value
+                // --name=value
+
+                String argName = arg.substring(2);
+                String argValue = null;
+                final int equalPos = argName.indexOf('=');
+                // If equalsPos < 0, this is fine. The value is not part of the argument name token.
+                if (equalPos == 0) {
+                    // The argument starts with "--=", which is not acceptable.
+                    throw new ArgumentException(ERR_ARGPARSER_LONG_ARG_WITHOUT_NAME.get(arg));
+                } else if (equalPos > 0) {
+                    // The argument is in the form --name=value, so parse them both out.
+                    argValue = argName.substring(equalPos + 1);
+                    argName = argName.substring(0, equalPos);
+                }
+
+                // If we're not case-sensitive, then convert the name to lowercase.
+                final String origArgName = argName;
+                argName = formatLongIdentifier(argName);
+
+                // Get the argument with the specified name.
+                final Argument a = longIDMap.get(argName);
+                if (a == null) {
+                    if (OPTION_LONG_HELP.equals(argName)) {
+                        // "--help" will always be interpreted as requesting usage information.
+                        writeToUsageOutputStream(getUsage());
+                        return;
+                    } else if (OPTION_LONG_PRODUCT_VERSION.equals(argName)) {
+                        // "--version" will always be interpreted as requesting version information.
+                        printVersion();
+                        return;
+                    } else {
+                        // There is no such argument registered.
+                        throw new ArgumentException(
+                                ERR_ARGPARSER_NO_ARGUMENT_WITH_LONG_ID.get(origArgName));
+                    }
+                } else {
+                    a.setPresent(true);
+
+                    // If this is the usage argument, then immediately stop and
+                    // print usage information.
+                    if (isUsageArgument(a)) {
+                        writeToUsageOutputStream(getUsage());
+                        return;
+                    }
+                }
+
+                // See if the argument takes a value. If so, then make sure one
+                // was provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            throw new ArgumentException(
+                                    ERR_ARGPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID.get(origArgName));
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    final LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        throw new ArgumentException(
+                                ERR_ARGPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID.get(argValue,
+                                        origArgName, invalidReason));
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        throw new ArgumentException(ERR_ARGPARSER_NOT_MULTIVALUED_FOR_LONG_ID.get(origArgName));
+                    }
+
+                    a.addValue(argValue);
+                } else if (argValue != null) {
+                    throw new ArgumentException(
+                            ERR_ARGPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE.get(origArgName));
+                }
+            } else if (arg.startsWith("-")) {
+                // This indicates that we are using the 1-character name to
+                // reference the argument. It may be in any of the following
+                // forms:
+                // -n
+                // -nvalue
+                // -n value
+                if (arg.equals("-")) {
+                    throw new ArgumentException(ERR_ARGPARSER_INVALID_DASH_AS_ARGUMENT.get());
+                }
+
+                final char argCharacter = arg.charAt(1);
+                String argValue;
+                if (arg.length() > 2) {
+                    argValue = arg.substring(2);
+                } else {
+                    argValue = null;
+                }
+
+                // Get the argument with the specified short ID.
+                final Argument a = shortIDMap.get(argCharacter);
+                if (a == null) {
+                    if (argCharacter == '?') {
+                        writeToUsageOutputStream(getUsage());
+                        return;
+                    } else if (versionHandler != null && argCharacter == OPTION_SHORT_PRODUCT_VERSION
+                            && !shortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION)) {
+                        printVersion();
+                        return;
+                    } else {
+                        // There is no such argument registered.
+                        throw new ArgumentException(ERR_ARGPARSER_NO_ARGUMENT_WITH_SHORT_ID.get(argCharacter));
+                    }
+                } else {
+                    a.setPresent(true);
+
+                    // If this is the usage argument, then immediately stop and
+                    // print usage information.
+                    if (isUsageArgument(a)) {
+                        writeToUsageOutputStream(getUsage());
+                        return;
+                    }
+                }
+
+                // See if the argument takes a value. If so, then make sure one
+                // was provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            throw new ArgumentException(
+                                    ERR_ARGPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID.get(argCharacter));
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    final LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        throw new ArgumentException(ERR_ARGPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID.get(
+                                argValue, argCharacter, invalidReason));
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        throw new ArgumentException(ERR_ARGPARSER_NOT_MULTIVALUED_FOR_SHORT_ID.get(argCharacter));
+                    }
+
+                    a.addValue(argValue);
+                } else if (argValue != null) {
+                    // If we've gotten here, then it means that we're in a scenario like
+                    // "-abc" where "a" is a valid argument that doesn't take a value.
+                    // However, this could still be valid if all remaining characters
+                    // in the value are also valid argument characters that don't take values.
+                    final int valueLength = argValue.length();
+                    for (int j = 0; j < valueLength; j++) {
+                        final char c = argValue.charAt(j);
+                        final Argument b = shortIDMap.get(c);
+                        if (b == null) {
+                            // There is no such argument registered.
+                            throw new ArgumentException(
+                                    ERR_ARGPARSER_NO_ARGUMENT_WITH_SHORT_ID.get(argCharacter));
+                        } else if (b.needsValue()) {
+                            // This means we're in a scenario like "-abc"
+                            // where b is a valid argument that takes a value.
+                            // We don't support that.
+                            throw new ArgumentException(
+                                    ERR_ARGPARSER_CANT_MIX_ARGS_WITH_VALUES.get(argCharacter, argValue, c));
+                        } else {
+                            b.setPresent(true);
+
+                            // If this is the usage argument,
+                            // then immediately stop and print usage information.
+                            if (isUsageArgument(b)) {
+                                writeToUsageOutputStream(getUsage());
+                                return;
+                            }
+                        }
+                    }
+                }
+            } else if (allowsTrailingArguments) {
+                // It doesn't start with a dash, so it must be a trailing
+                // argument if that is acceptable.
+                inTrailingArgs = true;
+                trailingArguments.add(arg);
+            } else {
+                // It doesn't start with a dash and we don't allow trailing
+                // arguments, so this is illegal.
+                throw new ArgumentException(ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT.get(arg));
+            }
+        }
+
+        // If we allow trailing arguments and there is a minimum number,
+        // then make sure at least that many were provided.
+        if (allowsTrailingArguments
+                && minTrailingArguments > 0
+                && trailingArguments.size() < minTrailingArguments) {
+            throw new ArgumentException(ERR_ARGPARSER_TOO_FEW_TRAILING_ARGUMENTS.get(minTrailingArguments));
+        }
+
+        // If we don't have the argumentProperties, try to load a properties file
+        if (argumentProperties == null) {
+            argumentProperties = checkExternalProperties();
+        }
+
+        // Iterate through all of the arguments.
+        // For any that were not provided on the command line,
+        // see if there is an alternate default that can be used.
+        // For cases where there is not, see that argument is required.
+        normalizeArguments(argumentProperties, argumentList);
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information
+     * associated with this parser accordingly. Default values for unspecified
+     * arguments may be read from the specified properties file.
+     *
+     * @param rawArguments
+     *            The set of raw arguments to parse.
+     * @param propertiesFile
+     *            The path to the properties file to use to obtain default
+     *            values for unspecified properties.
+     * @param requirePropertiesFile
+     *            Indicates whether the parsing should fail if the provided
+     *            properties file does not exist or is not accessible.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided
+     *             arguments or interacting with the properties file.
+     */
+    public void parseArguments(final String[] rawArguments, final String propertiesFile,
+            final boolean requirePropertiesFile) throws ArgumentException {
+        Properties argumentProperties = null;
+
+        try (final FileInputStream fis = new FileInputStream(propertiesFile)) {
+            final Properties p = new Properties();
+            p.load(fis);
+            argumentProperties = p;
+        } catch (final Exception e) {
+            if (requirePropertiesFile) {
+                final LocalizableMessage message =
+                        ERR_ARGPARSER_CANNOT_READ_PROPERTIES_FILE.get(propertiesFile, getExceptionMessage(e));
+                throw new ArgumentException(message, e);
+            }
+        }
+
+        parseArguments(rawArguments, argumentProperties);
+    }
+
+    /**
+     * Indicates whether or not argument group description headers should be
+     * printed.
+     *
+     * @return boolean where true means print the descriptions
+     */
+    boolean printUsageGroupHeaders() {
+        // If there is only a single group then we won't print them.
+        int groupsContainingArgs = 0;
+        for (final ArgumentGroup argGroup : argumentGroups) {
+            if (argGroup.containsNonHiddenArguments()) {
+                groupsContainingArgs++;
+            }
+        }
+        return groupsContainingArgs > 1;
+    }
+
+    /**
+     * Sets the provided argument which will be used to identify the file
+     * properties.
+     *
+     * @param argument
+     *            The argument which will be used to identify the file
+     *            properties.
+     */
+    public void setFilePropertiesArgument(final StringArgument argument) {
+        filePropertiesPathArgument = argument;
+    }
+
+    /**
+     * Sets the provided argument which will be used to identify the file
+     * properties.
+     *
+     * @param argument
+     *            The argument which will be used to indicate if we have to look
+     *            for properties file.
+     */
+    public void setNoPropertiesFileArgument(final BooleanArgument argument) {
+        noPropertiesFileArgument = argument;
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the
+     * output of usage information if it is provided on the command line and no
+     * further argument validation will be performed. Note that the caller will
+     * still need to add this argument to the parser with the
+     * <CODE>addArgument</CODE> method, and the argument should not be required
+     * and should not take a value. Also, the caller will still need to check
+     * for the presence of the usage argument after calling
+     * <CODE>parseArguments</CODE> to know that no further processing will be
+     * required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the
+     *            display of usage information.
+     */
+    public void setUsageArgument(final Argument argument) {
+        usageArgument = argument;
+        usageOutputStream = System.out;
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the
+     * output of usage information if it is provided on the command line and no
+     * further argument validation will be performed. Note that the caller will
+     * still need to add this argument to the parser with the
+     * <CODE>addArgument</CODE> method, and the argument should not be required
+     * and should not take a value. Also, the caller will still need to check
+     * for the presence of the usage argument after calling
+     * <CODE>parseArguments</CODE> to know that no further processing will be
+     * required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the
+     *            display of usage information.
+     * @param outputStream
+     *            The output stream to which the usage information should be
+     *            written.
+     */
+    public void setUsageArgument(final Argument argument, final OutputStream outputStream) {
+        usageArgument = argument;
+        usageOutputStream = outputStream;
+    }
+
+    /**
+     * Sets whether the usage or version displayed.
+     *
+     * @param usageOrVersionDisplayed the usageOrVersionDisplayed to set
+     */
+    public void setUsageOrVersionDisplayed(boolean usageOrVersionDisplayed) {
+        this.usageOrVersionDisplayed = usageOrVersionDisplayed;
+    }
+
+    /**
+     * Sets the version handler which will be used to display the product version.
+     *
+     * @param handler
+     *            The version handler.
+     */
+    public void setVersionHandler(final VersionHandler handler) {
+        versionHandler = handler;
+    }
+
+    /**
+     * Indicates whether the version or the usage information has been displayed
+     * to the end user either by an explicit argument like "-H" or "--help", or
+     * by a built-in argument like "-?".
+     *
+     * @return {@code true} if the usage information has been displayed, or
+     *         {@code false} if not.
+     */
+    public boolean usageOrVersionDisplayed() {
+        return usageOrVersionDisplayed;
+    }
+
+    /**
+     * Get the absolute path of the properties file.
+     *
+     * @param directory
+     *            The location in which we should look for properties file
+     * @return The absolute path of the properties file or null
+     */
+    private String findPropertiesFile(final String directory) {
+        // Look for the tools properties file
+        final File f =
+                new File(directory, DEFAULT_OPENDJ_PROPERTIES_FILE_NAME
+                        + DEFAULT_OPENDJ_PROPERTIES_FILE_EXTENSION);
+        if (f.exists() && f.canRead()) {
+            return f.getAbsolutePath();
+        }
+        return null;
+    }
+
+    private void initGroups() {
+        this.argumentGroups.add(defaultArgGroup);
+        this.argumentGroups.add(ldapArgGroup);
+        this.argumentGroups.add(generalArgGroup);
+        this.argumentGroups.add(ioArgGroup);
+
+        try {
+            versionArgument = getVersionArgument(true);
+            // JNR: why not call addGeneralArgument(versionArgument) here?
+            this.generalArgGroup.addArgument(versionArgument);
+        } catch (final ArgumentException e) {
+            // ignore
+        }
+    }
+
+    private boolean isGeneralArgument(final Argument arg) {
+        if (arg != null) {
+            final String longId = arg.getLongIdentifier();
+            return OPTION_LONG_HELP.equals(longId) || OPTION_LONG_PRODUCT_VERSION.equals(longId);
+        }
+        return false;
+    }
+
+    private boolean isInputOutputArgument(final Argument arg) {
+        if (arg != null) {
+            final String longId = arg.getLongIdentifier();
+            return OPTION_LONG_VERBOSE.equals(longId)
+                            || OPTION_LONG_QUIET.equals(longId)
+                            || OPTION_LONG_NO_PROMPT.equals(longId)
+                            || OPTION_LONG_PROP_FILE_PATH.equals(longId)
+                            || OPTION_LONG_NO_PROP_FILE.equals(longId)
+                            || OPTION_LONG_SCRIPT_FRIENDLY.equals(longId)
+                            || OPTION_LONG_DONT_WRAP.equals(longId)
+                            || OPTION_LONG_ENCODING.equals(longId)
+                            || OPTION_LONG_BATCH_FILE_PATH.equals(longId);
+        }
+        return false;
+    }
+
+    private boolean isLdapConnectionArgument(final Argument arg) {
+        if (arg != null) {
+            final String longId = arg.getLongIdentifier();
+            return OPTION_LONG_USE_SSL.equals(longId)
+                            || OPTION_LONG_START_TLS.equals(longId)
+                            || OPTION_LONG_HOST.equals(longId)
+                            || OPTION_LONG_PORT.equals(longId)
+                            || OPTION_LONG_BINDDN.equals(longId)
+                            || OPTION_LONG_BINDPWD.equals(longId)
+                            || OPTION_LONG_BINDPWD_FILE.equals(longId)
+                            || OPTION_LONG_SASLOPTION.equals(longId)
+                            || OPTION_LONG_TRUSTALL.equals(longId)
+                            || OPTION_LONG_TRUSTSTOREPATH.equals(longId)
+                            || OPTION_LONG_TRUSTSTORE_PWD.equals(longId)
+                            || OPTION_LONG_TRUSTSTORE_PWD_FILE.equals(longId)
+                            || OPTION_LONG_KEYSTOREPATH.equals(longId)
+                            || OPTION_LONG_KEYSTORE_PWD.equals(longId)
+                            || OPTION_LONG_KEYSTORE_PWD_FILE.equals(longId)
+                            || OPTION_LONG_CERT_NICKNAME.equals(longId)
+                            || OPTION_LONG_REFERENCED_HOST_NAME.equals(longId)
+                            || OPTION_LONG_ADMIN_UID.equals(longId)
+                            || OPTION_LONG_REPORT_AUTHZ_ID.equals(longId)
+                            || OPTION_LONG_USE_PW_POLICY_CTL.equals(longId)
+                            || OPTION_LONG_USE_SASL_EXTERNAL.equals(longId)
+                            || OPTION_LONG_PROTOCOL_VERSION.equals(longId);
+        }
+        return false;
+    }
+
+    /**
+     * Appends argument usage information to the provided buffer.
+     *
+     * @param a
+     *            The argument to handle.
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     */
+    private void printArgumentUsage(final Argument a, final StringBuilder buffer) {
+        printLineForShortLongArgument(a, buffer);
+
+        // Write one or more lines with the description of the argument.
+        // We will indent the description five characters and try our best to wrap
+        // at or before column 79 so it will be friendly to 80-column displays.
+        final int indentLength = INDENT.length();
+        buffer.append(wrapText(a.getDescription(), MAX_LINE_WIDTH, indentLength));
+        buffer.append(EOL);
+
+        if (a.needsValue() && a.getDefaultValue() != null && a.getDefaultValue().length() > 0) {
+            buffer.append(INDENT);
+            buffer.append(INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()));
+            buffer.append(EOL);
+        }
+    }
+
+    /**
+     * Appends a line with the short and/or long identifiers that may be used for the argument to the provided string
+     * builder.
+     *
+     * @param a the argument for which to print a line
+     * @param buffer the string builder where to append the line
+     */
+    void printLineForShortLongArgument(final Argument a, final StringBuilder buffer) {
+        final Character shortID = a.getShortIdentifier();
+        final String longID = a.getLongIdentifier();
+        if (shortID != null) {
+            if (isUsageArgument(a)) {
+                buffer.append("-?, ");
+            }
+
+            buffer.append("-");
+            buffer.append(shortID.charValue());
+
+            if (a.needsValue() && longID == null) {
+                buffer.append(" ");
+                buffer.append(a.getValuePlaceholder());
+            }
+
+            if (longID != null) {
+                final StringBuilder newBuffer = new StringBuilder();
+                newBuffer.append(", --");
+                newBuffer.append(longID);
+
+                if (a.needsValue()) {
+                    newBuffer.append(" ");
+                    newBuffer.append(a.getValuePlaceholder());
+                }
+
+                final int currentLength = buffer.length();
+                final int lineLength = (buffer.length() - currentLength) + newBuffer.length();
+                if (lineLength > MAX_LINE_WIDTH) {
+                    buffer.append(EOL);
+                }
+                buffer.append(newBuffer);
+            }
+
+            buffer.append(EOL);
+        } else if (longID != null) {
+            if (isUsageArgument(a)) {
+                buffer.append("-?, ");
+            }
+            buffer.append("--");
+            buffer.append(longID);
+
+            if (a.needsValue()) {
+                buffer.append(" ");
+                buffer.append(a.getValuePlaceholder());
+            }
+
+            buffer.append(EOL);
+        }
+    }
+
+    void normalizeArguments(final Properties argumentProperties, final List<Argument> arguments)
+            throws ArgumentException {
+        for (final Argument a : arguments) {
+            // See if there is a value in the properties that can be used
+            if (!a.isPresent() && argumentProperties != null) {
+                final String value = argumentProperties.getProperty(a.getLongIdentifier().toLowerCase());
+                final LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                if (value != null) {
+                    boolean addValue = (a instanceof BooleanArgument) || a.valueIsAcceptable(value, invalidReason);
+                    if (addValue) {
+                        a.addValue(value);
+                        if (a.needsValue()) {
+                            a.setPresent(true);
+                        }
+                        a.valueSetByProperty();
+                    }
+                }
+            }
+
+            if (!a.isPresent() && a.needsValue()) {
+                // See if the argument defines a default.
+                if (a.getDefaultValue() != null) {
+                    a.addValue(a.getDefaultValue());
+                }
+
+                // If there is still no value and the argument is required, then that's a problem.
+                if (!a.hasValue() && a.isRequired()) {
+                    throw new ArgumentException(ERR_ARGPARSER_NO_VALUE_FOR_REQUIRED_ARG.get(a.getLongIdentifier()));
+                }
+            }
+        }
+    }
+
+    /**
+     * Displays the provided message on the provided stream followed by a help usage reference.
+     *
+     * @param printStream
+     *            The stream to print error message and help reference message.
+     * @param message
+     *            The error message to print.
+     */
+    public void displayMessageAndUsageReference(final PrintStream printStream, final LocalizableMessage message) {
+        printWrappedText(printStream, message);
+        printStream.println();
+        printWrappedText(printStream, getHelpUsageReference());
+    }
+
+    /**
+     * Retrieves a string describing how the user can get more help.
+     *
+     * @return A string describing how the user can get more help.
+     */
+    public LocalizableMessage getHelpUsageReference() {
+        setUsageOrVersionDisplayed(true);
+
+        LocalizableMessageBuilder buffer = new LocalizableMessageBuilder();
+        buffer.append(INFO_GLOBAL_HELP_REFERENCE.get(getScriptNameOrJava()));
+        buffer.append(EOL);
+        return buffer.toMessage();
+    }
+
+    /**
+     * Get the password which has to be used for the command without prompting the user. If no password was specified,
+     * return null.
+     *
+     * @param clearArg
+     *            The password StringArgument argument.
+     * @param fileArg
+     *            The password FileBased argument.
+     * @return The password stored into the specified file on by the command line argument, or null it if not specified.
+     */
+    public static String getBindPassword(StringArgument clearArg, FileBasedArgument fileArg) {
+        if (clearArg.isPresent()) {
+            return clearArg.getValue();
+        } else if (fileArg.isPresent()) {
+            return fileArg.getValue();
+        }
+        return null;
+    }
+
+    /**
+     * Replace the provided {@link Argument} from this parser by the provided {@link Argument}.
+     * If the {@link Argument} is not present in this parser, do nothing.
+     *
+     * @param argument
+     *          The {@link Argument} to replace.
+     */
+    public void replaceArgument(final Argument argument) {
+        replaceArgumentInCollections(longIDMap, shortIDMap, argumentList, argument);
+    }
+
+    void replaceArgumentInCollections(final Map<String, Argument> longIDToArg,
+            final Map<Character, Argument> shortIDToArg, final List<Argument> argumentList, final Argument argument) {
+        final String longID = formatLongIdentifier(argument.getLongIdentifier());
+        if (!longIDToArg.containsKey(longID)) {
+            return;
+        }
+        longIDToArg.put(longID, argument);
+        shortIDToArg.put(argument.getShortIdentifier(), argument);
+        argumentList.remove(argument);
+        argumentList.add(argument);
+        for (final ArgumentGroup group : argumentGroups) {
+            if (group.getArguments().contains(argument)) {
+                group.removeArgument(argument);
+                group.addArgument(argument);
+            }
+        }
+    }
+
+    String formatLongIdentifier(final String longIdentifier) {
+        return longArgumentsCaseSensitive ? longIdentifier : toLowerCase(longIdentifier);
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/BooleanArgument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/BooleanArgument.java
new file mode 100644
index 0000000..be5fb43
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/BooleanArgument.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.ERR_BOOLEANARG_NO_VALUE_ALLOWED;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+/**
+ * This class defines an argument type that will be used to represent Boolean
+ * values. These arguments will never take values from the command line but and
+ * will never be required. If the argument is provided, then it will be
+ * considered true, and if not then it will be considered false. As such, the
+ * default value will always be "false".
+ */
+public final class BooleanArgument extends Argument {
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * {@link BooleanArgument}.
+     *
+     * @param longIdentifier
+     *         The long identifier that will be used to refer to this argument.
+     * @return A builder to continue building the {@link BooleanArgument}.
+     */
+    public static Builder builder(final String longIdentifier) {
+        return new Builder(longIdentifier);
+    }
+
+    /** A fluent API for incrementally constructing {@link BooleanArgument}. */
+    public static final class Builder extends ArgumentBuilder<Builder, Boolean, BooleanArgument> {
+        private Builder(final String longIdentifier) {
+            super(longIdentifier);
+            this.needsValue = false;
+            this.defaultValue = false;
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        @Override
+        public BooleanArgument buildArgument() throws ArgumentException {
+            return new BooleanArgument(this);
+        }
+    }
+
+    private BooleanArgument(final Builder builder) throws ArgumentException {
+        super(builder);
+    }
+
+    @Override
+    public final void addValue(final String valueString) {
+        if (valueString != null) {
+            clearValues();
+            super.addValue(valueString);
+            super.setPresent(Boolean.valueOf(valueString));
+        }
+    }
+
+    @Override
+    public final void setPresent(final boolean isPresent) {
+        addValue(String.valueOf(isPresent));
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final String valueString, final LocalizableMessageBuilder invalidReason) {
+        // This argument type should never have a value, so any value
+        // provided will be unacceptable.
+        invalidReason.append(ERR_BOOLEANARG_NO_VALUE_ALLOWED.get(longIdentifier));
+
+        return false;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java
new file mode 100644
index 0000000..32be644
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CliConstants.java
@@ -0,0 +1,69 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * This class defines a number of constants used by tools.
+ */
+public final class CliConstants {
+
+    /** The minimum java specification supported string version. */
+    public static final float MINIMUM_JAVA_VERSION = 1.7F;
+
+    /** Default value for LDAP connection timeout. */
+    public static final int DEFAULT_LDAP_CONNECT_TIMEOUT = 30000;
+
+    /** Default value for incrementing port number. */
+    public static final int PORT_INCREMENT = 1000;
+
+    /** Default port number for the LDAP port. */
+    public static final int DEFAULT_LDAP_PORT = 389;
+
+    /** Default port number for the LDAPS port. */
+    public static final int DEFAULT_LDAPS_PORT = 1636;
+
+    /** Default port number for the administrator port. */
+    public static final int DEFAULT_ADMIN_PORT = 1444;
+
+    /** Default port number for the SSL Connection. */
+    public static final int DEFAULT_SSL_PORT = 636;
+
+    /** Default port number for the JMX Connection handler. */
+    public static final int DEFAULT_JMX_PORT = 1689;
+
+    /** Default port number for the HTTP Connection handler. */
+    public static final int DEFAULT_HTTP_PORT = 8080;
+
+    /** Default port number for the SNMP Connection handler. */
+    public static final int DEFAULT_SNMP_PORT = 161;
+
+    /** Default name of root user DN. */
+    public static final String DEFAULT_ROOT_USER_DN = "cn=Directory Manager";
+
+    /** Default Administration Connector port. */
+    public static final int DEFAULT_ADMINISTRATION_CONNECTOR_PORT = 4444;
+
+    /** Default Administration UID. */
+    public static final String GLOBAL_ADMIN_UID = "admin";
+
+
+    /** Prevent instantiation. */
+    private CliConstants() {
+
+    }
+
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ClientException.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ClientException.java
new file mode 100644
index 0000000..6316bba
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ClientException.java
@@ -0,0 +1,98 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.ERR_CONSOLE_INPUT_ERROR;
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * This class defines an exception that may be thrown if a local problem occurs in a Directory Server client.
+ */
+public class ClientException extends Exception implements LocalizableException {
+    /**
+     * The serial version identifier required to satisfy the compiler because this class extends
+     * <CODE>java.lang.Exception</CODE>, which implements the <CODE>java.io.Serializable</CODE> interface. This value
+     * was generated using the <CODE>serialver</CODE> command-line utility included with the Java SDK.
+     */
+    private static final long serialVersionUID = 1384120263337669664L;
+
+    /** The return code. */
+    private ReturnCode returnCode;
+
+    /** The message linked to that exception. */
+    private final LocalizableMessage message;
+
+    /**
+     * Adapts any exception that may have occurred whilst reading input from the
+     * console.
+     *
+     * @param cause
+     *            The exception that occurred whilst reading input from the
+     *            console.
+     * @return Returns a new CLI exception describing a problem that occurred
+     *         whilst reading input from the console.
+     */
+    public static ClientException adaptInputException(final Throwable cause) {
+        return new ClientException(ReturnCode.ERROR_USER_DATA, ERR_CONSOLE_INPUT_ERROR.get(cause.getMessage()), cause);
+    }
+
+    /**
+     * Creates a new client exception with the provided message.
+     *
+     * @param exitCode
+     *            The exit code that may be used if the client considers this to be a fatal problem.
+     * @param message
+     *            The message that explains the problem that occurred.
+     */
+    public ClientException(ReturnCode exitCode, LocalizableMessage message) {
+        super(message.toString());
+        this.returnCode = exitCode;
+        this.message = message;
+    }
+
+    /**
+     * Creates a new client exception with the provided message and root cause.
+     *
+     * @param exitCode
+     *            The exit code that may be used if the client considers this to be a fatal problem.
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @param cause
+     *            The exception that was caught to trigger this exception.
+     */
+    public ClientException(ReturnCode exitCode, LocalizableMessage message, Throwable cause) {
+        super(message.toString(), cause);
+        this.returnCode = exitCode;
+        this.message = message;
+    }
+
+    /**
+     * Retrieves the exit code that the client may use if it considers this to be a fatal problem.
+     *
+     * @return The exit code that the client may use if it considers this to be a fatal problem.
+     */
+    public int getReturnCode() {
+        return returnCode.get();
+    }
+
+    @Override
+    public LocalizableMessage getMessageObject() {
+        return message;
+    }
+
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java
new file mode 100644
index 0000000..6f379e8
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommandBuilder.java
@@ -0,0 +1,275 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.Utils.OBFUSCATED_VALUE;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.forgerock.opendj.util.OperatingSystem;
+
+/**
+ * Class used to be able to generate the non interactive mode.
+ */
+public class CommandBuilder {
+    private String commandName;
+    private String subcommandName;
+    private final ArrayList<Argument> args = new ArrayList<>();
+    private final HashSet<Argument> obfuscatedArgs = new HashSet<>();
+
+    /**
+     * The separator used to link the lines of the resulting command-lines.
+     */
+    public static final String LINE_SEPARATOR;
+    static {
+        if (OperatingSystem.isWindows()) {
+            LINE_SEPARATOR = " ";
+        } else {
+            LINE_SEPARATOR = " \\\n          ";
+        }
+    }
+
+    /**
+     * The separator used to link the lines of the resulting command-lines in HTML format.
+     */
+    public static final String HTML_LINE_SEPARATOR;
+    static {
+        if (OperatingSystem.isWindows()) {
+            HTML_LINE_SEPARATOR = "&nbsp;";
+        } else {
+            HTML_LINE_SEPARATOR = "&nbsp;\\<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
+        }
+    }
+
+    /** Creates a {@link CommandBuilder} with {@code null} command and subcommand names. */
+    public CommandBuilder() {
+        this(null, null);
+    }
+
+    /**
+     * The constructor for the CommandBuilder.
+     *
+     * @param commandName
+     *            The command name.
+     * @param subcommandName
+     *            The sub command name.
+     */
+    public CommandBuilder(String commandName, String subcommandName) {
+        this.commandName = commandName;
+        this.subcommandName = subcommandName;
+    }
+
+    /**
+     * Adds an argument to the list of the command builder.
+     *
+     * @param argument
+     *            The argument to be added.
+     */
+    public void addArgument(final Argument argument) {
+        // We use an ArrayList to be able to provide the possibility of updating
+        // the position of the attributes.
+        if (!args.contains(argument)) {
+            args.add(argument);
+        }
+    }
+
+    /**
+     * Adds an argument whose values must be obfuscated (passwords for instance).
+     *
+     * @param argument
+     *            The argument to be added.
+     */
+    public void addObfuscatedArgument(final Argument argument) {
+        addArgument(argument);
+        obfuscatedArgs.add(argument);
+    }
+
+    /**
+     * Removes the provided argument from this CommandBuilder.
+     *
+     * @param argument
+     *            The argument to be removed.
+     * @return <CODE>true</CODE> if the attribute was present and removed and <CODE>false</CODE> otherwise.
+     */
+    public boolean removeArgument(final Argument argument) {
+        obfuscatedArgs.remove(argument);
+        return args.remove(argument);
+    }
+
+    /**
+     * Removes the provided arguments from this CommandBuilder.
+     * Arguments which are not in this {@link CommandBuilder} will be ignored.
+     *
+     * @param arguments
+     *            Arguments to be removed.
+     */
+    public void removeArguments(final Argument... arguments) {
+        for (final Argument argument : arguments) {
+            removeArgument(argument);
+        }
+    }
+
+    /**
+     * Appends the arguments of another command builder to this command builder.
+     *
+     * @param builder
+     *            The CommandBuilder to append.
+     */
+    public void append(final CommandBuilder builder) {
+        for (final Argument arg : builder.args) {
+            if (builder.isObfuscated(arg)) {
+                addObfuscatedArgument(arg);
+            } else {
+                addArgument(arg);
+            }
+        }
+    }
+
+    /**
+     * Returns the String representation of this command builder (i.e. what we want to show to the user).
+     *
+     * @return The String representation of this command builder (i.e. what we want to show to the user).
+     */
+    @Override
+    public String toString() {
+        return toString(false, LINE_SEPARATOR);
+    }
+
+    /**
+     * Returns the String representation of this command builder (i.e. what we want to show to the user).
+     *
+     * @param lineSeparator
+     *            The String to be used to separate lines of the command-builder.
+     * @return The String representation of this command builder (i.e. what we want to show to the user).
+     */
+    public String toString(final String lineSeparator) {
+        return toString(false, lineSeparator);
+    }
+
+    /**
+     * Returns the String representation of this command builder (i.e. what we want to show to the user).
+     *
+     * @param showObfuscated
+     *            Displays in clear the obfuscated values.
+     * @param lineSeparator
+     *            The String to be used to separate lines of the command-builder.
+     * @return The String representation of this command builder (i.e. what we want to show to the user).
+     */
+    private String toString(final boolean showObfuscated, final String lineSeparator) {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(commandName);
+        if (subcommandName != null) {
+            builder.append(" ").append(subcommandName);
+        }
+        for (final Argument arg : args) {
+            // This CLI is always using SSL, and the argument has been removed from
+            // the user interface
+            if (ArgumentConstants.OPTION_LONG_USE_SSL.equals(arg.getLongIdentifier())) {
+                continue;
+            }
+            String argName;
+            if (arg.getLongIdentifier() != null) {
+                argName = "--" + arg.getLongIdentifier();
+            } else {
+                argName = "-" + arg.getShortIdentifier();
+            }
+
+            if (arg instanceof BooleanArgument) {
+                builder.append(lineSeparator).append(argName);
+            } else if (arg instanceof FileBasedArgument) {
+                for (String value : ((FileBasedArgument) arg).getNameToValueMap().keySet()) {
+                    builder.append(lineSeparator).append(argName).append(" ");
+                    builder.append(getOutputValue(value, arg, showObfuscated));
+                }
+            } else {
+                for (String value : arg.getValues()) {
+                    builder.append(lineSeparator).append(argName).append(" ");
+                    builder.append(getOutputValue(value, arg, showObfuscated));
+                }
+            }
+        }
+        return builder.toString();
+    }
+
+    private String getOutputValue(final String value, final Argument arg, final boolean showObfuscated) {
+        if (isObfuscated(arg) && !showObfuscated) {
+            return OBFUSCATED_VALUE;
+        }
+        return escapeValue(value);
+    }
+
+    /**
+     * Clears the arguments.
+     */
+    public void clearArguments() {
+        args.clear();
+        obfuscatedArgs.clear();
+    }
+
+    /**
+     * Returns the list of arguments.
+     *
+     * @return The list of arguments.
+     */
+    public List<Argument> getArguments() {
+        return args;
+    }
+
+    /**
+     * Tells whether the provided argument's values must be obfuscated or not.
+     *
+     * @param argument
+     *            The argument to handle.
+     * @return <CODE>true</CODE> if the attribute's values must be obfuscated and <CODE>false</CODE> otherwise.
+     */
+    public boolean isObfuscated(final Argument argument) {
+        return obfuscatedArgs.contains(argument);
+    }
+
+    /** Chars that require special treatment when passing them to command-line. */
+    private static final Set<Character> CHARSTOESCAPE = new TreeSet<>(Arrays.asList(
+        ' ', '\t', '\n', '|', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\''));
+
+    /**
+     * This method simply takes a value and tries to transform it (with escape or '"') characters so that it can be used
+     * in a command line.
+     *
+     * @param value
+     *            The String to be treated.
+     * @return The transformed value.
+     */
+    public static String escapeValue(String value) {
+        final StringBuilder b = new StringBuilder();
+        if (OperatingSystem.isUnix()) {
+            for (int i = 0; i < value.length(); i++) {
+                final char c = value.charAt(i);
+                if (CHARSTOESCAPE.contains(c)) {
+                    b.append('\\');
+                }
+                b.append(c);
+            }
+        } else {
+            b.append('"').append(value).append('"');
+        }
+        return b.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
new file mode 100644
index 0000000..ce34201
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/CommonArguments.java
@@ -0,0 +1,1138 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.SearchScope;
+
+/**
+ * This class regroup commons arguments used by the different CLI.
+ */
+public final class CommonArguments {
+
+    /** Prevent instantiation. */
+    private CommonArguments() {
+        // Nothing to do.
+    }
+
+    /**
+     * Returns the "show usage / help" boolean argument.
+     *
+     * @return The "show usage" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument showUsageArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_HELP)
+                .shortIdentifier(OPTION_SHORT_HELP)
+                .description(INFO_DESCRIPTION_SHOWUSAGE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "verbose" boolean argument.
+     *
+     * @return The "verbose" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument verboseArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_VERBOSE)
+                .shortIdentifier(OPTION_SHORT_VERBOSE)
+                .description(INFO_DESCRIPTION_VERBOSE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "port" integer argument. <br>
+     * <i> N.B : the 'p' short option is also used by skipdecode(DBTest),
+     * propertiesFile(JavaPropertiesToolArguments).</i>
+     *
+     * @param defaultPort
+     *            The default port number.
+     * @param description
+     *            Port number's description.
+     * @return The "port" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument portArgument(final int defaultPort, final LocalizableMessage description)
+            throws ArgumentException {
+        return IntegerArgument.builder(OPTION_LONG_PORT)
+                .shortIdentifier(OPTION_SHORT_PORT)
+                .description(description != null ? description : INFO_DESCRIPTION_ADMIN_PORT.get())
+                .range(1, 65535)
+                .defaultValue(defaultPort)
+                .valuePlaceholder(INFO_PORT_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "propertiesFilePath" string argument.
+     *
+     * @return The "propertiesFilePath" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument propertiesFileArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_PROP_FILE_PATH)
+                .description(INFO_DESCRIPTION_PROP_FILE_PATH.get())
+                .valuePlaceholder(INFO_PROP_FILE_PATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "proxyauthzid" string argument.
+     *
+     * @return The "proxyauthzid" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument proxyAuthIdArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_PROXYAUTHID)
+                .shortIdentifier(OPTION_SHORT_PROXYAUTHID)
+                .description(INFO_DESCRIPTION_PROXYAUTHZID.get())
+                .valuePlaceholder(INFO_PROXYAUTHID_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "No properties file" boolean argument.
+     *
+     * @return The "noPropertiesFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument noPropertiesFileArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_NO_PROP_FILE)
+                .description(INFO_DESCRIPTION_NO_PROP_FILE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "Continue On Error" boolean argument. <br>
+     * <i> N.B : the 'c' short option is also used by cleanupservice, compress.</i>
+     *
+     * @return The "continueOnError" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument continueOnErrorArgument() throws ArgumentException {
+        return BooleanArgument.builder("continueOnError")
+                .shortIdentifier('c')
+                .description(INFO_DESCRIPTION_CONTINUE_ON_ERROR.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "control" string argument.
+     *
+     * @return The "control" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument controlArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_CONTROL)
+                .shortIdentifier(OPTION_SHORT_CONTROL)
+                .description(INFO_DESCRIPTION_CONTROLS.get())
+                .docDescriptionSupplement(SUPPLEMENT_DESCRIPTION_CONTROLS.get())
+                .multiValued()
+                .valuePlaceholder(INFO_LDAP_CONTROL_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "ldapVersion" integer argument.
+     *
+     * @return The "ldapVersion" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument ldapVersionArgument() throws ArgumentException {
+        return IntegerArgument.builder(OPTION_LONG_PROTOCOL_VERSION)
+                .shortIdentifier(OPTION_SHORT_PROTOCOL_VERSION)
+                .description(INFO_DESCRIPTION_VERSION.get())
+                .defaultValue(3)
+                .valuePlaceholder(INFO_PROTOCOL_VERSION_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "quiet" boolean argument.
+     *
+     * @return The "quiet" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument quietArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_QUIET)
+                .shortIdentifier(OPTION_SHORT_QUIET)
+                .description(INFO_DESCRIPTION_QUIET.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "no-op" boolean argument. <br>
+     * <i> N.B : the 'n' short option is also used by backendid, newGroupName, newPassword, no-prompt.</i>
+     *
+     * @return The "no-op" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument noOpArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_DRYRUN)
+                .shortIdentifier(OPTION_SHORT_DRYRUN)
+                .description(INFO_DESCRIPTION_NOOP.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "no-prompt" boolean argument. <br>
+     * <i> N.B : the 'n' short option is also used by backendid, newGroupName, newPassword, no-prompt.</i>
+     *
+     * @return The "no-prompt" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument noPromptArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_NO_PROMPT)
+                .shortIdentifier(OPTION_SHORT_NO_PROMPT)
+                .description(INFO_DESCRIPTION_NO_PROMPT.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "acceptLicense" boolean argument.
+     *
+     * @return The "acceptLicense" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument acceptLicenseArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_ACCEPT_LICENSE)
+                .description(INFO_OPTION_ACCEPT_LICENSE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "trustAll" boolean argument.
+     *
+     * @return The "trustAll" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument trustAllArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_TRUSTALL)
+                .shortIdentifier(OPTION_SHORT_TRUSTALL)
+                .description(INFO_DESCRIPTION_TRUSTALL.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "trustStorePath" string argument.
+     *
+     * @return The "trustStorePath" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument trustStorePathArgument() throws ArgumentException {
+        return trustStorePathArgument(null);
+    }
+
+    /**
+     * Returns the "trustStorePath" string argument initialized with the provided default value.
+     *
+     * @param defaultValue
+     *          The "trustStorePath" argument default value
+     * @return The "trustStorePath" string argument initialized with the provided default value.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument trustStorePathArgument(final String defaultValue) throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_TRUSTSTOREPATH)
+                .shortIdentifier(OPTION_SHORT_TRUSTSTOREPATH)
+                .description(INFO_DESCRIPTION_TRUSTSTOREPATH.get())
+                .defaultValue(defaultValue)
+                .valuePlaceholder(INFO_TRUSTSTOREPATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "truststorepw" string argument.
+     *
+     * @return The "truststorepw" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument trustStorePasswordArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_TRUSTSTORE_PWD)
+                .shortIdentifier(OPTION_SHORT_TRUSTSTORE_PWD)
+                .description(INFO_DESCRIPTION_TRUSTSTOREPASSWORD.get())
+                .valuePlaceholder(INFO_TRUSTSTORE_PWD_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "trustStorePasswordFile" file argument.
+     *
+     * @return The "trustStorePasswordFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static FileBasedArgument trustStorePasswordFileArgument() throws ArgumentException {
+        return FileBasedArgument.builder(OPTION_LONG_TRUSTSTORE_PWD_FILE)
+                .shortIdentifier(OPTION_SHORT_TRUSTSTORE_PWD_FILE)
+                .description(INFO_DESCRIPTION_TRUSTSTOREPASSWORD_FILE.get())
+                .valuePlaceholder(INFO_TRUSTSTORE_PWD_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns a "connectTimeout" hidden integer argument.
+     *
+     * @return A "connectTimeout" hidden integer argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument connectTimeOutHiddenArgument() throws ArgumentException {
+        return connectTimeOutArgument(true);
+    }
+
+    /**
+     * Returns a "connectTimeout" integer argument.
+     *
+     * @return A "connectTimeout" integer argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument connectTimeOutArgument() throws ArgumentException {
+        return connectTimeOutArgument(false);
+    }
+
+    private static IntegerArgument connectTimeOutArgument(final boolean hidden) throws ArgumentException {
+        final IntegerArgument.Builder builder = IntegerArgument.builder(OPTION_LONG_CONNECT_TIMEOUT)
+                .description(INFO_DESCRIPTION_CONNECTION_TIMEOUT.get())
+                .lowerBound(0)
+                .defaultValue(DEFAULT_LDAP_CONNECT_TIMEOUT)
+                .valuePlaceholder(INFO_TIMEOUT_PLACEHOLDER.get());
+        if (hidden) {
+            builder.hidden();
+        }
+        return builder.buildArgument();
+    }
+
+    /**
+     * Returns the "CLI" boolean argument. <br>
+     * <i> N.B : the 'i' short option is also used by encoding.</i>
+     *
+     * @return The "CLI" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument cliArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_CLI)
+                .shortIdentifier(OPTION_SHORT_CLI)
+                .description(INFO_ARGUMENT_DESCRIPTION_CLI.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "configfile" string argument. <br>
+     * <i> N.B : the 'f' short option is also used by filename</i>
+     *
+     * @return The "configfile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument configFileArgument() throws ArgumentException {
+        return StringArgument.builder("configFile")
+                .shortIdentifier('f')
+                .description(INFO_DESCRIPTION_CONFIG_FILE.get())
+                .hidden()
+                .required()
+                .valuePlaceholder(INFO_CONFIGFILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "configclass" string argument.
+     *
+     * @param configFileHandlerName
+     *            The config file handler name.
+     * @return The "configclass" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument configClassArgument(final String configFileHandlerName) throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_CONFIG_CLASS)
+                .shortIdentifier(OPTION_SHORT_CONFIG_CLASS)
+                .description(INFO_DESCRIPTION_CONFIG_CLASS.get())
+                .hidden()
+                .required()
+                .defaultValue(configFileHandlerName)
+                .valuePlaceholder(INFO_CONFIGCLASS_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "baseDN" string argument.
+     *
+     * @return The "baseDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument baseDNArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_BASEDN)
+                .shortIdentifier(OPTION_SHORT_BASEDN)
+                .description(INFO_ARGUMENT_DESCRIPTION_BASEDN.get())
+                .multiValued()
+                .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "bindDN" string argument. <br/>
+     * <i> N.B : the 'D' short option is also used by rootUserDN.</i>
+     *
+     * @param defaultBindDN
+     *            The default bind DN.
+     * @return The "bindDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument bindDNArgument(final String defaultBindDN) throws ArgumentException {
+        return bindDNArgument(defaultBindDN, INFO_DESCRIPTION_BINDDN.get());
+    }
+
+
+    /**
+     * Returns the "bindDN" string argument. <br/>
+     * <i> N.B : the 'D' short option is also used by rootUserDN.</i>
+     *
+     * @param defaultBindDN
+     *            The default bind DN.
+     * @param description
+     *            The localized description to print in help messages.
+     * @return The "bindDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument bindDNArgument(final String defaultBindDN, final LocalizableMessage description)
+            throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_BINDDN)
+                .shortIdentifier(OPTION_SHORT_BINDDN)
+                .description(description)
+                .defaultValue(defaultBindDN)
+                .valuePlaceholder(INFO_BINDDN_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "bindPassword" string argument.
+     *
+     * @return The "bindPassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument bindPasswordArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_BINDPWD)
+                .shortIdentifier(OPTION_SHORT_BINDPWD)
+                .description(INFO_DESCRIPTION_BINDPASSWORD.get())
+                .valuePlaceholder(INFO_BINDPWD_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "bindPasswordFile" file argument.
+     *
+     * @return The "bindPasswordFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static FileBasedArgument bindPasswordFileArgument() throws ArgumentException {
+        return FileBasedArgument.builder(OPTION_LONG_BINDPWD_FILE)
+                .shortIdentifier(OPTION_SHORT_BINDPWD_FILE)
+                .description(INFO_DESCRIPTION_BINDPASSWORDFILE.get())
+                .valuePlaceholder(INFO_BINDPWD_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "addbaseentry" boolean argument.
+     * <br><i> N.B : the 'a' short option is also used by backupall, defaultAdd.</i>
+     *
+     * @return The "addbaseentry" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument addBaseEntryArgument() throws ArgumentException {
+        return BooleanArgument.builder("addBaseEntry")
+                .shortIdentifier('a')
+                .description(INFO_ARGUMENT_DESCRIPTION_ADDBASE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "rejectfile" string argument. <br>
+     * <i> N.B : the 'R' short option is also used by restart, serverRoot.</i>
+     *
+     * @return The "rejectfile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument rejectedImportLdifArgument() throws ArgumentException {
+        return StringArgument.builder("rejectFile")
+                .shortIdentifier('R')
+                .description(INFO_GENERAL_DESCRIPTION_REJECTED_FILE.get())
+                .valuePlaceholder(INFO_REJECT_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "remote" boolean argument. <br>
+     * <i> N.B : the 'r' short option is also used by useSASLExternal, stopreason.</i>
+     *
+     * @return The "remote" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument remoteArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_REMOTE)
+                .shortIdentifier(OPTION_SHORT_REMOTE)
+                .description(INFO_DESCRIPTION_REMOTE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "reportauthzid" boolean argument.
+     *
+     * @return The "reportauthzid" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument reportAuthzIdArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_REPORT_AUTHZ_ID)
+                .shortIdentifier('E')
+                .description(INFO_DESCRIPTION_REPORT_AUTHZID.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "restart" boolean argument. <br>
+     * <i> N.B : the 'R' short option is also used by rejectfile, serverRoot.</i>
+     *
+     * @return The "restart" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument restartArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_RESTART)
+                .shortIdentifier('R')
+                .description(INFO_DESCRIPTION_RESTART.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "skip file" string argument.
+     *
+     * @return The "skipFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument skippedImportFileArgument() throws ArgumentException {
+        return StringArgument.builder("skipFile")
+                .description(INFO_GENERAL_DESCRIPTION_SKIPPED_FILE.get())
+                .valuePlaceholder(INFO_SKIP_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "sample data" integer argument. <br>
+     * <i> N.B : the 'd' short option is also used by backupdirectory, disableservice.</i>
+     *
+     * @return The "sampleData" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument sampleDataArgument() throws ArgumentException {
+        return IntegerArgument.builder("sampleData")
+                .shortIdentifier('d')
+                .description(INFO_SETUP_DESCRIPTION_SAMPLE_DATA.get())
+                .lowerBound(0)
+                .defaultValue(0)
+                .valuePlaceholder(INFO_NUM_ENTRIES_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "sasloption" string argument. <br>
+     * <i> N.B : the 'o' short option is also used by outputLDIF.</i>
+     *
+     * @return The "sasloption" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument saslArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_SASLOPTION)
+                .shortIdentifier(OPTION_SHORT_SASLOPTION)
+                .description(INFO_LDAP_CONN_DESCRIPTION_SASLOPTIONS.get())
+                .multiValued()
+                .valuePlaceholder(INFO_SASL_OPTION_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "searchScope" string argument.<br>
+     * <i> N.B : the 's' short option is also used by servicestate, sourceldif, randomSeed, script-friendly.</i>
+     *
+     * @return The "searchScope" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static MultiChoiceArgument<SearchScope> searchScopeArgument() throws ArgumentException {
+        return MultiChoiceArgument.<SearchScope>builder(OPTION_LONG_SEARCHSCOPE)
+                .shortIdentifier(OPTION_SHORT_SEARCHSCOPE)
+                .description(INFO_SEARCH_DESCRIPTION_SEARCH_SCOPE.get())
+                .allowedValues(SearchScope.values())
+                .defaultValue(SearchScope.WHOLE_SUBTREE)
+                .valuePlaceholder(INFO_SEARCH_SCOPE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "script-friendly" boolean argument.<br>
+     * <i> N.B : the 's' short option is also used by searchScope, servicestate, sourceldif, randomSeed.</i>
+     *
+     * @return The "script-friendly" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument scriptFriendlyArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_SCRIPT_FRIENDLY)
+                .shortIdentifier(OPTION_SHORT_SCRIPT_FRIENDLY)
+                .description(INFO_DESCRIPTION_SCRIPT_FRIENDLY.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "LDAP port" integer argument.
+     *
+     * @param defaultLdapPort
+     *            Default LDAP Connector port.
+     * @return The "ldapPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument ldapPortArgument(final int defaultLdapPort) throws ArgumentException {
+        return IntegerArgument.builder("ldapPort")
+                .shortIdentifier(OPTION_SHORT_PORT)
+                .description(INFO_ARGUMENT_DESCRIPTION_LDAPPORT.get())
+                .range(1, 65535)
+                .defaultValue(defaultLdapPort)
+                .valuePlaceholder(INFO_PORT_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "Admin port" integer argument.
+     *
+     * @param defaultAdminPort
+     *            Default Administration Connector port.
+     * @return The "adminConnectorPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument adminLdapPortArgument(final int defaultAdminPort) throws ArgumentException {
+        return IntegerArgument.builder("adminConnectorPort")
+                .description(INFO_ARGUMENT_DESCRIPTION_ADMINCONNECTORPORT.get())
+                .range(1, 65535)
+                .defaultValue(defaultAdminPort)
+                .valuePlaceholder(INFO_PORT_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "advanced" boolean argument.
+     *
+     * @return The "advanced" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument advancedModeArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_ADVANCED)
+                .description(INFO_DESCRIPTION_ADVANCED.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "JMX port" integer argument.
+     *
+     * @param defaultJMXPort
+     *            Default JMX port.
+     * @return The "jmxPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument jmxPortArgument(final int defaultJMXPort) throws ArgumentException {
+        return IntegerArgument.builder("jmxPort")
+                .shortIdentifier('x')
+                .description(INFO_ARGUMENT_DESCRIPTION_SKIPPORT.get())
+                .range(1, 65535)
+                .defaultValue(defaultJMXPort)
+                .valuePlaceholder(INFO_JMXPORT_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "skipPortCheck" boolean argument.
+     *
+     * @return The "skipPortCheck" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument skipPortCheckArgument() throws ArgumentException {
+        return BooleanArgument.builder("skipPortCheck")
+                .shortIdentifier('S')
+                .description(INFO_ARGUMENT_DESCRIPTION_SKIPPORT.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "startTLS" boolean argument.
+     *
+     * @return The "startTLS" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument startTLSArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_START_TLS)
+                .shortIdentifier(OPTION_SHORT_START_TLS)
+                .description(INFO_DESCRIPTION_START_TLS.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "rootUserDN" string argument. <br>
+     * <i> N.B : the 'D' short option is also used by bindDN.</i>
+     *
+     * @return The "rootUserDN" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument rootDNArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_ROOT_USER_DN)
+                .shortIdentifier(OPTION_SHORT_ROOT_USER_DN)
+                .description(INFO_ARGUMENT_DESCRIPTION_ROOTDN.get())
+                .defaultValue("cn=Directory Manager")
+                .valuePlaceholder(INFO_ROOT_USER_DN_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "directory manager DN password" string argument.
+     *
+     * @return The "rootUserPassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument rootDNPwdArgument() throws ArgumentException {
+        return StringArgument.builder("rootUserPassword")
+                .shortIdentifier(OPTION_SHORT_BINDPWD)
+                .description(INFO_ROOT_USER_PWD_PLACEHOLDER.get())
+                .valuePlaceholder(INFO_ROOT_USER_PWD_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "directory manager DN password file" file argument.
+     *
+     * @return The "rootUserPasswordFile" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static FileBasedArgument rootDNPwdFileArgument() throws ArgumentException {
+        return FileBasedArgument.builder("rootUserPasswordFile")
+                .shortIdentifier(OPTION_SHORT_BINDPWD_FILE)
+                .description(INFO_ARGUMENT_DESCRIPTION_ROOTPWFILE.get())
+                .valuePlaceholder(INFO_ROOT_USER_PWD_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "enable window service" integer argument.
+     *
+     * @return The "enableWindowsService" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument enableWindowsServiceArgument() throws ArgumentException {
+        return BooleanArgument.builder("enableWindowsService")
+                .shortIdentifier('e')
+                .description(INFO_ARGUMENT_DESCRIPTION_ENABLE_WINDOWS_SERVICE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "encoding" string argument. <br>
+     * <i> N.B : the 'i' short option is also used by cli</i>
+     *
+     * @return The "encoding" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument encodingArgument() throws ArgumentException {
+        return StringArgument.builder("encoding")
+                .shortIdentifier('i')
+                .description(INFO_DESCRIPTION_ENCODING.get())
+                .valuePlaceholder(INFO_ENCODING_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "do not start" boolean argument.
+     *
+     * @return The "doNotStart" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument doNotStartArgument() throws ArgumentException {
+        return BooleanArgument.builder("doNotStart")
+                .shortIdentifier('O')
+                .description(INFO_SETUP_DESCRIPTION_DO_NOT_START.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "displayCommand" boolean argument.
+     *
+     * @return The "displayCommand" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument displayEquivalentCommandArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_DISPLAY_EQUIVALENT)
+                .description(INFO_DESCRIPTION_DISPLAY_EQUIVALENT.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "commandFilePath" string argument.
+     *
+     * @param description
+     *            The description of this argument.
+     * @return The "commandFilePath" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument equivalentCommandFileArgument(final LocalizableMessage description)
+            throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_EQUIVALENT_COMMAND_FILE_PATH)
+                .description(description)
+                .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "filename" string argument.
+     * <i> N.B : the 'f' short option is also used by configfile</i>
+     * @param description
+     *            The description of this argument.
+     * @return The "filename" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument filenameArgument(final LocalizableMessage description) throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_FILENAME)
+                .shortIdentifier(OPTION_SHORT_FILENAME)
+                .description(description)
+                .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "enable start TLS" boolean argument.
+     *
+     * @return The "enableStartTLS" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument enableTLSArgument() throws ArgumentException {
+        return BooleanArgument.builder("enableStartTLS")
+                .shortIdentifier(OPTION_SHORT_START_TLS)
+                .description(INFO_SETUP_DESCRIPTION_ENABLE_STARTTLS.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "ldaps port" integer argument. <br>
+     * <i> N.B : the 'Z' short option is also used by useSSL.</i>
+     *
+     * @param defaultSecurePort
+     *            Default value for the LDAPS port.
+     * @return The "ldapsPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static IntegerArgument ldapsPortArgument(final int defaultSecurePort) throws ArgumentException {
+        return IntegerArgument.builder("ldapsPort")
+                .shortIdentifier(OPTION_SHORT_USE_SSL)
+                .description(INFO_ARGUMENT_DESCRIPTION_LDAPSPORT.get())
+                .range(1, 65535)
+                .defaultValue(defaultSecurePort)
+                .valuePlaceholder(INFO_PORT_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "ldiffile" string argument.
+     *
+     * @param description
+     *            The description of this argument.
+     * @return The "ldapsPort" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument ldifFileArgument(final LocalizableMessage description) throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_LDIF_FILE)
+                .shortIdentifier(OPTION_SHORT_LDIF_FILE)
+                .description(description)
+                .multiValued()
+                .valuePlaceholder(INFO_LDIFFILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "generate self certificate" boolean argument.
+     *
+     * @return The "generateSelfSignedCertificate" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument generateSelfSignedArgument() throws ArgumentException {
+        return BooleanArgument.builder("generateSelfSignedCertificate")
+                .description(INFO_ARGUMENT_DESCRIPTION_USE_SELF_SIGNED_CERTIFICATE.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "hostname" string argument.
+     *
+     * @param defaultHostName
+     *            The default host name value.
+     * @return The "hostname" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument hostNameArgument(final String defaultHostName) throws ArgumentException {
+        return hostNameArgument(defaultHostName, null);
+    }
+
+    /**
+     * Returns the "hostname" string argument.
+     *
+     * @param defaultHostName
+     *            The default host name value.
+     * @param description
+     *            The custom description.
+     * @return The "hostname" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument hostNameArgument(final String defaultHostName, final LocalizableMessage description)
+            throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_HOST)
+                .shortIdentifier(OPTION_SHORT_HOST)
+                .description(description != null ? description : INFO_ARGUMENT_DESCRIPTION_HOST_NAME.get())
+                .defaultValue(defaultHostName)
+                .valuePlaceholder(INFO_HOST_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "use PKCS11 key store" boolean argument.
+     *
+     * @return The "usePkcs11Keystore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument usePKCS11KeystoreArgument() throws ArgumentException {
+        return BooleanArgument.builder("usePkcs11Keystore")
+                .description(INFO_ARGUMENT_DESCRIPTION_USE_PKCS11.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "use java key store" string argument.
+     *
+     * @return The "useJavaKeystore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument useJavaKeyStoreArgument() throws ArgumentException {
+        return StringArgument.builder("useJavaKeystore")
+                .description(INFO_ARGUMENT_DESCRIPTION_USE_JAVAKEYSTORE.get())
+                .valuePlaceholder(INFO_KEYSTOREPATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "use JCEKS" string argument.
+     *
+     * @return The "useJCEKS" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument useJCEKSArgument() throws ArgumentException {
+        return StringArgument.builder("useJCEKS")
+                .description(INFO_ARGUMENT_DESCRIPTION_USE_JCEKS.get())
+                .valuePlaceholder(INFO_KEYSTOREPATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "use PKCS12 key store" string argument.
+     *
+     * @return The "usePkcs12keyStore" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument usePKCS12KeyStoreArgument() throws ArgumentException {
+        return StringArgument.builder("usePkcs12keyStore")
+                .description(INFO_ARGUMENT_DESCRIPTION_USE_PKCS12.get())
+                .valuePlaceholder(INFO_KEYSTOREPATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "useSSL" boolean argument. <br>
+     * <i> N.B : the 'Z' short option is also used by ldapsport.</i>
+     *
+     * @return The "useSSL" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static BooleanArgument useSSLArgument() throws ArgumentException {
+        return BooleanArgument.builder(OPTION_LONG_USE_SSL)
+                .shortIdentifier(OPTION_SHORT_USE_SSL)
+                .description(INFO_DESCRIPTION_USE_SSL.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "key store password" string argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument keyStorePasswordArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_KEYSTORE_PWD)
+                .shortIdentifier(OPTION_SHORT_KEYSTORE_PWD)
+                .description(INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD.get())
+                .valuePlaceholder(INFO_KEYSTORE_PWD_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "key store password file" file argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static FileBasedArgument keyStorePasswordFileArgument() throws ArgumentException {
+        return FileBasedArgument.builder(OPTION_LONG_KEYSTORE_PWD_FILE)
+                .shortIdentifier(OPTION_SHORT_KEYSTORE_PWD_FILE)
+                .description(INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD_FILE.get())
+                .valuePlaceholder(INFO_KEYSTORE_PWD_FILE_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "keyStorePath" string argument.
+     *
+     * @return The "keyStorePath" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument keyStorePathArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_KEYSTOREPATH)
+                .shortIdentifier(OPTION_SHORT_KEYSTOREPATH)
+                .description(INFO_DESCRIPTION_KEYSTOREPATH.get())
+                .valuePlaceholder(INFO_KEYSTOREPATH_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "key store password file" string argument.
+     *
+     * @return The "keyStorePassword" argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument certNickNameArgument() throws ArgumentException {
+        return StringArgument.builder(OPTION_LONG_CERT_NICKNAME)
+                .shortIdentifier(OPTION_SHORT_CERT_NICKNAME)
+                .description(INFO_ARGUMENT_DESCRIPTION_CERT_NICKNAME.get())
+                .multiValued()
+                .valuePlaceholder(INFO_NICKNAME_PLACEHOLDER.get())
+                .buildArgument();
+    }
+
+    /**
+     * Returns the "admin uid" string argument with the provided description.
+     *
+     * @param description
+     *            The argument localizable description.
+     * @return The "admin uid" string argument with the provided description.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument adminUid(final LocalizableMessage description) throws ArgumentException {
+        return adminUidArgument(false, description);
+    }
+
+    /**
+     * Returns the "admin uid" hidden string argument.
+     *
+     * @param description
+     *            The argument localizable description.
+     * @return The "admin uid" hidden string argument.
+     * @throws ArgumentException
+     *             If there is a problem with any of the parameters used to create this argument.
+     */
+    public static StringArgument adminUidHiddenArgument(final LocalizableMessage description)
+            throws ArgumentException {
+        return adminUidArgument(true, description);
+    }
+
+    private static StringArgument adminUidArgument(final boolean hidden, final LocalizableMessage description)
+            throws ArgumentException {
+        final StringArgument.Builder builder = StringArgument.builder(OPTION_LONG_ADMIN_UID)
+                .shortIdentifier('I')
+                .description(description)
+                .defaultValue(CliConstants.GLOBAL_ADMIN_UID)
+                .valuePlaceholder(INFO_ADMINUID_PLACEHOLDER.get());
+        if (hidden) {
+            builder.hidden();
+        }
+        return builder.buildArgument();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java
new file mode 100644
index 0000000..7a3c7c4
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java
@@ -0,0 +1,841 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.CliConstants.DEFAULT_LDAP_PORT;
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.Utils.getHostNameForLdapUrl;
+import static com.forgerock.opendj.cli.Utils.throwIfArgumentsConflict;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.KeyManagers;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyRequestControl;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.ExternalSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.GSSAPISASLBindRequest;
+import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.util.Options;
+import org.forgerock.util.time.Duration;
+
+/**
+ * A connection factory designed for use with command line tools.
+ */
+public final class ConnectionFactoryProvider {
+    /** The Logger. */
+    static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The 'hostName' global argument. */
+    private StringArgument hostNameArg;
+    /** The 'port' global argument. */
+    private IntegerArgument portArg;
+
+    /** The 'bindDN' global argument. */
+    private StringArgument bindNameArg;
+    /** The 'bindPasswordFile' global argument. */
+    private FileBasedArgument bindPasswordFileArg;
+
+    /** The 'password' value. */
+    private char[] password;
+    /** The 'bindPassword' global argument. */
+    private StringArgument bindPasswordArg;
+
+    /** The 'connectTimeOut' global argument. */
+    private IntegerArgument connectTimeOut;
+
+    /** The 'trustAllArg' global argument. */
+    private BooleanArgument trustAllArg;
+    /** The 'trustStore' global argument. */
+    private StringArgument trustStorePathArg;
+    /** The 'trustStorePassword' global argument. */
+    private StringArgument trustStorePasswordArg;
+    /** The 'trustStorePasswordFile' global argument. */
+    private FileBasedArgument trustStorePasswordFileArg;
+
+    /** The 'keyStore' global argument. */
+    private StringArgument keyStorePathArg;
+    /** The 'keyStorePassword' global argument. */
+    private StringArgument keyStorePasswordArg;
+    /** The 'keyStorePasswordFile' global argument. */
+    private FileBasedArgument keyStorePasswordFileArg;
+
+    /** The 'certNicknameArg' global argument. */
+    private StringArgument certNicknameArg;
+
+    /** The 'useSSLArg' global argument. */
+    private BooleanArgument useSSLArg;
+    /** The 'useStartTLSArg' global argument. */
+    private BooleanArgument useStartTLSArg;
+    /** Argument indicating a SASL option. */
+    private StringArgument saslOptionArg;
+
+    /**
+     * Whether to request that the server return the authorization ID in the
+     * bind response.
+     */
+    private final BooleanArgument reportAuthzIDArg;
+
+    /** Whether to use the password policy control in the bind request. */
+    private final BooleanArgument usePasswordPolicyControlArg;
+
+    /** The SSL context linked to this connection. */
+    private SSLContext sslContext;
+
+    /**  The basic connection factory. */
+    private ConnectionFactory connFactory;
+
+    /** The bind request to connect with. */
+    private BindRequest bindRequest;
+
+    /** The console application linked to this connection in interactive mode. */
+    private final ConsoleApplication app;
+
+    /** If this connection should be an admin connection. */
+    private boolean isAdminConnection;
+
+    /**
+     * Default constructor to create a connection factory designed for use with command line tools,
+     * adding basic LDAP connection arguments to the specified parser (e.g: hostname, bindname...etc).
+     *
+     * @param argumentParser
+     *            The argument parser.
+     * @param app
+     *            The console application linked to this connection factory.
+     * @throws ArgumentException
+     *             If an error occurs during parsing the arguments.
+     */
+    public ConnectionFactoryProvider(final ArgumentParser argumentParser,
+            final ConsoleApplication app) throws ArgumentException {
+        this(argumentParser, app, "", DEFAULT_LDAP_PORT, false);
+    }
+
+    /**
+     * Constructor to create a connection factory designed for use with command line tools,
+     * adding basic LDAP connection arguments to the specified parser (e.g: hostname, bindname...etc).
+     *
+     * @param argumentParser
+     *            The argument parser.
+     * @param app
+     *            The console application linked to this connection factory.
+     * @param defaultBindDN
+     *            The bind DN default's value.
+     * @param defaultPort
+     *            The LDAP port default's value.
+     * @param alwaysSSL
+     *            {@code true} if this connection should be used with SSL.
+     * @throws ArgumentException
+     *             If an error occurs during parsing the elements.
+     */
+    public ConnectionFactoryProvider(final ArgumentParser argumentParser,
+            final ConsoleApplication app, final String defaultBindDN, final int defaultPort,
+            final boolean alwaysSSL) throws ArgumentException {
+        this.app = app;
+
+        useSSLArg = useSSLArgument();
+        if (!alwaysSSL) {
+            argumentParser.addLdapConnectionArgument(useSSLArg);
+        } else {
+            // simulate that the useSSL arg has been given in the CLI
+            useSSLArg.setPresent(true);
+        }
+
+        useStartTLSArg = startTLSArgument();
+        if (!alwaysSSL) {
+            argumentParser.addLdapConnectionArgument(useStartTLSArg);
+        }
+
+        String defaultHostName;
+        try {
+            defaultHostName = InetAddress.getLocalHost().getHostName();
+        } catch (final Exception e) {
+            defaultHostName = "Unknown (" + e + ")";
+        }
+        hostNameArg = hostNameArgument(defaultHostName);
+        argumentParser.addLdapConnectionArgument(hostNameArg);
+
+        LocalizableMessage portDescription = INFO_DESCRIPTION_PORT.get();
+        if (alwaysSSL) {
+            portDescription = INFO_DESCRIPTION_ADMIN_PORT.get();
+        }
+
+        portArg = portArgument(defaultPort, portDescription);
+        argumentParser.addLdapConnectionArgument(portArg);
+
+        bindNameArg = bindDNArgument(defaultBindDN);
+        argumentParser.addLdapConnectionArgument(bindNameArg);
+
+        bindPasswordArg = bindPasswordArgument();
+        argumentParser.addLdapConnectionArgument(bindPasswordArg);
+
+        bindPasswordFileArg = bindPasswordFileArgument();
+        argumentParser.addLdapConnectionArgument(bindPasswordFileArg);
+
+        saslOptionArg = saslArgument();
+        argumentParser.addLdapConnectionArgument(saslOptionArg);
+
+        trustAllArg = trustAllArgument();
+        argumentParser.addLdapConnectionArgument(trustAllArg);
+
+        trustStorePathArg = trustStorePathArgument();
+        argumentParser.addLdapConnectionArgument(trustStorePathArg);
+
+        trustStorePasswordArg = trustStorePasswordArgument();
+        argumentParser.addLdapConnectionArgument(trustStorePasswordArg);
+
+        trustStorePasswordFileArg = trustStorePasswordFileArgument();
+        argumentParser.addLdapConnectionArgument(trustStorePasswordFileArg);
+
+        keyStorePathArg = keyStorePathArgument();
+        argumentParser.addLdapConnectionArgument(keyStorePathArg);
+
+        keyStorePasswordArg = keyStorePasswordArgument();
+        argumentParser.addLdapConnectionArgument(keyStorePasswordArg);
+
+        keyStorePasswordFileArg = keyStorePasswordFileArgument();
+        argumentParser.addLdapConnectionArgument(keyStorePasswordFileArg);
+
+        certNicknameArg = certNickNameArgument();
+        argumentParser.addLdapConnectionArgument(certNicknameArg);
+
+        reportAuthzIDArg = reportAuthzIdArgument();
+        argumentParser.addArgument(reportAuthzIDArg);
+
+        connectTimeOut = connectTimeOutHiddenArgument();
+        argumentParser.addArgument(connectTimeOut);
+
+        usePasswordPolicyControlArg =
+                BooleanArgument.builder(OPTION_LONG_USE_PW_POLICY_CTL)
+                        .description(INFO_DESCRIPTION_USE_PWP_CONTROL.get())
+                        .buildAndAddToParser(argumentParser);
+    }
+
+    /**
+     * Returns the connect time out.
+     *
+     * @return The connect time out value.
+     */
+    public int getConnectTimeout() {
+        if (connectTimeOut.isPresent()) {
+            try {
+                return connectTimeOut.getIntValue();
+            } catch (ArgumentException e) {
+                return Integer.valueOf(connectTimeOut.getDefaultValue());
+            }
+        }
+        return Integer.valueOf(connectTimeOut.getDefaultValue());
+    }
+
+
+    /**
+     * Returns the host name if the argument is present otherwise, if the application
+     * is interactive, prompt the user for it.
+     *
+     * @return The host name value.
+     * @throws ArgumentException
+     *             If the host name cannot be retrieved.
+     */
+    public String getHostname() throws ArgumentException {
+        String value = "";
+
+        if (hostNameArg.isPresent()) {
+            value = hostNameArg.getValue();
+        } else if (app.isInteractive()) {
+            try {
+                value = app.readInput(INFO_DESCRIPTION_HOST.get(), getHostNameDefaultValue(value));
+                app.println();
+                hostNameArg.addValue(value);
+                hostNameArg.setPresent(true);
+            } catch (ClientException e) {
+                throw new ArgumentException(ERR_ERROR_CANNOT_READ_HOST_NAME.get(), e);
+            }
+        } else {
+            return getHostNameDefaultValue(value);
+        }
+
+        return getHostNameForLdapUrl(value);
+    }
+
+    private String getHostNameDefaultValue(String fallbackValue) {
+        return hostNameArg.getDefaultValue() != null ? hostNameArg.getDefaultValue() : fallbackValue;
+    }
+
+    /**
+     * Get the port which has to be used for the command.
+     *
+     * @return The port specified by the command line argument, or the default value, if not specified.
+     */
+    public int getPort() {
+        if (portArg.isPresent()) {
+            try {
+                return portArg.getIntValue();
+            } catch (ArgumentException e) {
+                return Integer.valueOf(portArg.getDefaultValue());
+            }
+        } else if (app.isInteractive()) {
+            final LocalizableMessage portMsg =
+                    isAdminConnection ? INFO_DESCRIPTION_ADMIN_PORT.get() : INFO_DESCRIPTION_PORT.get();
+            int value = app.askPort(portMsg, Integer.valueOf(portArg.getDefaultValue()), logger);
+            app.println();
+            portArg.addValue(Integer.toString(value));
+            portArg.setPresent(true);
+            return value;
+        }
+        return Integer.valueOf(portArg.getDefaultValue());
+    }
+
+    /**
+     * Indicate if the SSL mode is required.
+     *
+     * @return True if SSL mode is required
+     */
+    public boolean useSSL() {
+        return useSSLArg.isPresent();
+    }
+
+    /**
+     * Indicate if the startTLS mode is required.
+     *
+     * @return True if startTLS mode is required
+     */
+    public boolean useStartTLS() {
+        return useStartTLSArg.isPresent();
+    }
+
+    /**
+     * Constructs a connection factory for pre-authenticated connections. Checks if any conflicting arguments are
+     * present, build the connection with selected arguments and returns the connection factory. If the application is
+     * interactive, it will prompt the user for missing parameters.
+     *
+     * @return The connection factory.
+     * @throws ArgumentException
+     *         If an error occurs during the parsing of the arguments (conflicting arguments or if an error occurs
+     *         during building SSL context).
+     */
+    public ConnectionFactory getAuthenticatedConnectionFactory() throws ArgumentException {
+        return getConnectionFactory(true);
+    }
+
+    /**
+     * Constructs a connection factory for unauthenticated connections. Checks if any conflicting arguments are present,
+     * build the connection with selected arguments and returns the connection factory. If the application is
+     * interactive, it will prompt the user for missing parameters.
+     *
+     * @return The connection factory.
+     * @throws ArgumentException
+     *         If an error occurs during the parsing of the arguments (conflicting arguments or if an error occurs
+     *         during building SSL context).
+     */
+    public ConnectionFactory getUnauthenticatedConnectionFactory() throws ArgumentException {
+        return getConnectionFactory(false);
+    }
+
+    private ConnectionFactory getConnectionFactory(boolean usePreAuthentication) throws ArgumentException {
+        if (connFactory == null) {
+            checkForConflictingArguments();
+
+            if (app.isInteractive()) {
+                boolean portIsMissing = !portArg.isPresent() || portArg.getIntValue() == 0;
+                boolean bindPwdIsMissing = !bindPasswordArg.isPresent() && !bindPasswordFileArg.isPresent();
+                if (!hostNameArg.isPresent() || portIsMissing || !bindNameArg.isPresent() || bindPwdIsMissing) {
+                    app.printHeader(INFO_LDAP_CONN_HEADING_CONNECTION_PARAMETERS.get());
+                }
+                if (!hostNameArg.isPresent()) {
+                    getHostname();
+                }
+                if (portIsMissing) {
+                    getPort();
+                }
+                if (!bindNameArg.isPresent()) {
+                    getBindName();
+                }
+                if (bindPwdIsMissing) {
+                    getPassword();
+                }
+            }
+
+            try {
+                if (useSSLArg.isPresent() || useStartTLSArg.isPresent()) {
+                    String clientAlias;
+                    if (certNicknameArg.isPresent()) {
+                        clientAlias = certNicknameArg.getValue();
+                    } else {
+                        clientAlias = null;
+                    }
+
+                    if (sslContext == null) {
+                        final TrustManager trustManager = getTrustManager();
+
+                        X509KeyManager keyManager = null;
+                        final X509KeyManager akm = getKeyManager(keyStorePathArg.getValue());
+
+                        if (akm != null && clientAlias != null) {
+                            keyManager = KeyManagers.useSingleCertificate(clientAlias, akm);
+                        }
+
+                        sslContext =
+                                new SSLContextBuilder().setTrustManager(trustManager)
+                                        .setKeyManager(keyManager).getSSLContext();
+                    }
+                }
+            } catch (final Exception e) {
+                throw new ArgumentException(ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL.get(e.toString()),
+                        e);
+            }
+
+            Options options = Options.defaultOptions();
+            if (sslContext != null) {
+                options.set(SSL_CONTEXT, sslContext)
+                    .set(SSL_USE_STARTTLS, useStartTLSArg.isPresent());
+            }
+            options.set(CONNECT_TIMEOUT, new Duration((long) getConnectTimeout(), TimeUnit.MILLISECONDS));
+            if (usePreAuthentication) {
+                options.set(AUTHN_BIND_REQUEST, getBindRequest());
+            }
+            connFactory = new LDAPConnectionFactory(hostNameArg.getValue(), portArg.getIntValue(), options);
+        }
+        return connFactory;
+    }
+
+    /**
+     * Verifies if the connection arguments are not conflicting together or if they are readable.
+     *
+     * @throws ArgumentException
+     *             If arguments are conflicting or if the files cannot be read,
+     *             an argument exception is thrown.
+     */
+    private void checkForConflictingArguments() throws ArgumentException {
+        throwIfArgumentsConflict(bindPasswordArg, bindPasswordFileArg);
+        throwIfArgumentsConflict(trustAllArg, trustStorePathArg);
+        throwIfArgumentsConflict(trustAllArg, trustStorePasswordArg);
+        throwIfArgumentsConflict(trustAllArg, trustStorePasswordFileArg);
+        throwIfArgumentsConflict(trustStorePasswordArg, trustStorePasswordFileArg);
+        throwIfArgumentsConflict(useStartTLSArg, useSSLArg);
+
+        if (trustStorePathArg.isPresent()) {
+            // Check that the path exists and is readable
+            final String value = trustStorePathArg.getValue();
+            if (!canReadPath(value)) {
+                final LocalizableMessage message = ERR_CANNOT_READ_TRUSTSTORE.get(value);
+                throw new ArgumentException(message);
+            }
+        }
+
+        if (keyStorePathArg.isPresent()) {
+            // Check that the path exists and is readable
+            final String value = keyStorePathArg.getValue();
+            if (!canReadPath(value)) {
+                final LocalizableMessage message = ERR_CANNOT_READ_KEYSTORE.get(value);
+                throw new ArgumentException(message);
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if we can read on the provided path and
+     * {@code false} otherwise.
+     *
+     * @param path
+     *            the path.
+     * @return {@code true} if we can read on the provided path and
+     *         {@code false} otherwise.
+     */
+    private boolean canReadPath(final String path) {
+        final File file = new File(path);
+        return file.exists() && file.canRead();
+    }
+
+    private String getAuthID(final String mech) throws ArgumentException {
+        String value = getAuthID();
+        if (value == null && bindNameArg.isPresent()) {
+            value = "dn: " + bindNameArg.getValue();
+        }
+        if (value == null && app.isInteractive()) {
+            try {
+                value =
+                        app.readInput(LocalizableMessage.raw("Authentication ID:"), bindNameArg
+                                .getDefaultValue() == null ? null : "dn: "
+                                + bindNameArg.getDefaultValue());
+            } catch (ClientException e) {
+                throw new ArgumentException(LocalizableMessage
+                        .raw("Unable to read authentication ID"), e);
+            }
+        }
+        if (value == null) {
+            throw new ArgumentException(ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(mech));
+        }
+        return value;
+    }
+
+    private String getAuthID() throws ArgumentException {
+        return getSaslProperty(SASL_PROPERTY_AUTHID);
+    }
+
+    private String getAuthzID() throws ArgumentException {
+        return getSaslProperty(SASL_PROPERTY_AUTHZID);
+    }
+
+    /**
+     * Returns the bind name if the argument is present otherwise, in interactive mode, it
+     * will prompt the user.
+     *
+     * @return The bind name used for this connection.
+     * @throws ArgumentException
+     *             If the bind name cannot be retrieved.
+     */
+    public String getBindName() throws ArgumentException {
+        String value = "";
+        if (bindNameArg.isPresent()) {
+            value = bindNameArg.getValue();
+        } else if (app.isInteractive()) {
+            LocalizableMessage bindMsg;
+            if (isAdminConnection) {
+                bindMsg = INFO_DESCRIPTION_ADMIN_BINDDN.get();
+            } else {
+                bindMsg = INFO_DESCRIPTION_BINDDN.get();
+            }
+            try {
+                value = app.readInput(bindMsg,
+                        bindNameArg.getDefaultValue() == null ? value : bindNameArg.getDefaultValue());
+                app.println();
+                bindNameArg.clearValues();
+                bindNameArg.addValue(value);
+                bindNameArg.setPresent(true);
+            } catch (ClientException e) {
+                throw new ArgumentException(ERR_ERROR_CANNOT_READ_BIND_NAME.get(), e);
+            }
+        }
+
+        return value;
+    }
+
+    /**
+     * Returns the bind request for this connection.
+     *
+     * @return The bind request for this connection.
+     * @throws ArgumentException
+     *             If the arguments of this connection are wrong.
+     */
+    public BindRequest getBindRequest() throws ArgumentException {
+        if (bindRequest == null) {
+            String mech = getMechanism();
+
+            if (mech == null) {
+                if (bindNameArg.isPresent() || bindPasswordFileArg.isPresent()
+                        || bindPasswordArg.isPresent()) {
+                    bindRequest = Requests.newSimpleBindRequest(getBindName(), getPassword());
+                }
+            } else if (DigestMD5SASLBindRequest.SASL_MECHANISM_NAME.equals(mech)) {
+                bindRequest =
+                        Requests.newDigestMD5SASLBindRequest(
+                                getAuthID(DigestMD5SASLBindRequest.SASL_MECHANISM_NAME),
+                                getPassword()).setAuthorizationID(getAuthzID())
+                                .setRealm(getRealm());
+            } else if (CRAMMD5SASLBindRequest.SASL_MECHANISM_NAME.equals(mech)) {
+                bindRequest =
+                        Requests.newCRAMMD5SASLBindRequest(
+                                getAuthID(CRAMMD5SASLBindRequest.SASL_MECHANISM_NAME),
+                                getPassword());
+            } else if (GSSAPISASLBindRequest.SASL_MECHANISM_NAME.equals(mech)) {
+                bindRequest =
+                        Requests.newGSSAPISASLBindRequest(
+                                getAuthID(GSSAPISASLBindRequest.SASL_MECHANISM_NAME), getPassword())
+                                .setKDCAddress(getKDC()).setRealm(getRealm()).setAuthorizationID(
+                                        getAuthzID());
+            } else if (ExternalSASLBindRequest.SASL_MECHANISM_NAME.equals(mech)) {
+                if (sslContext == null) {
+                    final LocalizableMessage message = ERR_TOOL_SASLEXTERNAL_NEEDS_SSL_OR_TLS.get();
+                    throw new ArgumentException(message);
+                }
+                if (!keyStorePathArg.isPresent() && getKeyStore() == null) {
+                    final LocalizableMessage message = ERR_TOOL_SASLEXTERNAL_NEEDS_KEYSTORE.get();
+                    throw new ArgumentException(message);
+                }
+                bindRequest =
+                        Requests.newExternalSASLBindRequest().setAuthorizationID(getAuthzID());
+            } else if (PlainSASLBindRequest.SASL_MECHANISM_NAME.equals(mech)) {
+                bindRequest =
+                        Requests.newPlainSASLBindRequest(
+                                getAuthID(PlainSASLBindRequest.SASL_MECHANISM_NAME), getPassword())
+                                .setAuthorizationID(getAuthzID());
+            } else {
+                throw new ArgumentException(ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mech));
+            }
+
+            if (reportAuthzIDArg.isPresent()) {
+                bindRequest.addControl(AuthorizationIdentityRequestControl.newControl(false));
+            }
+
+            if (usePasswordPolicyControlArg.isPresent()) {
+                bindRequest.addControl(PasswordPolicyRequestControl.newControl(false));
+            }
+        }
+        return bindRequest;
+    }
+
+    private String getMechanism() throws ArgumentException {
+        return getSaslProperty(SASL_PROPERTY_MECH);
+    }
+
+    private String getKDC() throws ArgumentException {
+        return getSaslProperty(SASL_PROPERTY_KDC);
+    }
+
+    private String getRealm() throws ArgumentException {
+        return getSaslProperty(SASL_PROPERTY_REALM);
+    }
+
+    private String getSaslProperty(String propertyName) throws ArgumentException {
+        for (final String s : saslOptionArg.getValues()) {
+            if (s.startsWith(propertyName)) {
+                return parseSASLOptionValue(s);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return connFactory.toString();
+    }
+
+    /**
+     * Retrieves a <CODE>KeyManager</CODE> object that may be used for
+     * interactions requiring access to a key manager.
+     *
+     * @param keyStoreFile
+     *            The path to the file containing the key store data.
+     * @return A set of <CODE>KeyManager</CODE> objects that may be used for
+     *         interactions requiring access to a key manager.
+     * @throws java.security.KeyStoreException
+     *             If a problem occurs while interacting with the key store.
+     * @throws IOException
+     *             If there is an I/O or format problem with the keystore data.
+     * @throws NoSuchAlgorithmException
+     *             If a problem occurs while loading with the key store.
+     * @throws CertificateException
+     *             If a problem occurs while loading with the key store.
+     */
+    public X509KeyManager getKeyManager(String keyStoreFile) throws KeyStoreException,
+            IOException, NoSuchAlgorithmException, CertificateException {
+        if (keyStoreFile == null) {
+            // Lookup the file name through the JDK property.
+            keyStoreFile = getKeyStore();
+        }
+
+        if (keyStoreFile == null) {
+            return null;
+        }
+
+        final String keyStorePass = getKeyStorePIN();
+        char[] keyStorePIN = null;
+        if (keyStorePass != null) {
+            keyStorePIN = keyStorePass.toCharArray();
+        }
+
+        final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
+        try (final FileInputStream fos = new FileInputStream(keyStoreFile)) {
+            keystore.load(fos, keyStorePIN);
+        }
+
+        return new ApplicationKeyManager(keystore, keyStorePIN);
+    }
+
+    /**
+     * Read the KeyStore from the JSSE system property.
+     *
+     * @return The path to the key store file.
+     */
+
+    private String getKeyStore() {
+        return System.getProperty("javax.net.ssl.keyStore");
+    }
+
+    /**
+     * Read the KeyStore PIN from the JSSE system property.
+     *
+     * @return The PIN that should be used to access the key store.
+     */
+
+    private String getKeyStorePIN() {
+        String pwd;
+        if (keyStorePasswordArg.isPresent()) {
+            pwd = keyStorePasswordArg.getValue();
+        } else if (keyStorePasswordFileArg.isPresent()) {
+            pwd = keyStorePasswordFileArg.getValue();
+        } else {
+            pwd = System.getProperty("javax.net.ssl.keyStorePassword");
+        }
+        return pwd;
+    }
+
+    /**
+     * Get the password which has to be used for the command. In interactive mode, if
+     * the password arguments are missing, the user will be prompted.
+     *
+     * @return The password stored into the specified file on by the command
+     *         line argument, or empty it if not specified.
+     * @throws ArgumentException
+     *             If a problem occurs while interacting with the password.
+     */
+    public char[] getPassword() throws ArgumentException {
+        char[] value = "".toCharArray();
+
+        if (bindPasswordArg.isPresent()) {
+            value = bindPasswordArg.getValue().toCharArray();
+        } else if (bindPasswordFileArg.isPresent()) {
+            value = bindPasswordFileArg.getValue().toCharArray();
+        } else if (password != null) {
+            return password;
+        }
+
+        if (value.length == 0 && app.isInteractive()) {
+            LocalizableMessage msg;
+            if (isAdminConnection) {
+                msg = INFO_LDAPAUTH_PASSWORD_PROMPT.get(getBindName());
+            } else {
+                msg = INFO_DESCRIPTION_BINDPASSWORD.get();
+            }
+            try {
+                value = app.readPassword(msg);
+                app.println();
+            } catch (ClientException e) {
+                throw new ArgumentException(ERR_ERROR_CANNOT_READ_PASSWORD.get(), e);
+            }
+            password = value;
+        }
+        return value;
+    }
+
+    /**
+     * Retrieves a <CODE>TrustManager</CODE> object that may be used for
+     * interactions requiring access to a trust manager.
+     *
+     * @return A set of <CODE>TrustManager</CODE> objects that may be used for
+     *         interactions requiring access to a trust manager.
+     * @throws IOException
+     *             If the trust store file could not be found or could not be read.
+     * @throws GeneralSecurityException
+     *             If a problem occurs while interacting with the trust store.
+     */
+    public TrustManager getTrustManager() throws IOException, GeneralSecurityException {
+        if (trustAllArg.isPresent()) {
+            return TrustManagers.trustAll();
+        }
+
+        X509TrustManager tm = null;
+        if (trustStorePathArg.isPresent() && trustStorePathArg.getValue().length() > 0) {
+            tm = TrustManagers.checkValidityDates(TrustManagers.checkHostName(hostNameArg.getValue(),
+                    TrustManagers.checkUsingTrustStore(trustStorePathArg.getValue(), getTrustStorePIN(), null)));
+        } else if (getTrustStore() != null) {
+            tm = TrustManagers.checkValidityDates(TrustManagers.checkHostName(hostNameArg.getValue(),
+                    TrustManagers.checkUsingTrustStore(getTrustStore(), getTrustStorePIN(), null)));
+        }
+
+        if (app != null && !app.isQuiet()) {
+            return new PromptingTrustManager(app, tm);
+        }
+
+        return tm;
+    }
+
+    /**
+     * Read the TrustStore from the JSSE system property.
+     *
+     * @return The path to the trust store file.
+     */
+
+    private String getTrustStore() {
+        return System.getProperty("javax.net.ssl.trustStore");
+    }
+
+    /**
+     * Read the TrustStore PIN from the JSSE system property.
+     *
+     * @return The PIN that should be used to access the trust store, can be null.
+     */
+    private char[] getTrustStorePIN() {
+        String pwd;
+        if (trustStorePasswordArg.isPresent()) {
+            pwd = trustStorePasswordArg.getValue();
+        } else if (trustStorePasswordFileArg.isPresent()) {
+            pwd = trustStorePasswordFileArg.getValue();
+        } else {
+            pwd = System.getProperty("javax.net.ssl.trustStorePassword");
+        }
+        return pwd == null ? null : pwd.toCharArray();
+    }
+
+    private String parseSASLOptionValue(final String option) throws ArgumentException {
+        final int equalPos = option.indexOf('=');
+        if (equalPos == -1) {
+            throw new ArgumentException(ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION.get(option));
+        }
+        return option.substring(equalPos + 1, option.length());
+    }
+
+    /**
+     * Specifies if this connection should be an administrator connection. If sets as one, the messages prompted to the
+     * user will be different as a normal connection. E.g if set :
+     *
+     * <pre>
+     * >>>> Specify OpenDJ LDAP connection parameters
+     *
+     * Directory server administration port number [4444]:
+     * </pre>
+     *
+     * vs normal mode
+     *
+     * <pre>
+     * >>>> Specify OpenDJ LDAP connection parameters
+     *
+     * Directory server port number [1389]:
+     * </pre>
+     */
+    public void setIsAnAdminConnection() {
+        isAdminConnection = true;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
new file mode 100644
index 0000000..3e22ef7
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
@@ -0,0 +1,767 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008-2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ * Portions copyright 2011 Nemanja Lukić
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.io.BufferedReader;
+import java.io.Console;
+import java.io.EOFException;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+
+/**
+ * This class provides an abstract base class which can be used as the basis of a console-based application.
+ */
+public abstract class ConsoleApplication {
+
+    private static final int PROGRESS_LINE = 70;
+
+    private final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+    private final InputStream in = System.in;
+    private final PrintStream out;
+    private final PrintStream err;
+    private final Console console = System.console();
+
+    private boolean isProgressSuite;
+
+    /** Defines the different line styles for output. */
+    public enum Style {
+        /** Defines a title. */
+        TITLE,
+        /** Defines a subtitle. */
+        SUBTITLE,
+        /** Defines a notice. */
+        NOTICE,
+        /** Defines a normal line. */
+        NORMAL,
+        /** Defines an error. */
+        ERROR,
+        /** Defines a warning. */
+        WARNING
+    }
+
+    /**
+     * Creates a new console application instance.
+     */
+    public ConsoleApplication() {
+        this(System.out, System.err);
+    }
+
+    /**
+     * Creates a new console application instance with provided standard and error out streams.
+     *
+     * @param out
+     *            The output stream.
+     * @param err
+     *            The error stream.
+     */
+    public ConsoleApplication(PrintStream out, PrintStream err) {
+        this.out = out;
+        this.err = err;
+    }
+
+    /**
+     * Returns the application error stream.
+     *
+     * @return The application error stream.
+     */
+    public final PrintStream getErrorStream() {
+        return err;
+    }
+
+    /**
+     * Returns the application input stream.
+     *
+     * @return The application input stream.
+     */
+    public final InputStream getInputStream() {
+        return in;
+    }
+
+    /**
+     * Returns the application output stream.
+     *
+     * @return The application output stream.
+     */
+    public final PrintStream getOutputStream() {
+        return out;
+    }
+
+    /**
+     * Indicates whether or not the user has requested interactive behavior. The default implementation returns
+     * {@code true}.
+     *
+     * @return {@code true} if the user has requested interactive behavior.
+     */
+    public boolean isInteractive() {
+        return true;
+    }
+
+    /**
+     * Indicates whether or not the user has requested quiet output. The default implementation returns {@code false}.
+     *
+     * @return {@code true} if the user has requested quiet output.
+     */
+    public boolean isQuiet() {
+        return false;
+    }
+
+    /**
+     * Indicates whether or not the user has requested script-friendly output. The default implementation returns
+     * {@code false}.
+     *
+     * @return {@code true} if the user has requested script-friendly output.
+     */
+    public boolean isScriptFriendly() {
+        return false;
+    }
+
+    /**
+     * Indicates whether or not the user has requested verbose output. The default implementation returns {@code false}.
+     *
+     * @return {@code true} if the user has requested verbose output.
+     */
+    public boolean isVerbose() {
+        return false;
+    }
+
+    /**
+     * Indicates whether or not the user has requested advanced mode.
+     *
+     * @return Returns <code>true</code> if the user has requested advanced mode.
+     */
+    public boolean isAdvancedMode() {
+        return false;
+    }
+
+    /**
+     * Indicates whether or not this console application is running in its menu-driven mode. This can be used to dictate
+     * whether output should go to the error stream or not. In addition, it may also dictate whether or not sub-menus
+     * should display a cancel option as well as a quit option.
+     *
+     * @return Returns <code>true</code> if this console application is running in its menu-driven mode.
+     */
+    public boolean isMenuDrivenMode() {
+        return false;
+    }
+
+    /**
+     * Interactively prompts the user to press return to continue. This method should be called in situations where a
+     * user needs to be given a chance to read some documentation before continuing (continuing may cause the
+     * documentation to be scrolled out of view).
+     */
+    public final void pressReturnToContinue() {
+        try {
+            readLineOfInput(INFO_MENU_PROMPT_RETURN_TO_CONTINUE.get());
+        } catch (final ClientException e) {
+            // Ignore the exception - applications don't care.
+        }
+    }
+
+    /**
+     * Displays a message to the error stream.
+     *
+     * @param msg
+     *            The message.
+     */
+    public final void errPrint(final LocalizableMessage msg) {
+        getErrStream().print(wrap(msg));
+    }
+
+    /**
+     * Displays a blank line to the error stream.
+     */
+    public final void errPrintln() {
+        getErrStream().println();
+    }
+
+    /**
+     * Displays a message to the error stream.
+     *
+     * @param msg
+     *            The message.
+     */
+    public final void errPrintln(final LocalizableMessage msg) {
+        getErrStream().println(wrap(msg));
+    }
+
+    /**
+     * Displays a message to the error stream indented by the specified number of columns.
+     *
+     * @param msg
+     *            The message.
+     * @param indent
+     *            The number of columns to indent.
+     */
+    public final void errPrintln(final LocalizableMessage msg, final int indent) {
+        getErrStream().println(wrapText(msg, MAX_LINE_WIDTH, indent));
+    }
+
+    /**
+     * Displays a message to the error stream if verbose mode is enabled.
+     *
+     * @param msg
+     *            The verbose message.
+     */
+    public final void errPrintVerboseMessage(final LocalizableMessage msg) {
+        if (isVerbose()) {
+            getErrStream().println(wrap(msg));
+        }
+    }
+
+    /**
+     * Displays a message to the output stream.
+     *
+     * @param msg
+     *            The message.
+     */
+    public final void print(final LocalizableMessage msg) {
+        if (!isQuiet()) {
+            out.print(wrap(msg));
+        }
+    }
+
+    /**
+     * Displays a blank line to the output stream.
+     */
+    public final void println() {
+        if (!isQuiet()) {
+            out.println();
+        }
+    }
+
+    /**
+     * Displays a message to the output stream.
+     *
+     * @param msg
+     *            The message.
+     */
+    public final void println(final LocalizableMessage msg) {
+        if (!isQuiet()) {
+            out.println(wrap(msg));
+        }
+    }
+
+    /**
+     * Displays a message to the output stream indented by the specified number of columns.
+     *
+     * @param msg
+     *            The message.
+     * @param indent
+     *            The number of columns to indent.
+     */
+    public final void println(final LocalizableMessage msg, final int indent) {
+        if (!isQuiet()) {
+            out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
+        }
+    }
+
+    /**
+     * Prints a progress bar on the same output stream line if not in quiet mode.
+     *
+     * <pre>
+     * Like
+     *   msg......   50%
+     *   if progress is up to 100 :
+     *   msg.....................  100%
+     *   if progress is < 0 :
+     *   msg....  FAIL
+     *   msg.....................  FAIL
+     * </pre>
+     *
+     * @param linePos
+     *            The progress bar starts at this position on the line.
+     * @param progress
+     *            The current percentage progress to print.
+     */
+    private final void printProgressBar(final int linePos, final int progress) {
+        if (!isQuiet()) {
+            final int spacesLeft = MAX_LINE_WIDTH - linePos - 10;
+            StringBuilder bar = new StringBuilder();
+            if (progress != 0) {
+                for (int i = 0; i < PROGRESS_LINE; i++) {
+                    if (i < (Math.abs(progress) * spacesLeft) / 100 && bar.length() < spacesLeft) {
+                        bar.append(".");
+                    }
+                }
+            }
+            bar.append(".   ");
+            if (progress >= 0) {
+                bar.append(progress).append("%     ");
+            } else {
+                bar.append("FAIL");
+                isProgressSuite = false;
+            }
+            final int endBuilder = linePos + bar.length();
+            for (int i = 0; i < endBuilder; i++) {
+                bar.append("\b");
+            }
+            if (progress >= 100 || progress < 0) {
+                bar.append(EOL);
+                isProgressSuite = false;
+            }
+            out.print(bar);
+        }
+    }
+
+    /**
+     * Prints a progress bar on the same output stream line if not in quiet mode.
+     * If the line's length is upper than the limit, the message is wrapped and the progress
+     * bar is affected to the last one.
+     * e.g.
+     * <pre>
+     *   Changing matching rule for 'userCertificate' and 'caCertificate' to
+     *   CertificateExactMatch...............................................   100%
+     * </pre>
+     *
+     * @param msg
+     *            The message to display before the progress line.
+     * @param progress
+     *            The current percentage progress to print.
+     * @param indent
+     *            Indentation of the message.
+     */
+    public final void printProgressBar(String msg, final int progress, final int indent) {
+        if (!isQuiet()) {
+            String msgToDisplay = wrapText(msg, PROGRESS_LINE, indent);
+            if (msgToDisplay.length() > PROGRESS_LINE) {
+                final String[] msgWrapped = msgToDisplay.split(LINE_SEPARATOR);
+                if (!isProgressSuite) {
+                    for (int pos = 0; pos < msgWrapped.length - 1; pos++) {
+                        println(LocalizableMessage.raw(msgWrapped[pos]));
+                    }
+                    isProgressSuite = true;
+                }
+                msgToDisplay = msgWrapped[msgWrapped.length - 1];
+            }
+            print(LocalizableMessage.raw(msgToDisplay));
+            printProgressBar(msgToDisplay.length(), progress);
+        }
+    }
+
+    /**
+     * Print a line with EOL in the output stream.
+     *
+     * @param msgStyle
+     *            The type of formatted output desired.
+     * @param msg
+     *            The message to display in normal mode.
+     * @param indent
+     *            The indentation.
+     */
+    public final void println(final Style msgStyle, final LocalizableMessage msg, final int indent) {
+        if (!isQuiet()) {
+            switch (msgStyle) {
+            case TITLE:
+                out.println();
+                out.println(">>>> " + wrapText(msg, MAX_LINE_WIDTH, indent));
+                out.println();
+                break;
+            case SUBTITLE:
+                out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
+                out.println();
+                break;
+            case NOTICE:
+                out.println(wrapText("* " + msg, MAX_LINE_WIDTH, indent));
+                break;
+            case ERROR:
+                out.println();
+                out.println(wrapText("** " + msg, MAX_LINE_WIDTH, indent));
+                out.println();
+                break;
+            case WARNING:
+                out.println(wrapText("[!] " + msg, MAX_LINE_WIDTH, indent));
+                break;
+            default:
+                out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
+                break;
+            }
+        }
+    }
+
+    /**
+     * Displays a message to the output stream if verbose mode is enabled.
+     *
+     * @param msg
+     *            The verbose message.
+     */
+    public final void printVerboseMessage(final LocalizableMessage msg) {
+        if (isVerbose()) {
+            out.println(wrap(msg));
+        }
+    }
+
+    /**
+     * Interactively prompts (on error output) the user to provide a string value. Any non-empty string will be allowed
+     * (the empty string will indicate that the default should be used, if there is one).
+     *
+     * @param prompt
+     *            The prompt to present to the user.
+     * @param defaultValue
+     *            The default value to assume if the user presses ENTER without typing anything, or {@code null} if
+     *            there should not be a default and the user must explicitly provide a value.
+     * @throws ClientException
+     *             If the line of input could not be retrieved for some reason.
+     * @return The string value read from the user.
+     */
+    public final String readInput(LocalizableMessage prompt, final String defaultValue) throws ClientException {
+        return readInput(prompt, defaultValue, null);
+    }
+
+    /**
+     * Interactively prompts (on error output) the user to provide a string value. Any non-empty string will be allowed
+     * (the empty string will indicate that the default should be used, if there is one).
+     *
+     * @param prompt
+     *            The prompt to present to the user.
+     * @param defaultValue
+     *            The default value to assume if the user presses ENTER without typing anything, or {@code null} if
+     *            there should not be a default and the user must explicitly provide a value.
+     * @param msgStyle
+     *            The formatted style chosen.
+     * @throws ClientException
+     *             If the line of input could not be retrieved for some reason.
+     * @return The string value read from the user.
+     */
+    public final String readInput(LocalizableMessage prompt, final String defaultValue, final Style msgStyle)
+            throws ClientException {
+        if (msgStyle != null && msgStyle == Style.TITLE) {
+            println();
+        }
+        while (true) {
+            if (defaultValue != null) {
+                prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
+            }
+            final String response = readLineOfInput(prompt);
+
+            if (msgStyle != null && (msgStyle == Style.TITLE || msgStyle == Style.SUBTITLE)) {
+                println();
+            }
+
+            if ("".equals(response)) {
+                if (defaultValue != null) {
+                    return defaultValue;
+                }
+                println(INFO_ERROR_EMPTY_RESPONSE.get());
+            }
+            return response;
+        }
+    }
+
+    /**
+     * Interactively reads a password from the console.
+     *
+     * @param prompt
+     *            The password prompt.
+     * @return The password.
+     * @throws ClientException
+     *             If the password could not be retrieved for some reason.
+     */
+    public final char[] readPassword(final LocalizableMessage prompt) throws ClientException {
+        if (console != null) {
+            if (prompt != null) {
+                out.print(wrap(prompt));
+                out.print(" ");
+            }
+            try {
+                final char[] password = console.readPassword();
+                if (password == null) {
+                    throw new EOFException("End of input");
+                }
+                return password;
+            } catch (final Throwable e) {
+                throw ClientException.adaptInputException(e);
+            }
+        } else {
+            // FIXME: should go direct to char[] and avoid the String.
+            return readLineOfInput(prompt).toCharArray();
+        }
+    }
+
+    /**
+     * Reads a password from the console without echoing it to the client.
+     * FIXME This method should disappear when all
+     * the tools will extend to ConsoleApplication.
+     *
+     * @return The password as an array of characters.
+     * @throws ClientException
+     *             If an error occurs when reading the password.
+     */
+    public static char[] readPassword() throws ClientException {
+        try {
+            return System.console().readPassword();
+        } catch (IOError e) {
+            throw ClientException.adaptInputException(e);
+        }
+    }
+
+    /**
+     * Interactively retrieves a line of input from the console.
+     *
+     * @param prompt
+     *            The prompt.
+     * @return The line of input.
+     * @throws ClientException
+     *             If the line of input could not be retrieved for some reason.
+     */
+    public final String readLineOfInput(final LocalizableMessage prompt) throws ClientException {
+        if (prompt != null) {
+            out.print(wrap(prompt));
+            out.print(" ");
+        }
+        try {
+            final String s = reader.readLine();
+            if (s == null) {
+                throw ClientException.adaptInputException(new EOFException("End of input"));
+            }
+            return s;
+        } catch (final IOException e) {
+            throw ClientException.adaptInputException(e);
+        }
+    }
+
+    /**
+     * Interactively retrieves a port value from the console.
+     *
+     * @param prompt
+     *            The port prompt.
+     * @param defaultValue
+     *            The port default value.
+     * @return Returns the port.
+     * @throws ClientException
+     *             If the port could not be retrieved for some reason.
+     */
+    public final int readPort(LocalizableMessage prompt, final int defaultValue) throws ClientException {
+        if (defaultValue != -1) {
+            prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
+        }
+
+        return readValidatedInput(prompt, Utils.portValidationCallback(defaultValue), CONFIRMATION_MAX_TRIES);
+    }
+
+    /**
+     * Interactively prompts for user input and continues until valid input is provided.
+     *
+     * @param <T>
+     *            The type of decoded user input.
+     * @param prompt
+     *            The interactive prompt which should be displayed on each input attempt.
+     * @param validator
+     *            An input validator responsible for validating and decoding the user's response.
+     * @return Returns the decoded user's response.
+     * @throws ClientException
+     *             If an unexpected error occurred which prevented validation.
+     */
+    public final <T> T readValidatedInput(final LocalizableMessage prompt, final ValidationCallback<T> validator)
+            throws ClientException {
+        while (true) {
+            final String response = readLineOfInput(prompt);
+            final T value = validator.validate(this, response);
+            if (value != null) {
+                return value;
+            }
+        }
+    }
+
+    /**
+     * Interactively prompts for user input and continues until valid input is provided.
+     *
+     * @param <T>
+     *            The type of decoded user input.
+     * @param prompt
+     *            The interactive prompt which should be displayed on each input attempt.
+     * @param validator
+     *            An input validator responsible for validating and decoding the user's response.
+     * @param maxTries
+     *            The maximum number of tries that we can make.
+     * @return Returns the decoded user's response.
+     * @throws ClientException
+     *             If an unexpected error occurred which prevented validation or if the maximum number of tries was
+     *             reached.
+     */
+    public final <T> T readValidatedInput(final LocalizableMessage prompt, final ValidationCallback<T> validator,
+            final int maxTries) throws ClientException {
+        int nTries = 0;
+        while (nTries < maxTries) {
+            final String response = readLineOfInput(prompt);
+            final T value = validator.validate(this, response);
+            if (value != null) {
+                return value;
+            }
+            nTries++;
+        }
+        throw new ClientException(ReturnCode.ERROR_USER_DATA, ERR_TRIES_LIMIT_REACHED.get(maxTries));
+    }
+
+    /**
+     * Inserts line breaks into the provided buffer to wrap text at no more than the specified column width (80).
+     *
+     * @param msg
+     *            The message to wrap.
+     * @return The wrapped message.
+     */
+    private String wrap(final LocalizableMessage msg) {
+        return wrapText(msg, MAX_LINE_WIDTH);
+    }
+
+    /**
+     * Returns the error stream. Effectively, when an application is in "interactive mode" all the informations should
+     * be written in the STDout.
+     *
+     * @return The error stream that should be used with this application.
+     */
+    protected PrintStream getErrStream() {
+        if (isInteractive()) {
+            return out;
+        }
+        return err;
+    }
+
+    /**
+     * Commodity method that interactively confirms whether a user wishes to perform an action. If
+     * the application is non-interactive, then the provided default is returned automatically. If there is an error an
+     * error message is logged to the provided Logger and the default value is returned.
+     *
+     * @param prompt
+     *            The prompt describing the action.
+     * @param defaultValue
+     *            The default value for the confirmation message. This will be returned if the application is
+     *            non-interactive or if the user just presses return.
+     * @param logger
+     *            the Logger to be used to log the error message.
+     * @return Returns <code>true</code> if the user wishes the action to be performed, or <code>false</code> if they
+     *         refused.
+     * @throws ClientException
+     *             if the user did not provide valid answer after a certain number of tries
+     *             (ConsoleApplication.CONFIRMATION_MAX_TRIES)
+     */
+    protected final boolean askConfirmation(LocalizableMessage prompt, boolean defaultValue, LocalizedLogger logger)
+            throws ClientException {
+        int nTries = 0;
+        while (nTries < CONFIRMATION_MAX_TRIES) {
+            nTries++;
+            try {
+                return confirmAction(prompt, defaultValue);
+            } catch (ClientException ce) {
+                if (ce.getMessageObject().toString().contains(ERR_CONFIRMATION_TRIES_LIMIT_REACHED.get(nTries))) {
+                    throw ce;
+                }
+                logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
+                // Try again...
+                println();
+            }
+        }
+
+        throw new ClientException(ReturnCode.ERROR_USER_DATA,
+            ERR_CONFIRMATION_TRIES_LIMIT_REACHED.get(CONFIRMATION_MAX_TRIES));
+    }
+
+    /**
+     * Interactively confirms whether a user wishes to perform an action.
+     * If the application is non-interactive, then the provided default is returned automatically.
+     *
+     * @param prompt
+     *            The prompt describing the action.
+     * @param defaultValue
+     *            The default value for the confirmation message. This will be returned if the application is
+     *            non-interactive or if the user just presses return.
+     * @return Returns <code>true</code> if the user wishes the action to be performed, or <code>false</code> if they
+     *         refused, or if an exception occurred.
+     * @throws ClientException
+     *             If the user's response could not be read from the console for some reason.
+     */
+    public final boolean confirmAction(LocalizableMessage prompt, final boolean defaultValue) throws ClientException {
+        if (!isInteractive()) {
+            return defaultValue;
+        }
+
+        final LocalizableMessage yes = INFO_GENERAL_YES.get();
+        final LocalizableMessage no = INFO_GENERAL_NO.get();
+        final LocalizableMessage errMsg = ERR_CONSOLE_APP_CONFIRM.get(yes, no);
+        prompt = INFO_MENU_PROMPT_CONFIRM.get(prompt, yes, no, defaultValue ? yes : no);
+
+        ValidationCallback<Boolean> validator = new ValidationCallback<Boolean>() {
+
+            @Override
+            public Boolean validate(ConsoleApplication app, String input) {
+                String ninput = input.toLowerCase().trim();
+                if (ninput.length() == 0) {
+                    return defaultValue;
+                } else if (no.toString().toLowerCase().startsWith(ninput)) {
+                    return false;
+                } else if (yes.toString().toLowerCase().startsWith(ninput)) {
+                    return true;
+                } else {
+                    // Try again...
+                    app.println();
+                    app.println(errMsg);
+                    app.println();
+                    return null;
+                }
+            }
+        };
+
+        return readValidatedInput(prompt, validator, CONFIRMATION_MAX_TRIES);
+    }
+
+    /**
+     * Commodity method used to repeatedly ask the user to provide a port value.
+     *
+     * @param prompt
+     *            the prompt message.
+     * @param defaultValue
+     *            the default value of the port to be proposed to the user.
+     * @param logger
+     *            the logger where the errors will be written.
+     * @return the port value provided by the user.
+     */
+    protected int askPort(LocalizableMessage prompt, int defaultValue, LocalizedLogger logger) {
+        while (true) {
+            try {
+                int port = readPort(prompt, defaultValue);
+                if (port != -1) {
+                    return port;
+                }
+            } catch (ClientException ce) {
+                logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
+            }
+        }
+    }
+
+    /**
+     * Prints a header in the console application.
+     *
+     * @param header
+     *            The message to display as a header.
+     */
+    void printHeader(final LocalizableMessage header) {
+        println();
+        println();
+        println(header);
+        println();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocDescriptionSupplement.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocDescriptionSupplement.java
new file mode 100644
index 0000000..43bdfbd
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocDescriptionSupplement.java
@@ -0,0 +1,32 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * Documentation that supplements generated descriptions.
+ */
+public interface DocDescriptionSupplement {
+
+    /**
+     * Retrieves a supplement to the description intended for use in generated reference documentation.
+     *
+     * @return The supplement to the description for use in generated reference documentation,
+     *         or LocalizableMessage.EMPTY if there is no supplement.
+     */
+    LocalizableMessage getDocDescriptionSupplement();
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
new file mode 100644
index 0000000..b8f410a
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
@@ -0,0 +1,124 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateExceptionHandler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.Map;
+
+/**
+ * This class provides utility functions to help generate reference documentation.
+ */
+public final class DocGenerationHelper {
+
+    /** Prevent instantiation. */
+    private DocGenerationHelper() {
+        // Do nothing.
+    }
+
+    /** FreeMarker template configuration. */
+    private static Configuration configuration;
+
+    /**
+     * Gets a FreeMarker configuration for applying templates.
+     *
+     * @return              A FreeMarker configuration.
+     */
+    private static Configuration getConfiguration() {
+        if (configuration == null) {
+            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+            configuration.setClassForTemplateLoading(DocGenerationHelper.class, "/templates");
+            configuration.setDefaultEncoding("UTF-8");
+            configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
+        }
+        return configuration;
+    }
+
+    /**
+     * Appends the String result from applying a FreeMarker template.
+     *
+     * @param builder       Append the result to this.
+     * @param template      The name of a template file found in {@code resources/templates/}.
+     * @param map           The map holding the data to use in the template.
+     */
+    public static void applyTemplate(StringBuilder builder, final String template, final Map<String, Object> map) {
+        // FixMe: This method is public so it can be used by the SubCommandUsageHandler
+        // in org.forgerock.opendj.config.dsconfig.DSConfig.
+
+        // FreeMarker requires a configuration to find the template.
+        configuration = getConfiguration();
+
+        // FreeMarker takes the data and a Writer to process the template.
+        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            Writer writer = new OutputStreamWriter(outputStream)) {
+            Template configurationTemplate = configuration.getTemplate(template);
+            configurationTemplate.process(map, writer);
+            builder.append(outputStream.toString());
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Returns an option synopsis.
+     *
+     * <br>
+     *
+     * Note: The synopsis might contain characters that must be escaped in XML.
+     *
+     * @param argument  The argument option.
+     * @return          A synopsis.
+     */
+    static String getOptionSynopsis(final Argument argument) {
+        StringBuilder builder = new StringBuilder();
+
+        final Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            builder.append("-").append(shortID.charValue());
+        }
+        final String longID = argument.getLongIdentifier();
+        if (shortID != null && longID != null) {
+            builder.append(" | ");
+        }
+        if (longID != null) {
+            builder.append("--").append(longID);
+        }
+        if (argument.needsValue()) {
+            builder.append(" ").append(argument.getValuePlaceholder());
+        }
+
+        return builder.toString();
+    }
+
+    /**
+     * Returns true when the argument handles properties.
+     *
+     * @param argument  The argument.
+     * @return True if the argument handles properties.
+     */
+    public static boolean doesHandleProperties(final Argument argument) {
+        // FixMe: This method is public so it can be used by the SubCommandUsageHandler
+        // in org.forgerock.opendj.config.dsconfig.DSConfig.
+
+        final String id = argument.getLongIdentifier();
+        return ("add".equals(id) || "remove".equals(id) || "reset".equals(id) || "set".equals(id));
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/FileBasedArgument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/FileBasedArgument.java
new file mode 100644
index 0000000..7a8d962
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/FileBasedArgument.java
@@ -0,0 +1,169 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+/**
+ * This class defines an argument whose value will be read from a file rather
+ * than actually specified on the command-line. When a value is specified on the
+ * command line, it will be treated as the path to the file containing the
+ * actual value rather than the value itself. <BR>
+ * <BR>
+ * Note that if no filename is provided on the command line but a default
+ * value is specified programmatically or if the default value is read from a
+ * specified property, then that default value will be taken as the actual value
+ * rather than a filename. <BR>
+ * <BR>
+ * Also note that this argument type assumes that the entire value for the
+ * argument is on a single line in the specified file. If the file contains
+ * multiple lines, then only the first line will be read.
+ */
+public final class FileBasedArgument extends Argument {
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * {@link FileBasedArgument}.
+     *
+     * @param longIdentifier
+     *         The generic long identifier that will be used to refer to this argument.
+     * @return A builder to continue building the {@link FileBasedArgument}.
+     */
+    public static Builder builder(final String longIdentifier) {
+        return new Builder(longIdentifier);
+    }
+
+    /** A fluent API for incrementally constructing {@link FileBasedArgument}. */
+    public static final class Builder extends ArgumentBuilder<Builder, String, FileBasedArgument> {
+        private Builder(final String longIdentifier) {
+            super(longIdentifier);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        @Override
+        public FileBasedArgument buildArgument() throws ArgumentException {
+            return new FileBasedArgument(this);
+        }
+    }
+
+    /** The mapping between filenames specified and the first lines read from those files. */
+    private final Map<String, String> namesToValues = new LinkedHashMap<>();
+
+    private FileBasedArgument(final Builder builder) throws ArgumentException {
+        super(builder);
+    }
+
+    /**
+     * Adds a value to the set of values for this argument. This should only be
+     * called if the value is allowed by the <CODE>valueIsAcceptable</CODE>
+     * method. Note that in this case, correct behavior depends on a previous
+     * successful call to <CODE>valueIsAcceptable</CODE> so that the value read
+     * from the file may be stored in the name-to-value hash and used in place
+     * of the filename here.
+     *
+     * @param valueString
+     *            The string representation of the value to add to this
+     *            argument.
+     */
+    @Override
+    public void addValue(final String valueString) {
+        final String actualValue = namesToValues.get(valueString);
+        if (actualValue != null) {
+            super.addValue(actualValue);
+        }
+    }
+
+    /**
+     * Retrieves a map between the filenames specified on the command line and
+     * the first lines read from those files.
+     *
+     * @return A map between the filenames specified on the command line and the
+     *         first lines read from those files.
+     */
+    public Map<String, String> getNameToValueMap() {
+        return namesToValues;
+    }
+
+    /**
+     * Indicates whether the provided value is acceptable for use in this
+     * argument.
+     *
+     * @param valueString
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            A buffer into which the invalid reason may be written if the
+     *            value is not acceptable.
+     * @return <CODE>true</CODE> if the value is acceptable, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    @Override
+    public boolean valueIsAcceptable(final String valueString,
+            final LocalizableMessageBuilder invalidReason) {
+        // First, make sure that the specified file exists.
+        File valueFile;
+        try {
+            valueFile = new File(valueString);
+            if (!valueFile.exists()) {
+                invalidReason.append(ERR_FILEARG_NO_SUCH_FILE.get(valueString, longIdentifier));
+                return false;
+            }
+        } catch (final Exception e) {
+            invalidReason.append(ERR_FILEARG_CANNOT_VERIFY_FILE_EXISTENCE.get(
+                    valueString, longIdentifier, getExceptionMessage(e)));
+            return false;
+        }
+
+        // Open the file, read the first line and close the file.
+        String line;
+        try (BufferedReader reader = new BufferedReader(new FileReader(valueFile))) {
+            line = reader.readLine();
+        } catch (final FileNotFoundException e) {
+            invalidReason.append(ERR_FILEARG_CANNOT_OPEN_FILE.get(valueString, longIdentifier, getExceptionMessage(e)));
+            return false;
+        } catch (final IOException e) {
+            invalidReason.append(ERR_FILEARG_CANNOT_READ_FILE.get(valueString, longIdentifier, getExceptionMessage(e)));
+            return false;
+        }
+
+        // If the line read is null, then that means the file was empty.
+        if (line == null) {
+            invalidReason.append(ERR_FILEARG_EMPTY_FILE.get(valueString, longIdentifier));
+            return false;
+        }
+
+        // Store the value in the hash so it will be available for addValue.
+        // We won't do any validation on the value itself, so anything that we
+        // read will be considered acceptable.
+        namesToValues.put(valueString, line);
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java
new file mode 100644
index 0000000..ba11844
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/HelpCallback.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * An interface for displaying help interactively.
+ */
+public interface HelpCallback {
+
+    /**
+     * Displays help to the provided application console.
+     *
+     * @param app
+     *            The console application.
+     */
+    void display(ConsoleApplication app);
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java
new file mode 100644
index 0000000..a17cb12
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/IntegerArgument.java
@@ -0,0 +1,139 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+/**
+ * This class defines an argument type that will only accept integer values, and
+ * potentially only those in a given range.
+ */
+public final class IntegerArgument extends Argument {
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * {@link IntegerArgument}.
+     *
+     * @param name
+     *         The generic name that will be used to refer to this argument.
+     * @return A builder to continue building the {@link IntegerArgument}.
+     */
+    public static Builder builder(final String name) {
+        return new Builder(name);
+    }
+
+    /** A fluent API for incrementally constructing {@link IntegerArgument}. */
+    public static final class Builder extends ArgumentBuilder<Builder, Integer, IntegerArgument> {
+        private int lowerBound = Integer.MIN_VALUE;
+        private int upperBound = Integer.MAX_VALUE;
+
+        private Builder(final String name) {
+            super(name);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Sets the lower bound of this {@link IntegerArgument}.
+         *
+         * @param lowerBound
+         *         The lower bound value.
+         * @return This builder.
+         */
+        public Builder lowerBound(final int lowerBound) {
+            this.lowerBound = lowerBound;
+            return getThis();
+        }
+
+        /**
+         * Sets the range of this {@link IntegerArgument}.
+         *
+         * @param lowerBound
+         *          The range lower bound value.
+         * @param upperBound
+         *          The range upper bound value.
+         * @return This builder.
+         */
+        public Builder range(final int lowerBound, final int upperBound) {
+            this.lowerBound = lowerBound;
+            this.upperBound = upperBound;
+            return getThis();
+        }
+
+        @Override
+        public IntegerArgument buildArgument() throws ArgumentException {
+            return new IntegerArgument(this, lowerBound, upperBound);
+        }
+    }
+
+    /** The lower bound that will be enforced for this argument. */
+    private final int lowerBound;
+    /** The upper bound that will be enforced for this argument. */
+    private final int upperBound;
+
+    private IntegerArgument(final Builder builder, final int lowerBound, final int upperBound)
+            throws ArgumentException {
+        super(builder);
+        this.lowerBound = lowerBound;
+        this.upperBound = upperBound;
+
+        if (lowerBound > upperBound) {
+            final LocalizableMessage message =
+                    ERR_INTARG_LOWER_BOUND_ABOVE_UPPER_BOUND.get(builder.longIdentifier, lowerBound, upperBound);
+            throw new ArgumentException(message);
+        }
+    }
+
+    /**
+     * Indicates whether the provided value is acceptable for use in this
+     * argument.
+     *
+     * @param valueString
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            A buffer into which the invalid reason may be written if the
+     *            value is not acceptable.
+     * @return <CODE>true</CODE> if the value is acceptable, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    @Override
+    public boolean valueIsAcceptable(final String valueString, final LocalizableMessageBuilder invalidReason) {
+        try {
+            final int intValue = Integer.parseInt(valueString);
+            if (intValue < lowerBound) {
+                invalidReason.append(ERR_INTARG_VALUE_BELOW_LOWER_BOUND.get(longIdentifier, intValue, lowerBound));
+                return false;
+            }
+
+            if (intValue > upperBound) {
+                invalidReason.append(ERR_INTARG_VALUE_ABOVE_UPPER_BOUND.get(longIdentifier, intValue, upperBound));
+                return false;
+            }
+
+            return true;
+        } catch (final NumberFormatException e) {
+            invalidReason.append(ERR_ARG_CANNOT_DECODE_AS_INT.get(valueString, longIdentifier));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java
new file mode 100644
index 0000000..ec75b82
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Menu.java
@@ -0,0 +1,38 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * An interactive console-based menu.
+ *
+ * @param <T>
+ *          The type of success result value(s) returned by the
+ *          call-back. Use <code>Void</code> if the call-backs do
+ *          not return any values.
+ */
+public interface Menu<T> {
+
+    /**
+     * Displays the menu and waits for the user to select a valid option. When the user selects an option, the call-back
+     * associated with the option will be invoked and its result returned.
+     *
+     * @return Returns the result of invoking the chosen menu call-back.
+     * @throws ClientException
+     *             If an I/O exception occurred or if one of the menu option call-backs failed for some reason.
+     */
+    MenuResult<T> run() throws ClientException;
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java
new file mode 100644
index 0000000..07b3ce7
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuBuilder.java
@@ -0,0 +1,701 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2007-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * An interface for incrementally building a command-line menu.
+ *
+ * @param <T>
+ *            The type of value returned by the call-backs. Use <code>Void</code> if the call-backs do not return a
+ *            value.
+ */
+public final class MenuBuilder<T> {
+
+    /**
+     * A simple menu option call-back which is a composite of zero or more underlying call-backs.
+     *
+     * @param <T>
+     *            The type of value returned by the call-back.
+     */
+    private static final class CompositeCallback<T> implements MenuCallback<T> {
+
+        /** The list of underlying call-backs. */
+        private final Collection<MenuCallback<T>> callbacks;
+
+        /**
+         * Creates a new composite call-back with the specified set of call-backs.
+         *
+         * @param callbacks
+         *            The set of call-backs.
+         */
+        public CompositeCallback(Collection<MenuCallback<T>> callbacks) {
+            this.callbacks = callbacks;
+        }
+
+        @Override
+        public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
+            List<T> values = new ArrayList<>();
+            for (MenuCallback<T> callback : callbacks) {
+                MenuResult<T> result = callback.invoke(app);
+                if (!result.isSuccess()) {
+                    // Throw away all the other results.
+                    return result;
+                }
+                values.addAll(result.getValues());
+            }
+            return MenuResult.success(values);
+        }
+    }
+
+    /**
+     * Underlying menu implementation generated by this menu builder.
+     *
+     * @param <T>
+     *            The type of value returned by the call-backs. Use <code>Void</code> if the call-backs do not return a
+     *            value.
+     */
+    private static final class MenuImpl<T> implements Menu<T> {
+
+        /** Indicates whether the menu will allow selection of multiple numeric options. */
+        private final boolean allowMultiSelect;
+
+        /** The application console. */
+        private final ConsoleApplication app;
+
+        /** The call-back lookup table. */
+        private final Map<String, MenuCallback<T>> callbacks;
+
+        /** The char options table builder. */
+        private final TableBuilder cbuilder;
+
+        /** The call-back for the optional default action. */
+        private final MenuCallback<T> defaultCallback;
+        /** The description of the optional default action. */
+        private final LocalizableMessage defaultDescription;
+
+        /** The numeric options table builder. */
+        private final TableBuilder nbuilder;
+
+        /** The table printer. */
+        private final TablePrinter printer;
+
+        /** The menu prompt. */
+        private final LocalizableMessage prompt;
+
+        /** The menu title. */
+        private final LocalizableMessage title;
+
+        /**
+         * The maximum number of times we display the menu if the user provides
+         * bad input (-1 for unlimited).
+         */
+        private int nMaxTries;
+
+        /** Private constructor. */
+        private MenuImpl(ConsoleApplication app, LocalizableMessage title, LocalizableMessage prompt,
+                TableBuilder ntable, TableBuilder ctable, TablePrinter printer, Map<String, MenuCallback<T>> callbacks,
+                boolean allowMultiSelect, MenuCallback<T> defaultCallback, LocalizableMessage defaultDescription,
+                int nMaxTries) {
+            this.app = app;
+            this.title = title;
+            this.prompt = prompt;
+            this.nbuilder = ntable;
+            this.cbuilder = ctable;
+            this.printer = printer;
+            this.callbacks = callbacks;
+            this.allowMultiSelect = allowMultiSelect;
+            this.defaultCallback = defaultCallback;
+            this.defaultDescription = defaultDescription;
+            this.nMaxTries = nMaxTries;
+        }
+
+        @Override
+        public MenuResult<T> run() throws ClientException {
+            // The validation call-back which will be used to determine the
+            // action call-back.
+            ValidationCallback<MenuCallback<T>> validator = new ValidationCallback<MenuCallback<T>>() {
+
+                @Override
+                public MenuCallback<T> validate(ConsoleApplication app, String input) {
+                    String ninput = input.trim();
+
+                    if (ninput.length() == 0) {
+                        if (defaultCallback != null) {
+                            return defaultCallback;
+                        } else if (allowMultiSelect) {
+                            app.println();
+                            app.println(ERR_MENU_BAD_CHOICE_MULTI.get());
+                            app.println();
+                            return null;
+                        } else {
+                            app.println();
+                            app.println(ERR_MENU_BAD_CHOICE_SINGLE.get());
+                            app.println();
+                            return null;
+                        }
+                    } else if (allowMultiSelect) {
+                        // Use a composite call-back to collect all the results.
+                        List<MenuCallback<T>> cl = new ArrayList<>();
+                        for (String value : ninput.split(",")) {
+                            // Make sure that there are no duplicates.
+                            String nvalue = value.trim();
+                            Set<String> choices = new HashSet<>();
+
+                            if (choices.contains(nvalue)) {
+                                app.println();
+                                app.println(ERR_MENU_BAD_CHOICE_MULTI_DUPE.get(value));
+                                app.println();
+                                return null;
+                            } else if (!callbacks.containsKey(nvalue)) {
+                                app.println();
+                                app.println(ERR_MENU_BAD_CHOICE_MULTI.get());
+                                app.println();
+                                return null;
+                            } else {
+                                cl.add(callbacks.get(nvalue));
+                                choices.add(nvalue);
+                            }
+                        }
+
+                        return new CompositeCallback<>(cl);
+                    } else if (!callbacks.containsKey(ninput)) {
+                        app.println();
+                        app.println(ERR_MENU_BAD_CHOICE_SINGLE.get());
+                        app.println();
+                        return null;
+                    } else {
+                        return callbacks.get(ninput);
+                    }
+                }
+            };
+
+            // Determine the correct choice prompt.
+            LocalizableMessage promptMsg;
+            if (allowMultiSelect) {
+                if (defaultDescription != null) {
+                    promptMsg = INFO_MENU_PROMPT_MULTI_DEFAULT.get(defaultDescription);
+                } else {
+                    promptMsg = INFO_MENU_PROMPT_MULTI.get();
+                }
+            } else {
+                if (defaultDescription != null) {
+                    promptMsg = INFO_MENU_PROMPT_SINGLE_DEFAULT.get(defaultDescription);
+                } else {
+                    promptMsg = INFO_MENU_PROMPT_SINGLE.get();
+                }
+            }
+
+            // If the user selects help then we need to loop around and
+            // display the menu again.
+            while (true) {
+                // Display the menu.
+                if (title != null) {
+                    app.println(title);
+                    app.println();
+                }
+
+                if (prompt != null) {
+                    app.println(prompt);
+                    app.println();
+                }
+
+                if (nbuilder.getTableHeight() > 0) {
+                    nbuilder.print(printer);
+                    app.println();
+                }
+
+                if (cbuilder.getTableHeight() > 0) {
+                    TextTablePrinter cprinter = new TextTablePrinter(app.getErrorStream());
+                    cprinter.setDisplayHeadings(false);
+                    int sz = String.valueOf(nbuilder.getTableHeight()).length() + 1;
+                    cprinter.setIndentWidth(4);
+                    cprinter.setColumnWidth(0, sz);
+                    cprinter.setColumnWidth(1, 0);
+                    cbuilder.print(cprinter);
+                    app.println();
+                }
+
+                // Get the user's choice.
+                MenuCallback<T> choice;
+                if (nMaxTries != -1) {
+                    choice = app.readValidatedInput(promptMsg, validator, nMaxTries);
+                } else {
+                    choice = app.readValidatedInput(promptMsg, validator);
+                }
+
+                // Invoke the user's selected choice.
+                MenuResult<T> result = choice.invoke(app);
+
+                // Determine if the help needs to be displayed, display it and start again.
+                if (!result.isAgain()) {
+                    return result;
+                }
+                app.println();
+                app.println();
+            }
+        }
+    }
+
+    /**
+     * A simple menu option call-back which does nothing but return the provided menu result.
+     *
+     * @param <T>
+     *            The type of result returned by the call-back.
+     */
+    private static final class ResultCallback<T> implements MenuCallback<T> {
+
+        /** The result to be returned by this call-back. */
+        private final MenuResult<T> result;
+
+        /** Private constructor. */
+        private ResultCallback(MenuResult<T> result) {
+            this.result = result;
+        }
+
+        @Override
+        public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "(result=" + result + ")";
+        }
+    }
+
+    /** The multiple column display threshold. */
+    private int threshold = -1;
+
+    /** Indicates whether the menu will allow selection of multiple numeric options. */
+    private boolean allowMultiSelect;
+
+    /** The application console. */
+    private final ConsoleApplication app;
+
+    /** The char option call-backs. */
+    private final List<MenuCallback<T>> charCallbacks = new ArrayList<>();
+
+    /** The char option keys (must be single-character messages). */
+    private final List<LocalizableMessage> charKeys = new ArrayList<>();
+    /** The synopsis of char options. */
+    private final List<LocalizableMessage> charSynopsis = new ArrayList<>();
+
+    /** Optional column headings. */
+    private final List<LocalizableMessage> columnHeadings = new ArrayList<>();
+    /** Optional column widths. */
+    private final List<Integer> columnWidths = new ArrayList<>();
+
+    /** The call-back for the optional default action. */
+    private MenuCallback<T> defaultCallback;
+    /** The description of the optional default action. */
+    private LocalizableMessage defaultDescription;
+
+    /** The numeric option call-backs. */
+    private final List<MenuCallback<T>> numericCallbacks = new ArrayList<>();
+    /** The numeric option fields. */
+    private final List<List<LocalizableMessage>> numericFields = new ArrayList<>();
+
+    /** The menu title. */
+    private LocalizableMessage title;
+    /** The menu prompt. */
+    private LocalizableMessage prompt;
+
+    /**
+     * The maximum number of times that we allow the user to provide an invalid
+     * answer (-1 if unlimited).
+     */
+    private int nMaxTries = -1;
+
+    /**
+     * Creates a new menu.
+     *
+     * @param app
+     *            The application console.
+     */
+    public MenuBuilder(ConsoleApplication app) {
+        this.app = app;
+    }
+
+    /**
+     * Creates a "back" menu option. When invoked, this option will return a {@code MenuResult.cancel()} result.
+     *
+     * @param isDefault
+     *            Indicates whether this option should be made the menu default.
+     */
+    public void addBackOption(boolean isDefault) {
+        addCharOption(INFO_MENU_OPTION_BACK_KEY.get(), INFO_MENU_OPTION_BACK.get(), MenuResult.<T> cancel());
+
+        if (isDefault) {
+            setDefault(INFO_MENU_OPTION_BACK_KEY.get(), MenuResult.<T> cancel());
+        }
+    }
+
+    /**
+     * Creates a "cancel" menu option. When invoked, this option will return a {@code MenuResult.cancel()} result.
+     *
+     * @param isDefault
+     *            Indicates whether this option should be made the menu default.
+     */
+    public void addCancelOption(boolean isDefault) {
+        addCharOption(INFO_MENU_OPTION_CANCEL_KEY.get(), INFO_MENU_OPTION_CANCEL.get(), MenuResult.<T> cancel());
+
+        if (isDefault) {
+            setDefault(INFO_MENU_OPTION_CANCEL_KEY.get(), MenuResult.<T> cancel());
+        }
+    }
+
+    /**
+     * Adds a menu choice to the menu which will have a single letter as its key.
+     *
+     * @param c
+     *            The single-letter message which will be used as the key for this option.
+     * @param description
+     *            The menu option description.
+     * @param callback
+     *            The call-back associated with this option.
+     */
+    public void addCharOption(LocalizableMessage c, LocalizableMessage description, MenuCallback<T> callback) {
+        charKeys.add(c);
+        charSynopsis.add(description);
+        charCallbacks.add(callback);
+    }
+
+    /**
+     * Adds a menu choice to the menu which will have a single letter as its key and which returns the provided result.
+     *
+     * @param c
+     *            The single-letter message which will be used as the key for this option.
+     * @param description
+     *            The menu option description.
+     * @param result
+     *            The menu result which should be returned by this menu choice.
+     */
+    public void addCharOption(LocalizableMessage c, LocalizableMessage description, MenuResult<T> result) {
+        addCharOption(c, description, new ResultCallback<T>(result));
+    }
+
+    /**
+     * Creates a "help" menu option which will use the provided help call-back to display help relating to the other
+     * menu options. When the help menu option is selected help will be displayed and then the user will be shown the
+     * menu again and prompted to enter a choice.
+     *
+     * @param callback
+     *            The help call-back.
+     */
+    public void addHelpOption(final HelpCallback callback) {
+        MenuCallback<T> wrapper = new MenuCallback<T>() {
+
+            @Override
+            public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
+                app.println();
+                callback.display(app);
+                return MenuResult.again();
+            }
+
+        };
+
+        addCharOption(INFO_MENU_OPTION_HELP_KEY.get(), INFO_MENU_OPTION_HELP.get(), wrapper);
+    }
+
+    /**
+     * Adds a menu choice to the menu which will have a numeric key.
+     *
+     * @param description
+     *            The menu option description.
+     * @param callback
+     *            The call-back associated with this option.
+     * @param extraFields
+     *            Any additional fields associated with this menu option.
+     * @return Returns the number associated with menu choice.
+     */
+    public int addNumberedOption(LocalizableMessage description, MenuCallback<T> callback,
+            LocalizableMessage... extraFields) {
+        List<LocalizableMessage> fields = new ArrayList<>();
+        fields.add(description);
+        if (extraFields != null) {
+            fields.addAll(Arrays.asList(extraFields));
+        }
+
+        numericFields.add(fields);
+        numericCallbacks.add(callback);
+
+        return numericCallbacks.size();
+    }
+
+    /**
+     * Adds a menu choice to the menu which will have a numeric key and which returns the provided result.
+     *
+     * @param description
+     *            The menu option description.
+     * @param result
+     *            The menu result which should be returned by this menu choice.
+     * @param extraFields
+     *            Any additional fields associated with this menu option.
+     * @return Returns the number associated with menu choice.
+     */
+    public int addNumberedOption(LocalizableMessage description, MenuResult<T> result,
+            LocalizableMessage... extraFields) {
+        return addNumberedOption(description, new ResultCallback<T>(result), extraFields);
+    }
+
+    /**
+     * Creates a "quit" menu option. When invoked, this option will return a {@code MenuResult.quit()} result.
+     */
+    public void addQuitOption() {
+        addCharOption(INFO_MENU_OPTION_QUIT_KEY.get(), INFO_MENU_OPTION_QUIT.get(), MenuResult.<T> quit());
+    }
+
+    /**
+     * Sets the flag which indicates whether or not the menu will permit multiple numeric options to be selected at
+     * once. Users specify multiple choices by separating them with a comma. The default is <code>false</code>.
+     *
+     * @param allowMultiSelect
+     *            Indicates whether or not the menu will permit multiple numeric options to be selected at once.
+     */
+    public void setAllowMultiSelect(boolean allowMultiSelect) {
+        this.allowMultiSelect = allowMultiSelect;
+    }
+
+    /**
+     * Sets the optional column headings. The column headings will be displayed above the menu options.
+     *
+     * @param headings
+     *            The optional column headings.
+     */
+    public void setColumnHeadings(LocalizableMessage... headings) {
+        this.columnHeadings.clear();
+        if (headings != null) {
+            this.columnHeadings.addAll(Arrays.asList(headings));
+        }
+    }
+
+    /**
+     * Sets the optional column widths. A value of zero indicates that the column should be expandable, a value of
+     * <code>null</code> indicates that the column should use its default width.
+     *
+     * @param widths
+     *            The optional column widths.
+     */
+    public void setColumnWidths(Integer... widths) {
+        this.columnWidths.clear();
+        if (widths != null) {
+            this.columnWidths.addAll(Arrays.asList(widths));
+        }
+    }
+
+    /**
+     * Sets the optional default action for this menu. The default action call-back will be invoked if the user does not
+     * specify an option and just presses enter.
+     *
+     * @param description
+     *            A short description of the default action.
+     * @param callback
+     *            The call-back associated with the default action.
+     */
+    public void setDefault(LocalizableMessage description, MenuCallback<T> callback) {
+        defaultCallback = callback;
+        defaultDescription = description;
+    }
+
+    /**
+     * Sets the optional default action for this menu. The default action call-back will be invoked if the user does not
+     * specify an option and just presses enter.
+     *
+     * @param description
+     *            A short description of the default action.
+     * @param result
+     *            The menu result which should be returned by default.
+     */
+    public void setDefault(LocalizableMessage description, MenuResult<T> result) {
+        setDefault(description, new ResultCallback<T>(result));
+    }
+
+    /**
+     * Sets the number of numeric options required to trigger multiple-column display. A negative value (the default)
+     * indicates that the numeric options will always be displayed in a single column. A value of 0 indicates that
+     * numeric options will always be displayed in multiple columns.
+     *
+     * @param threshold
+     *            The number of numeric options required to trigger multiple-column display.
+     */
+    public void setMultipleColumnThreshold(int threshold) {
+        this.threshold = threshold;
+    }
+
+    /**
+     * Sets the optional menu prompt. The prompt will be displayed above the menu. Menus do not have a prompt by
+     * default.
+     *
+     * @param prompt
+     *            The menu prompt, or <code>null</code> if there is not prompt.
+     */
+    public void setPrompt(LocalizableMessage prompt) {
+        this.prompt = prompt;
+    }
+
+    /**
+     * Sets the optional menu title. The title will be displayed above the menu prompt. Menus do not have a title by
+     * default.
+     *
+     * @param title
+     *            The menu title, or <code>null</code> if there is not title.
+     */
+    public void setTitle(LocalizableMessage title) {
+        this.title = title;
+    }
+
+    /**
+     * Creates a menu from this menu builder.
+     *
+     * @return Returns the new menu.
+     */
+    public Menu<T> toMenu() {
+        TableBuilder nbuilder = new TableBuilder();
+        Map<String, MenuCallback<T>> callbacks = new HashMap<>();
+
+        // Determine whether multiple columns should be used for numeric options
+        boolean useMultipleColumns = threshold >= 0 && numericCallbacks.size() >= threshold;
+
+        // Create optional column headers.
+        if (!columnHeadings.isEmpty()) {
+            appendHeadings(nbuilder);
+
+            if (useMultipleColumns) {
+                appendHeadings(nbuilder);
+            }
+        }
+
+        // Add the numeric options first.
+        int sz = numericCallbacks.size();
+        int rows = sz;
+
+        if (useMultipleColumns) {
+            // Display in two columns the first column should contain half
+            // the options. If there are an odd number of columns then the
+            // first column should contain an additional option (e.g. if
+            // there are 23 options, the first column should contain 12
+            // options and the second column 11 options).
+            rows /= 2;
+            rows += sz % 2;
+        }
+
+        for (int i = 0, j = rows; i < rows; i++, j++) {
+            nbuilder.startRow();
+            appendCells(nbuilder, i);
+
+            callbacks.put(String.valueOf(i + 1), numericCallbacks.get(i));
+
+            // Second column.
+            if (useMultipleColumns && j < sz) {
+                appendCells(nbuilder, j);
+
+                callbacks.put(String.valueOf(j + 1), numericCallbacks.get(j));
+            }
+        }
+
+        // Add the char options last.
+        TableBuilder cbuilder = new TableBuilder();
+        for (int i = 0; i < charCallbacks.size(); i++) {
+            char c = charKeys.get(i).charAt(0);
+            LocalizableMessage option = INFO_MENU_CHAR_OPTION.get(c);
+
+            cbuilder.startRow();
+            cbuilder.appendCell(option);
+            cbuilder.appendCell(charSynopsis.get(i));
+
+            callbacks.put(String.valueOf(c), charCallbacks.get(i));
+        }
+
+        // Configure the table printer.
+        TextTablePrinter printer = new TextTablePrinter(app.getErrorStream());
+
+        boolean hasHeadings = !columnHeadings.isEmpty();
+        printer.setDisplayHeadings(hasHeadings);
+        if (hasHeadings) {
+            printer.setHeadingSeparatorStartColumn(1);
+        }
+
+        printer.setIndentWidth(4);
+        if (columnWidths.isEmpty()) {
+            printer.setColumnWidth(1, 0);
+            if (useMultipleColumns) {
+                printer.setColumnWidth(3, 0);
+            }
+        } else {
+            for (int i = 0; i < columnWidths.size(); i++) {
+                Integer j = columnWidths.get(i);
+                if (j != null) {
+                    // Skip the option key column.
+                    printer.setColumnWidth(i + 1, j);
+
+                    if (useMultipleColumns) {
+                        printer.setColumnWidth(i + 2 + columnWidths.size(), j);
+                    }
+                }
+            }
+        }
+
+        return new MenuImpl<>(app, title, prompt, nbuilder, cbuilder, printer, callbacks, allowMultiSelect,
+                defaultCallback, defaultDescription, nMaxTries);
+    }
+
+    private void appendCells(TableBuilder nbuilder, int i) {
+        nbuilder.appendCell(INFO_MENU_NUMERIC_OPTION.get(i + 1));
+        for (LocalizableMessage field : numericFields.get(i)) {
+            if (field != null) {
+                nbuilder.appendCell(field);
+            } else {
+                nbuilder.appendCell();
+            }
+        }
+    }
+
+    private void appendHeadings(TableBuilder nbuilder) {
+        nbuilder.appendHeading();
+        for (LocalizableMessage heading : columnHeadings) {
+            if (heading != null) {
+                nbuilder.appendHeading(heading);
+            } else {
+                nbuilder.appendHeading();
+            }
+        }
+    }
+
+    /**
+     * Sets the maximum number of tries that the user can provide an invalid value in the menu. -1 for unlimited tries
+     * (the default). If this limit is reached a ClientException will be thrown.
+     *
+     * @param nTries
+     *            the maximum number of tries.
+     */
+    public void setMaxTries(int nTries) {
+        nMaxTries = nTries;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java
new file mode 100644
index 0000000..ba46c5d
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuCallback.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+
+/**
+ * A menu call-back which should be associated with each menu option.
+ * When an option is selected the call-back is invoked.
+ *
+ * @param <T>
+ *          The type of success result value(s) returned by the
+ *          call-back. Use <code>Void</code> if the call-backs do
+ *          not return any values.
+ */
+public interface MenuCallback<T> {
+
+   /**
+    * Invoke the menu call-back.
+    *
+    * @param app
+    *          The application console.
+    * @return Returns the result of invoking the menu call-back.
+    * @throws ClientException
+    *           If the menu call-back fails for some reason.
+    */
+    MenuResult<T> invoke(ConsoleApplication app) throws ClientException;
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java
new file mode 100644
index 0000000..97efc14
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MenuResult.java
@@ -0,0 +1,253 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * The result of running a {@link Menu}. The result indicates to the
+ * application how it should proceed:
+ * <ul>
+ * <li>{@link #again()} - the menu should be displayed again. A good
+ * example of this is when a user chooses to view some help. Normally,
+ * after the help is displayed, the user is allowed to select another
+ * option
+ * <li>{@link #cancel()} - the user chose to cancel any task
+ * currently in progress and go back to the previous main menu if
+ * applicable
+ * <li>{@link #success()} - the user chose to apply any task
+ * currently in progress and go back to the previous menu if
+ * applicable. Any result values applicable to the chosen option can
+ * be retrieved using {@link #getValue()} or {@link #getValues()}
+ * <li>{@link #quit()} - the user chose to quit the application and
+ * cancel all outstanding tasks.
+ * </ul>
+ *
+ * @param <T>
+ *            The type of result value(s) contained in success results. Use <code>Void</code> if success results should
+ *            not contain values.
+ */
+public final class MenuResult<T> {
+
+    /** The type of result returned from the menu. */
+    private static enum Type {
+        /**
+         * The user selected an option which did not return a result,
+         * so the menu should be displayed again.
+         */
+        AGAIN,
+
+        /**
+         * The user did not select an option and instead
+         * chose to cancel the current task.
+         */
+        CANCEL,
+
+        /**
+         * The user did not select an option and instead
+         * chose to quit the entire application.
+         */
+        QUIT,
+
+        /**
+         * The user selected an option which succeeded
+         * and returned one or more result values.
+         */
+        SUCCESS
+    }
+
+    /**
+     * Creates a new menu result indicating that the menu should be displayed again. A good example of this is when a
+     * user chooses to view some help. Normally, after the help is displayed, the user is allowed to select another
+     * option.
+     *
+     * @param <T>
+     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
+     *            should not contain values.
+     * @return Returns a new menu result indicating that the menu should be displayed again.
+     */
+    public static <T> MenuResult<T> again() {
+        return new MenuResult<>(Type.AGAIN, Collections.<T> emptyList());
+    }
+
+    /**
+     * Creates a new menu result indicating that the user chose to cancel any task currently in progress and go back to
+     * the previous main menu if applicable.
+     *
+     * @param <T>
+     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
+     *            should not contain values.
+     * @return Returns a new menu result indicating that the user chose to cancel any task currently in progress and go
+     *         back to the previous main menu if applicable.
+     */
+    public static <T> MenuResult<T> cancel() {
+        return new MenuResult<>(Type.CANCEL, Collections.<T> emptyList());
+    }
+
+    /**
+     * Creates a new menu result indicating that the user chose to quit the application and cancel all outstanding
+     * tasks.
+     *
+     * @param <T>
+     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
+     *            should not contain values.
+     * @return Returns a new menu result indicating that the user chose to quit the application and cancel all
+     *         outstanding tasks.
+     */
+    public static <T> MenuResult<T> quit() {
+        return new MenuResult<>(Type.QUIT, Collections.<T> emptyList());
+    }
+
+    /**
+     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
+     * the previous menu if applicable. The menu result will not contain any result values.
+     *
+     * @param <T>
+     *            The type of result value(s) contained in success results. Use <code>Void</code> if success results
+     *            should not contain values.
+     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
+     *         back to the previous menu if applicable.The menu result will not contain any result values.
+     */
+    public static <T> MenuResult<T> success() {
+        return success(Collections.<T> emptySet());
+    }
+
+    /**
+     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
+     * the previous menu if applicable. The menu result will contain the provided values, which can be retrieved using
+     * {@link #getValue()} or {@link #getValues()}.
+     *
+     * @param <T>
+     *            The type of the result values.
+     * @param values
+     *            The result values.
+     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
+     *         back to the previous menu if applicable. The menu result will contain the provided values, which can be
+     *         retrieved using {@link #getValue()} or {@link #getValues()}.
+     */
+    public static <T> MenuResult<T> success(Collection<T> values) {
+        return new MenuResult<>(Type.SUCCESS, new ArrayList<>(values));
+    }
+
+    /**
+     * Creates a new menu result indicating that the user chose to apply any task currently in progress and go back to
+     * the previous menu if applicable. The menu result will contain the provided value, which can be retrieved using
+     * {@link #getValue()} or {@link #getValues()}.
+     *
+     * @param <T>
+     *            The type of the result value.
+     * @param value
+     *            The result value.
+     * @return Returns a new menu result indicating that the user chose to apply any task currently in progress and go
+     *         back to the previous menu if applicable. The menu result will contain the provided value, which can be
+     *         retrieved using {@link #getValue()} or {@link #getValues()}.
+     */
+    public static <T> MenuResult<T> success(T value) {
+        return success(Collections.singleton(value));
+    }
+
+    /** The type of result returned from the menu. */
+    private final Type type;
+
+    /** The menu result value(s). */
+    private final Collection<T> values;
+
+    /** Private constructor. */
+    private MenuResult(Type type, Collection<T> values) {
+        this.type = type;
+        this.values = values;
+    }
+
+    /**
+     * Gets the menu result value if this is a menu result indicating success.
+     *
+     * @return Returns the menu result value, or <code>null</code> if there was no result value or if this is not a
+     *         success menu result.
+     * @see #isSuccess()
+     */
+    public T getValue() {
+        if (!values.isEmpty()) {
+            return values.iterator().next();
+        }
+        return null;
+    }
+
+    /**
+     * Gets the menu result values if this is a menu result indicating success.
+     *
+     * @return Returns the menu result values, which may be empty if there were no result values or if this is not a
+     *         success menu result.
+     * @see #isSuccess()
+     */
+    public Collection<T> getValues() {
+        return new ArrayList<>(values);
+    }
+
+    /**
+     * Determines if this menu result indicates that the menu should be displayed again. A good example of this is when
+     * a user chooses to view some help. Normally, after the help is displayed, the user is allowed to select another
+     * option.
+     *
+     * @return Returns <code>true</code> if this menu result indicates that the menu should be displayed again.
+     */
+    public boolean isAgain() {
+        return type == Type.AGAIN;
+    }
+
+    /**
+     * Determines if this menu result indicates that the user chose to cancel any task currently in progress and go back
+     * to the previous main menu if applicable.
+     *
+     * @return Returns <code>true</code> if this menu result indicates that the user chose to cancel any task currently
+     *         in progress and go back to the previous main menu if applicable.
+     */
+    public boolean isCancel() {
+        return type == Type.CANCEL;
+    }
+
+    /**
+     * Determines if this menu result indicates that the user chose to quit the application and cancel all outstanding
+     * tasks.
+     *
+     * @return Returns <code>true</code> if this menu result indicates that the user chose to quit the application and
+     *         cancel all outstanding tasks.
+     */
+    public boolean isQuit() {
+        return type == Type.QUIT;
+    }
+
+    /**
+     * Determines if this menu result indicates that the user chose to apply any task currently in progress and go back
+     * to the previous menu if applicable. Any result values can be retrieved using the {@link #getValue()} or
+     * {@link #getValues()} methods.
+     *
+     * @return Returns <code>true</code> if this menu result indicates that the user chose to apply any task currently
+     *         in progress and go back to the previous menu if applicable.
+     * @see #getValue()
+     * @see #getValues()
+     */
+    public boolean isSuccess() {
+        return type == Type.SUCCESS;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(type=" + type + ", values=" + values + ")";
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiChoiceArgument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiChoiceArgument.java
new file mode 100644
index 0000000..7dd713c
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiChoiceArgument.java
@@ -0,0 +1,150 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.ERR_MCARG_VALUE_NOT_ALLOWED;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+/**
+ * This class defines an argument type that will only accept one or more of a
+ * specific set of string values.
+ *
+ * @param <V>
+ *            The type of values returned by this argument.
+ */
+public final class MultiChoiceArgument<V> extends Argument {
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * {@link MultiChoiceArgument<V>}.
+     *
+     * @param <V>
+     *         The type of values returned by this argument.
+     * @param longIdentifier
+     *         The generic long identifier that will be used to refer to this argument.
+     * @return A builder to continue building the {@link MultiChoiceArgument}.
+     */
+    public static <V> Builder<V> builder(final String longIdentifier) {
+        return new Builder<>(longIdentifier);
+    }
+
+    /** A fluent API for incrementally constructing {@link MultiChoiceArgument<V>}. */
+    public static final class Builder<V> extends ArgumentBuilder<Builder<V>, V, MultiChoiceArgument<V>> {
+        private final List<V> allowedValues = new LinkedList<>();
+
+        private Builder(final String longIdentifier) {
+            super(longIdentifier);
+        }
+
+        @Override
+        Builder<V> getThis() {
+            return this;
+        }
+
+        /**
+         * Specifies the set of values that are allowed for the {@link MultiChoiceArgument<V>}.
+         *
+         * @param allowedValues
+         *         The {@link MultiChoiceArgument<V>} allowed values.
+         * @return This builder.
+         */
+        public Builder<V> allowedValues(final Collection<V> allowedValues) {
+            this.allowedValues.addAll(allowedValues);
+            return getThis();
+        }
+
+        /**
+         * Specifies the set of values that are allowed for the {@link MultiChoiceArgument<V>}.
+         *
+         * @param allowedValues
+         *         The {@link MultiChoiceArgument<V>} allowed values.
+         * @return This builder.
+         */
+        @SuppressWarnings("unchecked")
+        public final Builder<V> allowedValues(final V... allowedValues) {
+            this.allowedValues.addAll(Arrays.asList(allowedValues));
+            return getThis();
+        }
+
+        @Override
+        public MultiChoiceArgument<V> buildArgument() throws ArgumentException {
+            return new MultiChoiceArgument<>(this, allowedValues);
+        }
+    }
+
+    /** The set of values that will be allowed for use with this argument. */
+    private final Collection<V> allowedValues;
+
+    private <V1> MultiChoiceArgument(final Builder<V1> builder, final Collection<V> allowedValues)
+            throws ArgumentException {
+        super(builder);
+        this.allowedValues = allowedValues;
+    }
+
+    /**
+     * Retrieves the string value for this argument. If it has multiple values,
+     * then the first will be returned. If it does not have any values, then the
+     * default value will be returned.
+     *
+     * @return The string value for this argument, or <CODE>null</CODE> if there
+     *         are no values and no default value has been given.
+     * @throws ArgumentException
+     *             The value cannot be parsed.
+     */
+    public V getTypedValue() throws ArgumentException {
+        final String v = super.getValue();
+        if (v == null) {
+            return null;
+        }
+        for (final V allowedValue : allowedValues) {
+            if (allowedValue.toString().equalsIgnoreCase(v)) {
+                return allowedValue;
+            }
+        }
+        throw new IllegalStateException("This MultiChoiceArgument value is not part of the allowed values.");
+    }
+
+    /**
+     * Indicates whether the provided value is acceptable for use in this
+     * argument.
+     *
+     * @param valueString
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            A buffer into which the invalid reason may be written if the
+     *            value is not acceptable.
+     * @return <CODE>true</CODE> if the value is acceptable, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    @Override
+    public boolean valueIsAcceptable(final String valueString, final LocalizableMessageBuilder invalidReason) {
+        for (final V allowedValue : allowedValues) {
+            if (allowedValue.toString().equalsIgnoreCase(valueString)) {
+                return true;
+            }
+        }
+        invalidReason.append(ERR_MCARG_VALUE_NOT_ALLOWED.get(longIdentifier, valueString));
+
+        return false;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiColumnPrinter.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiColumnPrinter.java
new file mode 100644
index 0000000..a1ece9a
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiColumnPrinter.java
@@ -0,0 +1,519 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.Utils.repeat;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Utility class for printing columns of data.
+ * <p>
+ * This printer can be used to print data in formatted table or in csv format.
+ * <p>
+ * Regarding the formatting table feature, this class allows you to specify for each {@link Column}s:
+ * <ul>
+ *     <li>A unique identifier</li>
+ *     <li>The column title which will be printed by a call to {@link MultiColumnPrinter#printTitleLine()}</li>
+ *     <li>The size (if a cell's data is bigger than the predefined size, then the data will not be truncated,
+ *         i.e. it will overflow)</li>
+ *     <li>The number of digits to keep (for {@link Double} data)</li>
+ * </ul>
+ * <p>
+ * Code to write data is independent of the {@link MultiColumnPrinter} configuration:
+ * <pre>
+ * void printData(final MultiColumnPrinter printer) {
+ *     String[][] myData = new String[][] {
+ *         new String[]{"U.S.A", "34.2", "40.8", ".us"},
+ *         new String[]{"United Kingdom", "261.1", "31.6", ".uk"},
+ *         new String[]{"France", "98.8", "30.1", ".fr"}
+ *     };
+ *
+ *     int i;
+ *     for (String[] countryData : myData) {
+ *         i = 0;
+ *         for (final MultiColumnPrinter.Column column : printer.getColumns()) {
+ *             printer.printData(countryData[i++]);
+ *         }
+ *     }
+ *  }
+ * </pre>
+ * <p>
+ * The following code sample presents how to create a {@link MultiColumnPrinter} to write CSV data:
+ * <pre>
+ * final List<MultiColumnPrinter.Column> columns = new ArrayList&lt;&gt;();
+ * columns.add(MultiColumnPrinter.column("CountryNameColumnId", "country_name", 0));
+ * columns.add(MultiColumnPrinter.column("populationDensityId", "population_density", 1));
+ * columns.add(MultiColumnPrinter.column("GiniId", "gini", 1));
+ * columns.add(MultiColumnPrinter.column("internetTLDId", "internet_tld", 0));
+ * MultiColumnPrinter myCsvPrinter = MultiColumnPrinter.builder(System.out, columns)
+ *                                                     .columnSeparator(",")
+ *                                                     .build();
+ * printData(myCsvPrinter);
+ * </pre>
+ * <p>
+ * The code above would print:
+ * <pre>
+ * country_name,population_density,gini,internet_tld
+ * U.S.A,34.2,40.8,.us
+ * United Kingdom,261.1,31.6,.uk
+ * France,98.8,30.1,.fr
+ * </pre>
+ * <p>
+ * The following code sample presents how to configure a {@link MultiColumnPrinter}
+ * to print the same data on console with some title headers.
+ * <pre>
+ *     final List<MultiColumnPrinter.Column> columns = new ArrayList&lt;&gt;();
+ *     columns.add(MultiColumnPrinter.separatorColumn());
+ *     columns.add(MultiColumnPrinter.column("CountryNameColumnId", "Country Name", 15, 0));
+ *     columns.add(MultiColumnPrinter.column("populationDensityId", "Density", 10, 1));
+ *     columns.add(MultiColumnPrinter.separatorColumn());
+ *     columns.add(MultiColumnPrinter.column("GiniID", "GINI", 5, 1));
+ *     columns.add(MultiColumnPrinter.column("internetTLDID", "TLD", 5, 0));
+ *     columns.add(MultiColumnPrinter.separatorColumn());
+ *     MultiColumnPrinter myPrinter = MultiColumnPrinter.builder(System.out, columns)
+ *                                                      .format(true)
+ *                                                      .columnSeparator("  ")
+ *                                                      .titleAlignment(MultiColumnPrinter.Alignment.CENTER)
+ *                                                      .build();
+ *     myPrinter.printDashedLine();
+ *     myPrinter.printTitleSection("General Information", 2);
+ *     myPrinter.printTitleSection("Data", 2);
+ *     myPrinter.printTitleLine();
+ *     myPrinter.printDashedLine();
+ *     printData(myPrinter);
+ *     myPrinter.printDashedLine();
+ * </pre>
+ * <p>
+ * The code above would print:
+ * <pre>
+ * --------------------------------------------------
+ * |       General Information     |       Data     |
+ * |     Country Name     Density  |   GINI    TLD  |
+ * --------------------------------------------------
+ * |            U.S.A        34.2  |   40.8    .us  |
+ * |   United Kingdom       261.1  |   31.6    .uk  |
+ * |           France        98.8  |   30.1    .fr  |
+ * --------------------------------------------------
+ * </pre>
+ */
+public final class MultiColumnPrinter {
+
+    /** The data alignment. */
+    public enum Alignment {
+        /** Data will be left-aligned. */
+        LEFT,
+        /** Data will be centered. */
+        CENTER,
+        /** Data will be right-aligned. */
+        RIGHT
+    }
+
+    private static final String SEPARATOR_ID = "separator";
+    private static int separatorIdNumber;
+
+    /**
+     * Returns a new separator {@link Column}.
+     * <p>
+     * This kind of {@link Column} can be used to separate data sections.
+     *
+     * @return A new separator {@link Column}.
+     */
+    public static Column separatorColumn() {
+        return new Column(SEPARATOR_ID + separatorIdNumber++, "", 1, 0);
+    }
+
+    /**
+     * Creates a new {@link Column} with the provided arguments.
+     *
+     * @param id
+     *      The column identifier.
+     * @param title
+     *      The column title.
+     * @param doublePrecision
+     *      The double precision used to print {@link Double} data for this column.
+     *      See {@link MultiColumnPrinter#printData(Double)}.
+     * @return
+     *      A new Column with the provided arguments.
+     */
+    public static Column column(final String id, final String title, final int doublePrecision) {
+        return new Column(id, title, 1, doublePrecision);
+    }
+
+    /**
+     * Creates a new Column with the provided arguments.
+     *
+     * @param id
+     *      The column identifier.
+     * @param title
+     *      The column title.
+     * @param width
+     *      The column width.
+     *      This information will only be used if the associated
+     *      {@link MultiColumnPrinter} is configured to apply formatting.
+     *      See {@link Builder#format(boolean)}.
+     * @param doublePrecision
+     *      The double precision to use to print data for this column.
+     * @return
+     *      A new Column with the provided arguments.
+     */
+    public static Column column(final String id, final String title, final int width, final int doublePrecision) {
+        return new Column(id, title, Math.max(width, title.length()), doublePrecision);
+    }
+
+    /**
+     * This class describes a Column of data used in the {@link MultiColumnPrinter}.
+     * <p>
+     * A column consists in the following fields:
+     * <ul>
+     *     <li>An identifier for the associated data.
+     *     <li>A title which is printed when {@link MultiColumnPrinter#printTitleLine()} is called.
+     *     <li>A width which is the max width for this column's data.
+     *         This information will only be used if the associated {@link MultiColumnPrinter}
+     *         is configure to apply formatting.See {@link Builder#format(boolean)}.
+     *     <li>A double precision which is the number of decimal to print for numeric data.
+     *         See {@link MultiColumnPrinter#printData(Double)}.
+     * </ul>
+     */
+    public static final class Column {
+        private final String id;
+        private final String title;
+        private final int width;
+        private final int doublePrecision;
+
+        private Column(final String id, final String title, final int width, final int doublePrecision) {
+            this.id = id;
+            this.title = title;
+            this.width = Math.max(width, title.length());
+            this.doublePrecision = doublePrecision;
+        }
+
+        /**
+         * Returns this {@link Column} identifier.
+         *
+         * @return This {@link Column} identifier.
+         */
+        public String getId() {
+            return id;
+        }
+    }
+
+    /**
+     * Creates a new {@link Builder} to build a {@link MultiColumnPrinter}.
+     *
+     * @param stream
+     *      The {@link PrintStream} to use to print data.
+     * @param columns
+     *      The {@link List} of {@link Column} data to print.
+     * @return
+     *      A new {@link Builder} to build a {@link MultiColumnPrinter}.
+     */
+    public static Builder builder(final PrintStream stream, final List<Column> columns) {
+        return new Builder(stream, columns);
+    }
+
+    /** A fluent API for incrementally constructing {@link MultiColumnPrinter}. */
+    public static final class Builder {
+        private final PrintStream stream;
+        private final List<Column> columns;
+
+        private Alignment titleAlignment = Alignment.RIGHT;
+        private String columnSeparator = " ";
+        private boolean format;
+
+        private Builder(final PrintStream stream, final List<Column> columns) {
+            Reject.ifNull(stream);
+            this.stream = stream;
+            this.columns = columns;
+        }
+
+        /**
+         * Sets whether the {@link MultiColumnPrinter} needs to apply formatting.
+         * <br>
+         * Default value is {@code false}.
+         *
+         * @param format
+         *      {@code true} if the {@link MultiColumnPrinter} needs to apply formatting.
+         * @return This builder.
+         */
+        public Builder format(final boolean format) {
+            this.format = format;
+            return this;
+        }
+
+        /**
+         * Sets the alignment for title elements which will be printed by the {@link MultiColumnPrinter}.
+         * <p>
+         * This is used only if the printer is configured to
+         * apply formatting, see {@link Builder#format(boolean)}.
+         * <br>
+         * Default value is {@link Alignment#RIGHT}.
+         *
+         * @param titleAlignment
+         *      The title alignment.
+         * @return This builder.
+         */
+        public Builder titleAlignment(final Alignment titleAlignment) {
+            this.titleAlignment = titleAlignment;
+            return this;
+        }
+
+        /**
+         * Sets the sequence to use to separate column.
+         * <p>
+         * Default value is {@code " "}.
+         *
+         * @param separator
+         *      The sequence {@link String}.
+         * @return This builder.
+         */
+        public Builder columnSeparator(final String separator) {
+            this.columnSeparator = separator;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link MultiColumnPrinter} as configured in this {@link Builder}.
+         *
+         * @return A new {@link MultiColumnPrinter} as configured in this {@link Builder}.
+         */
+        public MultiColumnPrinter build() {
+            return new MultiColumnPrinter(this);
+        }
+    }
+
+    private final PrintStream stream;
+    private final List<Column> columns;
+    private final boolean format;
+    private final Alignment titleAlignment;
+    private final String columnSeparator;
+
+    private List<Column> printableColumns;
+    private final int lineLength;
+    private Iterator<Column> columnIterator;
+    private Column currentColumn;
+
+    private MultiColumnPrinter(final Builder builder) {
+        this.stream = builder.stream;
+        this.columns = Collections.unmodifiableList(builder.columns);
+        this.format = builder.format;
+        this.columnSeparator = builder.columnSeparator;
+        this.titleAlignment = builder.titleAlignment;
+        this.lineLength = computeLineLength();
+        resetIterator();
+        computePrintableColumns();
+    }
+
+    /** Prints a dashed line. */
+    public void printDashedLine() {
+        startNewLineIfNeeded();
+        for (int i = 0; i < lineLength; i++) {
+            stream.print('-');
+        }
+        stream.println();
+    }
+
+    /**
+     * Formats and prints the provided text data.
+     * Merge the provided text over the provided number of column.
+     * <p>
+     * Separator columns between merged columns will not be printed.
+     *
+     * @param data
+     *      The section title to print.
+     * @param rowSpan
+     *      Specifies the number of rows a cell should span.
+     */
+    public void printTitleSection(final String data, final int rowSpan) {
+        consumeSeparatorColumn();
+        int lengthToPad = 0;
+        int nbColumnMerged = 0;
+
+        while (columnIterator.hasNext() && nbColumnMerged < rowSpan) {
+            lengthToPad += currentColumn.width + columnSeparator.length();
+            if (!isSeparatorColumn(currentColumn)) {
+                nbColumnMerged++;
+            }
+            currentColumn = columnIterator.next();
+        }
+        stream.print(align(data, titleAlignment, lengthToPad));
+        consumeSeparatorColumn();
+        if (!columnIterator.hasNext()) {
+            nextLine();
+        }
+    }
+
+    /** Prints a line with all column title and separator. */
+    public void printTitleLine() {
+        startNewLineIfNeeded();
+        passFirstSeparatorColumn();
+        for (final Column column : this.printableColumns) {
+            printCell(column.title, Alignment.RIGHT);
+        }
+    }
+
+    /**
+     * Prints the provided {@link Double} value on the current column.
+     * <p>
+     * If this {@link MultiColumnPrinter} is configured to apply formatting,
+     * the provided value will be truncated according to the decimal
+     * precision set in the corresponding {@link Column}.
+     * <br>
+     * See {@link MultiColumnPrinter#column(String, String, int, int)} for more details.
+     *
+     * @param value
+     *      The double value to print.
+     */
+    public void printData(final Double value) {
+        passFirstSeparatorColumn();
+        printData(value.isNaN() ? "-"
+                                : String.format(Locale.ENGLISH, "%." + currentColumn.doublePrecision + "f", value));
+    }
+
+    /**
+     * Prints the provided text data on the current column.
+     *
+     * @param data
+     *      The text data to print.
+     */
+    public void printData(final String data) {
+        passFirstSeparatorColumn();
+        printCell(data, Alignment.RIGHT);
+    }
+
+    /**
+     * Returns the data {@link Column} list of this {@link MultiColumnPrinter}.
+     * <p>
+     * Separator columns are filtered out.
+     *
+     * @return The {@link Column} list of this {@link MultiColumnPrinter}.
+     */
+    public List<Column> getColumns() {
+        return printableColumns;
+    }
+
+    private void printCell(final String data, final Alignment alignment) {
+        String toPrint = format ? align(data, alignment, currentColumn.width) : data;
+        if (columnIterator.hasNext()) {
+            toPrint += columnSeparator;
+        }
+        stream.print(toPrint);
+        nextLineOnEOLOrNextColumn();
+    }
+
+    /** Provided the provided string data according to the provided width and the provided alignment. */
+    private String align(final String data, final Alignment alignment, final int width) {
+        final String rawData = data.trim();
+        final int padding = width - rawData.length();
+
+        if (padding <= 0) {
+            return rawData;
+        }
+
+        switch (alignment) {
+        case RIGHT:
+            return pad(padding, rawData, 0);
+        case LEFT:
+            return pad(0, rawData, padding);
+        case CENTER:
+            final int paddingBefore = padding / 2;
+            return pad(paddingBefore, rawData, padding - paddingBefore);
+        default:
+            return "";
+        }
+    }
+
+    private String pad(final int leftPad, final String s, final int rightPad) {
+        return new StringBuilder().append(repeat(' ', leftPad))
+                                  .append(s)
+                                  .append(repeat(' ', rightPad))
+                                  .toString();
+    }
+
+    private void passFirstSeparatorColumn() {
+        if (cursorOnLineStart()) {
+            consumeSeparatorColumn();
+        }
+    }
+
+    private void consumeSeparatorColumn() {
+        if (isSeparatorColumn(currentColumn)) {
+            stream.print('|' + columnSeparator);
+            nextLineOnEOLOrNextColumn();
+        }
+    }
+
+    private void startNewLineIfNeeded() {
+        if (!cursorOnLineStart()) {
+            nextLine();
+        }
+    }
+
+    private void nextLineOnEOLOrNextColumn() {
+        if (columnIterator.hasNext()) {
+            currentColumn = columnIterator.next();
+            consumeSeparatorColumn();
+        } else {
+            nextLine();
+        }
+    }
+
+    private void nextLine() {
+        stream.println();
+        resetIterator();
+    }
+
+    private void resetIterator() {
+        columnIterator = columns.iterator();
+        currentColumn = columnIterator.next();
+    }
+
+    private boolean cursorOnLineStart() {
+        return currentColumn == columns.get(0);
+    }
+
+    private boolean isSeparatorColumn(final Column column) {
+        return column.id.startsWith(SEPARATOR_ID);
+    }
+
+    private void computePrintableColumns() {
+        printableColumns = new ArrayList<>(columns);
+        final Iterator<Column> it = printableColumns.iterator();
+
+        while (it.hasNext()) {
+            if (isSeparatorColumn(it.next())) {
+                it.remove();
+            }
+        }
+    }
+
+    private int computeLineLength() {
+        int lineLength = 0;
+        final int separatorLength = this.columnSeparator.length();
+        for (final Column column : this.columns) {
+            lineLength += column.width + separatorLength;
+        }
+        return lineLength - separatorLength;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/PromptingTrustManager.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/PromptingTrustManager.java
new file mode 100644
index 0000000..217e849
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/PromptingTrustManager.java
@@ -0,0 +1,388 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.util.Reject;
+
+/**
+ * A trust manager which prompts the user for the length of time that they would
+ * like to trust a server certificate.
+ */
+public final class PromptingTrustManager implements X509TrustManager {
+    /** Enumeration description server certificate trust option. */
+    private static enum TrustOption {
+        UNTRUSTED(1, INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_NO.get()),
+        SESSION(2, INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_SESSION.get()),
+        PERMANENT(3, INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_ALWAYS.get()),
+        CERTIFICATE_DETAILS(4, INFO_LDAP_CONN_PROMPT_SECURITY_CERTIFICATE_DETAILS.get());
+
+        private Integer choice;
+        private LocalizableMessage msg;
+
+        /**
+         * Private constructor.
+         *
+         * @param i
+         *            the menu return value.
+         * @param msg
+         *            the message message.
+         */
+        private TrustOption(final int i, final LocalizableMessage msg) {
+            choice = i;
+            this.msg = msg;
+        }
+
+        /**
+         * Returns the choice number.
+         *
+         * @return the attribute name.
+         */
+        Integer getChoice() {
+            return choice;
+        }
+
+        /**
+         * Return the menu message.
+         *
+         * @return the menu message.
+         */
+        LocalizableMessage getMenuMessage() {
+            return msg;
+        }
+    }
+
+    private static final LocalizedLogger LOG = LocalizedLogger.getLoggerForThisClass();
+
+    private static final String DEFAULT_PATH = System.getProperty("user.home") + File.separator
+            + ".opendj" + File.separator + "keystore";
+
+    private static final char[] DEFAULT_PASSWORD = "OpenDJ".toCharArray();
+
+    private final KeyStore inMemoryTrustStore;
+
+    private final KeyStore onDiskTrustStore;
+
+    private final X509TrustManager inMemoryTrustManager;
+
+    private final X509TrustManager onDiskTrustManager;
+
+    private final X509TrustManager nestedTrustManager;
+
+    private final ConsoleApplication app;
+
+    /**
+     * Creates a prompting trust manager based on these arguments.
+     *
+     * @param app
+     *            The linked console application.
+     * @param acceptedStorePath
+     *            The store path.
+     * @param sourceTrustManager
+     *            The source of the trust manager.
+     * @throws KeyStoreException
+     *             If no Provider supports a KeyStoreSpi implementation for the specified type.
+     * @throws IOException
+     *             If there is an I/O or format problem with the keystore data, if a password is required but not given,
+     *             or if the given password was incorrect. If the error is due to a wrong password, the cause of the
+     *             IOException should be an UnrecoverableKeyException.
+     * @throws NoSuchAlgorithmException
+     *             If no provider supports a trust manager factory spi implementation for the specified algorithm.
+     * @throws CertificateException
+     *             If any of the certificates in the key store could not be loaded
+     */
+    public PromptingTrustManager(final ConsoleApplication app, final String acceptedStorePath,
+            final X509TrustManager sourceTrustManager) throws KeyStoreException, IOException,
+            NoSuchAlgorithmException, CertificateException {
+        Reject.ifNull(app, acceptedStorePath);
+        this.app = app;
+        this.nestedTrustManager = sourceTrustManager;
+        inMemoryTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        onDiskTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+
+        final File onDiskTrustStorePath = new File(acceptedStorePath);
+        inMemoryTrustStore.load(null, null);
+        if (!onDiskTrustStorePath.exists()) {
+            onDiskTrustStore.load(null, null);
+        } else {
+            try (final FileInputStream fos = new FileInputStream(onDiskTrustStorePath)) {
+                onDiskTrustStore.load(fos, DEFAULT_PASSWORD);
+            }
+        }
+        final TrustManagerFactory tmf =
+                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+        tmf.init(inMemoryTrustStore);
+        X509TrustManager x509tm = null;
+        for (final TrustManager tm : tmf.getTrustManagers()) {
+            if (tm instanceof X509TrustManager) {
+                x509tm = (X509TrustManager) tm;
+                break;
+            }
+        }
+        if (x509tm == null) {
+            throw new NoSuchAlgorithmException();
+        }
+        this.inMemoryTrustManager = x509tm;
+
+        tmf.init(onDiskTrustStore);
+        x509tm = null;
+        for (final TrustManager tm : tmf.getTrustManagers()) {
+            if (tm instanceof X509TrustManager) {
+                x509tm = (X509TrustManager) tm;
+                break;
+            }
+        }
+        if (x509tm == null) {
+            throw new NoSuchAlgorithmException();
+        }
+        this.onDiskTrustManager = x509tm;
+    }
+
+    /**
+     * Creates a prompting trust manager based on these arguments.
+     *
+     * @param app
+     *            The linked console application.
+     * @param sourceTrustManager
+     *            The source of the trust manager.
+     * @throws KeyStoreException
+     *             If no Provider supports a KeyStoreSpi implementation for the specified type.
+     * @throws IOException
+     *             If there is an I/O or format problem with the keystore data, if a password is required but not given,
+     *             or if the given password was incorrect. If the error is due to a wrong password, the cause of the
+     *             IOException should be an UnrecoverableKeyException.
+     * @throws NoSuchAlgorithmException
+     *             If no provider supports a trust manager factory spi implementation for the specified algorithm.
+     * @throws CertificateException
+     *             If any of the certificates in the key store could not be loaded
+     */
+    public PromptingTrustManager(final ConsoleApplication app, final X509TrustManager sourceTrustManager)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+        this(app, DEFAULT_PATH, sourceTrustManager);
+    }
+
+    @Override
+    public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s)
+            throws CertificateException {
+        try {
+            inMemoryTrustManager.checkClientTrusted(x509Certificates, s);
+        } catch (final Exception ce1) {
+            try {
+                onDiskTrustManager.checkClientTrusted(x509Certificates, s);
+            } catch (final Exception ce2) {
+                if (nestedTrustManager != null) {
+                    try {
+                        nestedTrustManager.checkClientTrusted(x509Certificates, s);
+                    } catch (final Exception ce3) {
+                        checkManuallyTrusted(x509Certificates, ce3);
+                    }
+                } else {
+                    checkManuallyTrusted(x509Certificates, ce1);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s)
+            throws CertificateException {
+        try {
+            inMemoryTrustManager.checkServerTrusted(x509Certificates, s);
+        } catch (final Exception ce1) {
+            try {
+                onDiskTrustManager.checkServerTrusted(x509Certificates, s);
+            } catch (final Exception ce2) {
+                if (nestedTrustManager != null) {
+                    try {
+                        nestedTrustManager.checkServerTrusted(x509Certificates, s);
+                    } catch (final Exception ce3) {
+                        checkManuallyTrusted(x509Certificates, ce3);
+                    }
+                } else {
+                    checkManuallyTrusted(x509Certificates, ce1);
+                }
+            }
+        }
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        if (nestedTrustManager != null) {
+            return nestedTrustManager.getAcceptedIssuers();
+        }
+        return new X509Certificate[0];
+    }
+
+    /**
+     * This method is called when the user accepted a certificate.
+     *
+     * @param chain
+     *            the certificate chain accepted by the user. certificate.
+     */
+    private void acceptCertificate(final X509Certificate[] chain, final boolean permanent) {
+        if (permanent) {
+            LOG.debug(LocalizableMessage.raw("Permanently accepting certificate chain to " + "truststore"));
+        } else {
+            LOG.debug(LocalizableMessage.raw("Accepting certificate chain for this session"));
+        }
+
+        for (final X509Certificate aChain : chain) {
+            try {
+                final String alias = aChain.getSubjectDN().getName();
+                inMemoryTrustStore.setCertificateEntry(alias, aChain);
+                if (permanent) {
+                    onDiskTrustStore.setCertificateEntry(alias, aChain);
+                }
+            } catch (final Exception e) {
+                LOG.warn(LocalizableMessage.raw("Error setting certificate to store: " + e + "\nCert: " + aChain));
+            }
+        }
+
+        if (permanent) {
+            try {
+                final File truststoreFile = new File(DEFAULT_PATH);
+                if (!truststoreFile.exists()) {
+                    createFile(truststoreFile);
+                }
+                try (final FileOutputStream fos = new FileOutputStream(truststoreFile)) {
+                    onDiskTrustStore.store(fos, DEFAULT_PASSWORD);
+                }
+            } catch (final Exception e) {
+                LOG.warn(LocalizableMessage.raw("Error saving store to disk: " + e));
+            }
+        }
+    }
+
+    /**
+     * Indicate if the certificate chain can be trusted.
+     *
+     * @param chain
+     *            The certificate chain to validate certificate.
+     */
+    private void checkManuallyTrusted(final X509Certificate[] chain, final Exception exception)
+            throws CertificateException {
+        app.println();
+        app.println(INFO_LDAP_CONN_PROMPT_SECURITY_SERVER_CERTIFICATE.get());
+        app.println();
+        for (final X509Certificate element : chain) {
+            // Certificate DN
+            app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_USER_DN.get(element
+                    .getSubjectDN().toString()));
+
+            // certificate validity
+            app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_VALIDITY.get(element
+                    .getNotBefore().toString(), element.getNotAfter().toString()));
+
+            // certificate Issuer
+            app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_ISSUER.get(element.getIssuerDN()
+                    .toString()));
+
+            app.println();
+            app.println();
+        }
+
+        app.println();
+        app.println(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION.get());
+        app.println();
+
+        final Map<String, TrustOption> menuOptions = new HashMap<>();
+        for (final TrustOption t : TrustOption.values()) {
+            menuOptions.put(t.getChoice().toString(), t);
+
+            final LocalizableMessageBuilder builder = new LocalizableMessageBuilder();
+            builder.append(t.getChoice());
+            builder.append(") ");
+            builder.append(t.getMenuMessage());
+            app.println(builder.toMessage(), 2 /* Indent options */);
+        }
+
+        final TrustOption defaultTrustMethod = TrustOption.SESSION;
+        final LocalizableMessage promptMsg = INFO_MENU_PROMPT_SINGLE.get();
+
+        while (true) {
+            app.println();
+            String choice;
+            try {
+                choice = app.readInput(promptMsg, defaultTrustMethod.getChoice().toString());
+            } catch (final ClientException e) {
+                // What can we do here?
+                throw new CertificateException(exception);
+            } finally {
+                app.println();
+            }
+
+            final TrustOption option = menuOptions.get(choice.trim());
+            if (option == null) {
+                app.println(ERR_MENU_BAD_CHOICE_SINGLE.get());
+                app.println();
+                continue;
+            }
+
+            switch (option) {
+            case UNTRUSTED:
+                if (exception instanceof CertificateException) {
+                    throw (CertificateException) exception;
+                }
+                throw new CertificateException(exception);
+            case CERTIFICATE_DETAILS:
+                for (final X509Certificate aChain : chain) {
+                    app.println();
+                    app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE.get(aChain.toString()));
+                    app.println();
+                }
+                break;
+            default: // SESSION / PERMANENT.
+                // Update the trust manager with the new certificate
+                acceptCertificate(chain, option == TrustOption.PERMANENT);
+                return;
+            }
+        }
+    }
+
+    private boolean createFile(final File f) throws IOException {
+        boolean success = false;
+        if (f != null) {
+            final File parent = f.getParentFile();
+            if (!parent.exists()) {
+                parent.mkdirs();
+            }
+            success = f.createNewFile();
+        }
+        return success;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ReturnCode.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ReturnCode.java
new file mode 100644
index 0000000..b654eee
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ReturnCode.java
@@ -0,0 +1,218 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * List of return codes used by CLIs.
+ */
+public enum ReturnCode {
+    /**
+     * Successful setup.
+     * <PRE>
+     * Code value of 0.
+     * </PRE>
+     */
+    SUCCESS(0),
+    /**
+     * Unexpected error (potential bug).
+     * <PRE>
+     * Code value of 1.
+     * </PRE>
+     */
+    ERROR_UNEXPECTED(1),
+    /**
+     * Cannot parse arguments or data provided by user is not valid.
+     * <PRE>
+     * Code value of 2.
+     * </PRE>
+     */
+    ERROR_USER_DATA(2),
+    /**
+     * The LDAP result code for operations that fail due to a protocol error.
+     */
+    PROTOCOL_ERROR(2),
+    /**
+     * Error server already installed.
+     * <PRE>
+     * Code value of 3.
+     * </PRE>
+     */
+    ERROR_SERVER_ALREADY_INSTALLED(3),
+    /**
+     * Error initializing server.
+     * <PRE>
+     * Code value of 4.
+     * </PRE>
+     */
+    ERROR_INITIALIZING_SERVER(4),
+    /**
+     * The user failed providing password (for the key store for instance).
+     * <PRE>
+     * Code value of 5.
+     * </PRE>
+     */
+    ERROR_PASSWORD_LIMIT(5),
+    /**
+     * The user cancelled the process.
+     * <PRE>
+     * Code value of 6.
+     * </PRE>
+     */
+    ERROR_USER_CANCELLED(6),
+    /**
+     * The user doesn't accept the license.
+     * <PRE>
+     * Code value of 7.
+     * </PRE>
+     */
+    ERROR_LICENSE_NOT_ACCEPTED(7),
+    /**
+     * The LDAP result code for operations that fail because the requested
+     * authentication method is not supported.
+     */
+    AUTH_METHOD_NOT_SUPPORTED(7),
+    /**
+     * Incompatible java version.
+     * <PRE>
+     * Code value of 8.
+     * </PRE>
+     */
+    JAVA_VERSION_INCOMPATIBLE(8),
+    /**
+     * Application specific error.
+     */
+    APPLICATION_ERROR(10),
+    /**
+     * The LDAP result code used for multi-stage SASL bind operations that are not
+     * yet complete.
+     */
+    SASL_BIND_IN_PROGRESS(14),
+    /**
+     * Conflicting command line arguments.
+     */
+    CONFLICTING_ARGS(18),
+    /**
+     * The LDAP result code for operations that fail because a defined constraint
+     * has been violated.
+     */
+    CONSTRAINT_VIOLATION(19),
+    /**
+     * The LDAP result code for operations that fail because a targeted entry does
+     * not exist.
+     */
+    NO_SUCH_OBJECT(32),
+    /**
+     * The LDAP result code for operations that fail because the user supplied
+     * invalid credentials for an authentication attempt.
+     */
+    INVALID_CREDENTIALS(49),
+
+    /**
+     * The LDAP result code for operations that fail because the client does not
+     * have permission to perform the requested operation.
+     */
+    INSUFFICIENT_ACCESS_RIGHTS(50),
+    /**
+     * The LDAP result code for operations that fail because the requested
+     * operation would have resulted in an entry that conflicts with one that
+     * already exists.
+     */
+    ENTRY_ALREADY_EXISTS(68),
+    /**
+     * The LDAP result code for use in cases in which none of the other defined
+     * result codes are appropriate.
+     */
+    OTHER(80),
+    /**
+     * The client-side result code that indicates that a previously-established
+     * connection to the server was lost.  This is for client-side use only and
+     * should never be transferred over protocol.
+     */
+    CLIENT_SIDE_SERVER_DOWN(81),
+    /**
+     * The client-side result code that indicates that a local error occurred that
+     * had nothing to do with interaction with the server.  This is for
+     * client-side use only and should never be transferred over protocol.
+     */
+    CLIENT_SIDE_LOCAL_ERROR(82),
+    /**
+     * The client-side result code that indicates that an error occurred while
+     * encoding a request to send to the server.  This is for client-side use only
+     * and should never be transferred over protocol.
+     */
+    CLIENT_SIDE_ENCODING_ERROR(83),
+    /**
+     * The client-side result code that indicates that an error occurred while
+     * decoding a response from the server.  This is for client-side use only and
+     * should never be transferred over protocol.
+     */
+    CLIENT_SIDE_DECODING_ERROR(84),
+    /**
+     * The client-side result code that indicates that the user requested an
+     * unknown or unsupported authentication mechanism.  This is for client-side
+     * use only and should never be transferred over protocol.
+     */
+    CLIENT_SIDE_AUTH_UNKNOWN(86),
+    /**
+     * The client-side result code that indicates that there was a problem with one or more of the parameters provided
+     * by the user.
+     * <PRE>
+     * Code value of 89.
+     * </PRE>
+     */
+    CLIENT_SIDE_PARAM_ERROR(89),
+    /**
+     * The client-side result code that indicates that the client was not able to
+     * establish a connection to the server.  This is for client-side use only and
+     * should never be transferred over protocol.
+     */
+    CLIENT_SIDE_CONNECT_ERROR(91);
+
+    private int returnCode;
+    private static final Map<Integer, String> RETURNCODE = new HashMap<>();
+    static {
+        for (final ReturnCode rc : ReturnCode.values()) {
+            RETURNCODE.put(rc.get(), rc.name());
+        }
+    }
+
+    private ReturnCode(int returnCode) {
+        this.returnCode = returnCode;
+    }
+
+    /**
+     * Get the corresponding return code value.
+     *
+     * @return The corresponding return code value.
+     */
+    public int get() {
+        return returnCode;
+    }
+
+    /**
+     * Retrieves a string representation of the return code.
+     *
+     * @param code
+     *            The code value for which to obtain the string representation.
+     * @return The string representation of the return code.
+     */
+    public static String get(int code) {
+        return RETURNCODE.get(code);
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/StringArgument.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/StringArgument.java
new file mode 100644
index 0000000..0919be4
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/StringArgument.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+/** This class defines an argument type that will accept any string value. */
+public final class StringArgument extends Argument {
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * {@link StringArgument}.
+     *
+     * @param name
+     *         The generic name that will be used to refer to this argument.
+     * @return A builder to continue building the {@link StringArgument}.
+     */
+    public static Builder builder(final String name) {
+        return new Builder(name);
+    }
+
+    /** A fluent API for incrementally constructing {@link StringArgument}. */
+    public static final class Builder extends ArgumentBuilder<Builder, String, StringArgument> {
+        private Builder(final String name) {
+            super(name);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        @Override
+        public StringArgument buildArgument() throws ArgumentException {
+            return new StringArgument(this);
+        }
+    }
+
+    private StringArgument(final Builder builder) throws ArgumentException {
+        super(builder);
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final String valueString, final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for this argument.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java
new file mode 100644
index 0000000..e6d3c82
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java
@@ -0,0 +1,355 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * This class defines a data structure for holding information about a subcommand that may be used with the subcommand
+ * argument parser. The subcommand has a name, a description, and a set of arguments.
+ */
+public class SubCommand implements DocDescriptionSupplement {
+    /** Indicates whether this subCommand should be hidden in the usage information. */
+    private boolean isHidden;
+
+    /** The mapping between the short argument IDs and the arguments for this subcommand. */
+    private final HashMap<Character, Argument> shortIDMap = new HashMap<>();
+    /** The mapping between the long argument IDs and the arguments for this subcommand. */
+    private final HashMap<String, Argument> longIDMap = new HashMap<>();
+    /** The list of arguments associated with this subcommand. */
+    private final LinkedList<Argument> arguments = new LinkedList<>();
+
+    /** The description for this subcommand. */
+    private LocalizableMessage description;
+    /** The name of this subcommand. */
+    private String name;
+
+    /** The argument parser with which this subcommand is associated. */
+    private SubCommandArgumentParser parser;
+
+    /**
+     * Indicates whether this parser will allow additional unnamed
+     * arguments at the end of the list.
+     */
+    private boolean allowsTrailingArguments;
+
+    /** The maximum number of unnamed trailing arguments that may be provided. */
+    private int maxTrailingArguments;
+    /** The minimum number of unnamed trailing arguments that may be provided. */
+    private int minTrailingArguments;
+
+    /** The display name that will be used for the trailing arguments in the usage information. */
+    private String trailingArgsDisplayName;
+
+    /**
+     * Creates a new subcommand with the provided information. The subcommand will be automatically registered with the
+     * associated parser.
+     *
+     * @param parser
+     *            The argument parser with which this subcommand is associated.
+     * @param name
+     *            The name of this subcommand.
+     * @param description
+     *            The description of this subcommand.
+     * @throws ArgumentException
+     *             If the associated argument parser already has a subcommand with the same name.
+     */
+    public SubCommand(SubCommandArgumentParser parser, String name, LocalizableMessage description)
+            throws ArgumentException {
+        this(parser, name, false, 0, 0, null, description);
+    }
+
+    /**
+     * Creates a new subcommand with the provided information. The subcommand will be automatically registered with the
+     * associated parser.
+     *
+     * @param parser
+     *            The argument parser with which this subcommand is associated.
+     * @param name
+     *            The name of this subcommand.
+     * @param allowsTrailingArguments
+     *            Indicates whether this parser allows unnamed trailing arguments to be provided.
+     * @param minTrailingArguments
+     *            The minimum number of unnamed trailing arguments that must be provided. A value less than or equal to
+     *            zero indicates that no minimum will be enforced.
+     * @param maxTrailingArguments
+     *            The maximum number of unnamed trailing arguments that may be provided. A value less than or equal to
+     *            zero indicates that no maximum will be enforced.
+     * @param trailingArgsDisplayName
+     *            The display name that should be used as a placeholder for unnamed trailing arguments in the generated
+     *            usage information.
+     * @param description
+     *            The description of this subcommand.
+     * @throws ArgumentException
+     *             If the associated argument parser already has a subcommand with the same name.
+     */
+    public SubCommand(SubCommandArgumentParser parser, String name, boolean allowsTrailingArguments,
+            int minTrailingArguments, int maxTrailingArguments, String trailingArgsDisplayName,
+            LocalizableMessage description) throws ArgumentException {
+        this.parser = parser;
+        this.name = name;
+        this.description = description;
+        this.allowsTrailingArguments = allowsTrailingArguments;
+        this.minTrailingArguments = minTrailingArguments;
+        this.maxTrailingArguments = maxTrailingArguments;
+        this.trailingArgsDisplayName = trailingArgsDisplayName;
+        this.isHidden = false;
+
+        String nameToCheck = name;
+        if (parser.longArgumentsCaseSensitive()) {
+            nameToCheck = toLowerCase(name);
+        }
+
+        if (parser.hasSubCommand(nameToCheck)) {
+            LocalizableMessage message = ERR_ARG_SUBCOMMAND_DUPLICATE_SUBCOMMAND.get(name);
+            throw new ArgumentException(message);
+        }
+
+        parser.addSubCommand(this);
+    }
+
+    /**
+     * Retrieves the name of this subcommand.
+     *
+     * @return The name of this subcommand.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Retrieves the description for this subcommand.
+     *
+     * @return The description for this subcommand.
+     */
+    public LocalizableMessage getDescription() {
+        return description;
+    }
+
+    /**
+     * A supplement to the description for this subcommand
+     * intended for use in generated reference documentation.
+     */
+    private LocalizableMessage docDescriptionSupplement;
+
+    @Override
+    public LocalizableMessage getDocDescriptionSupplement() {
+        return docDescriptionSupplement != null ? docDescriptionSupplement : LocalizableMessage.EMPTY;
+    }
+
+    /**
+     * Sets a supplement to the description intended for use in generated reference documentation.
+     *
+     * @param docDescriptionSupplement
+     *            The supplement to the description for use in generated reference documentation.
+     */
+    public void setDocDescriptionSupplement(final LocalizableMessage docDescriptionSupplement) {
+        this.docDescriptionSupplement = docDescriptionSupplement;
+    }
+
+    /**
+     * Retrieves the set of arguments for this subcommand.
+     *
+     * @return The set of arguments for this subcommand.
+     */
+    public LinkedList<Argument> getArguments() {
+        return arguments;
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified short identifier.
+     *
+     * @param shortID
+     *            The short identifier of the argument to retrieve.
+     * @return The subcommand argument with the specified short identifier, or <CODE>null</CODE> if there is none.
+     */
+    public Argument getArgument(Character shortID) {
+        return shortIDMap.get(shortID);
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified long identifier.
+     *
+     * @param longID
+     *            The long identifier of the argument to retrieve.
+     * @return The subcommand argument with the specified long identifier, or <CODE>null</CODE> if there is none.
+     */
+    public Argument getArgument(String longID) {
+        return longIDMap.get(longID);
+    }
+
+    /**
+     * Retrieves the subcommand argument with the specified long identifier.
+     *
+     * @param longIdentifier
+     *            The long identifier of the argument to retrieve.
+     * @return The subcommand argument with the specified long identifier,
+     *         or <CODE>null</CODE> if there is no such argument.
+     */
+    public Argument getArgumentForLongIdentifier(final String longIdentifier) {
+        return longIDMap.get(parser.longArgumentsCaseSensitive() ? longIdentifier : toLowerCase(longIdentifier));
+    }
+
+    /**
+     * Adds the provided argument for use with this subcommand.
+     *
+     * @param argument
+     *            The argument to add for use with this subcommand.
+     * @throws ArgumentException
+     *             If either the short ID or long ID for the argument conflicts with that of another argument already
+     *             associated with this subcommand.
+     */
+    public void addArgument(Argument argument) throws ArgumentException {
+        final String argumentLongID = argument.getLongIdentifier();
+        if (getArgumentForLongIdentifier(argumentLongID) != null) {
+            throw new ArgumentException(ERR_ARG_SUBCOMMAND_DUPLICATE_ARGUMENT_NAME.get(name, argumentLongID));
+        }
+
+        if (parser.hasGlobalArgument(argumentLongID)) {
+            throw new ArgumentException(ERR_ARG_SUBCOMMAND_ARGUMENT_GLOBAL_CONFLICT.get(argumentLongID, name));
+        }
+
+        Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            if (shortIDMap.containsKey(shortID)) {
+                throw new ArgumentException(ERR_ARG_SUBCOMMAND_DUPLICATE_SHORT_ID.get(
+                        argumentLongID, name, String.valueOf(shortID), shortIDMap.get(shortID).getLongIdentifier()));
+            }
+
+            Argument arg = parser.getGlobalArgumentForShortID(shortID);
+            if (arg != null) {
+                throw new ArgumentException(ERR_ARG_SUBCOMMAND_ARGUMENT_SHORT_ID_GLOBAL_CONFLICT.get(
+                        argumentLongID, name, String.valueOf(shortID), arg.getLongIdentifier()));
+            }
+        }
+
+        String longID = argument.getLongIdentifier();
+        if (!parser.longArgumentsCaseSensitive()) {
+            longID = toLowerCase(longID);
+            if (longIDMap.containsKey(longID)) {
+                throw new ArgumentException(ERR_ARG_SUBCOMMAND_DUPLICATE_LONG_ID.get(argumentLongID, name));
+            }
+        }
+
+        Argument arg = parser.getGlobalArgumentForLongID(longID);
+        if (arg != null) {
+            throw new ArgumentException(ERR_ARG_SUBCOMMAND_ARGUMENT_LONG_ID_GLOBAL_CONFLICT.get(argumentLongID, name));
+        }
+
+        arguments.add(argument);
+
+        if (shortID != null) {
+            shortIDMap.put(shortID, argument);
+        }
+
+        if (longID != null) {
+            longIDMap.put(longID, argument);
+        }
+    }
+
+    /**
+     * Indicates whether this sub-command will allow unnamed trailing arguments. These will be arguments at the end of
+     * the list that are not preceded by either a long or short identifier and will need to be manually parsed by the
+     * application using this parser. Note that once an unnamed trailing argument has been identified, all remaining
+     * arguments will be classified as such.
+     *
+     * @return <CODE>true</CODE> if this sub-command allows unnamed trailing arguments, or <CODE>false</CODE> if it does
+     *         not.
+     */
+    public boolean allowsTrailingArguments() {
+        return allowsTrailingArguments;
+    }
+
+    /**
+     * Retrieves the minimum number of unnamed trailing arguments that must be provided.
+     *
+     * @return The minimum number of unnamed trailing arguments that must be provided, or a value less than or equal to
+     *         zero if no minimum will be enforced.
+     */
+    public int getMinTrailingArguments() {
+        return minTrailingArguments;
+    }
+
+    /**
+     * Retrieves the maximum number of unnamed trailing arguments that may be provided.
+     *
+     * @return The maximum number of unnamed trailing arguments that may be provided, or a value less than or equal to
+     *         zero if no maximum will be enforced.
+     */
+    public int getMaxTrailingArguments() {
+        return maxTrailingArguments;
+    }
+
+    /**
+     * Retrieves the trailing arguments display name.
+     *
+     * @return Returns the trailing arguments display name.
+     */
+    public String getTrailingArgumentsDisplayName() {
+        return trailingArgsDisplayName;
+    }
+
+    /**
+     * Retrieves the set of unnamed trailing arguments that were provided on the command line.
+     *
+     * @return The set of unnamed trailing arguments that were provided on the command line.
+     */
+    public ArrayList<String> getTrailingArguments() {
+        return parser.getTrailingArguments();
+    }
+
+    /**
+     * Indicates whether this subcommand should be hidden from the usage information.
+     *
+     * @return <CODE>true</CODE> if this subcommand should be hidden from the usage information, or <CODE>false</CODE>
+     *         if not.
+     */
+    public boolean isHidden() {
+        return isHidden;
+    }
+
+    /**
+     * Specifies whether this subcommand should be hidden from the usage information.
+     *
+     * @param isHidden
+     *            Indicates whether this subcommand should be hidden from the usage information.
+     */
+    public void setHidden(boolean isHidden) {
+        this.isHidden = isHidden;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName()).append("(").append("name=").append(this.name);
+        if (!longIDMap.isEmpty()) {
+            sb.append(", longIDs=").append(longIDMap.keySet());
+        }
+        if (!shortIDMap.isEmpty()) {
+            sb.append(", shortIDs=").append(shortIDMap.keySet());
+        }
+        sb.append(")");
+        return sb.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java
new file mode 100644
index 0000000..f2a5440
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java
@@ -0,0 +1,1219 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.cli.DocGenerationHelper.*;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+
+import com.forgerock.opendj.util.OperatingSystem;
+
+/**
+ * This class defines a variant of the argument parser that can be used with applications that use subcommands to
+ * customize their behavior and that have a different set of options per subcommand (e.g, "cvs checkout" takes different
+ * options than "cvs commit"). This parser also has the ability to use global options that will always be applicable
+ * regardless of the subcommand in addition to the subcommand-specific arguments. There must not be any conflicts
+ * between the global options and the option for any subcommand, but it is allowed to re-use subcommand-specific options
+ * for different purposes between different subcommands.
+ */
+public class SubCommandArgumentParser extends ArgumentParser {
+
+    private static final String INDENT = "    ";
+    private static final int COLUMN_ADJUST = OperatingSystem.isWindows() ? 1 : 0;
+
+    /** The arguments that will be used to trigger the display of usage information for groups of sub-commands. */
+    private final Map<Argument, Collection<SubCommand>> usageGroupArguments = new HashMap<>();
+
+    /** The set of global arguments defined for this parser, referenced by short ID. */
+    private final Map<Character, Argument> globalShortIDMap = new HashMap<>();
+    /** The set of global arguments defined for this parser, referenced by long ID. */
+    private final Map<String, Argument> globalLongIDMap = new HashMap<>();
+    /** The set of global arguments defined for this parser, referenced by argument name. */
+    private final Map<String, Argument> globalArgumentMap = new HashMap<>();
+    /** The total set of global arguments defined for this parser. */
+    private final List<Argument> globalArgumentList = new LinkedList<>();
+    /** The set of subcommands defined for this parser, referenced by subcommand name. */
+    private final SortedMap<String, SubCommand> subCommands = new TreeMap<>();
+
+    /** The subcommand requested by the user as part of the command-line arguments. */
+    private SubCommand subCommand;
+    private SubCommandUsageHandler subCommandUsageHandler;
+
+    /**
+     * Creates a new instance of this subcommand argument parser with no arguments.
+     *
+     * @param mainClassName
+     *            The fully-qualified name of the Java class that should be invoked to launch the program with which
+     *            this argument parser is associated.
+     * @param toolDescription
+     *            A human-readable description for the tool, which will be included when displaying usage information.
+     * @param longArgumentsCaseSensitive
+     *            Indicates whether subcommand and long argument names should be treated in a case-sensitive manner.
+     */
+    public SubCommandArgumentParser(String mainClassName, LocalizableMessage toolDescription,
+            boolean longArgumentsCaseSensitive) {
+        super(mainClassName, toolDescription, longArgumentsCaseSensitive);
+    }
+
+    /**
+     * Indicates whether this argument parser contains a global argument with the specified name.
+     *
+     * @param argumentName
+     *            The name for which to make the determination.
+     * @return <CODE>true</CODE> if a global argument exists with the specified name, or <CODE>false</CODE> if not.
+     */
+    public boolean hasGlobalArgument(String argumentName) {
+        return globalArgumentMap.containsKey(argumentName);
+    }
+
+    /**
+     * Retrieves the global argument with the specified short identifier.
+     *
+     * @param shortID
+     *            The short identifier for the global argument to retrieve.
+     * @return The global argument with the specified short identifier, or <CODE>null</CODE> if there is no such
+     *         argument.
+     */
+    public Argument getGlobalArgumentForShortID(Character shortID) {
+        return globalShortIDMap.get(shortID);
+    }
+
+    /**
+     * Retrieves the global argument with the specified long identifier.
+     *
+     * @param longID
+     *            The long identifier for the global argument to retrieve.
+     * @return The global argument with the specified long identifier, or <CODE>null</CODE> if there is no such
+     *         argument.
+     */
+    public Argument getGlobalArgumentForLongID(String longID) {
+        return globalLongIDMap.get(longID);
+    }
+
+    /**
+     * Indicates whether this argument parser has a subcommand with the specified name.
+     *
+     * @param name
+     *            The subcommand name for which to make the determination.
+     * @return <CODE>true</CODE> if this argument parser has a subcommand with the specified name, or <CODE>false</CODE>
+     *         if it does not.
+     */
+    public boolean hasSubCommand(String name) {
+        return subCommands.containsKey(name);
+    }
+
+    /**
+     * Retrieves the subcommand with the specified name.
+     *
+     * @param name
+     *            The name of the subcommand to retrieve.
+     * @return The subcommand with the specified name, or <CODE>null</CODE> if no such subcommand is defined.
+     */
+    public SubCommand getSubCommand(String name) {
+        return subCommands.get(name);
+    }
+
+    /**
+     * Retrieves the subcommand that was selected in the set of command-line arguments.
+     *
+     * @return The subcommand that was selected in the set of command-line arguments, or <CODE>null</CODE> if none was
+     *         selected.
+     */
+    public SubCommand getSubCommand() {
+        return subCommand;
+    }
+
+    /**
+     * Adds the provided argument to the set of global arguments handled by this parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another global or subcommand argument that has already been
+     *             defined.
+     */
+    public void addGlobalArgument(Argument argument) throws ArgumentException {
+        addGlobalArgument(argument, null);
+    }
+
+    /**
+     * Adds the provided argument to the set of arguments handled by this parser and puts the argument in the LDAP
+     * connection group.
+     *
+     * @param argument
+     *            The argument to add to this sub command.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another global or subcommand argument that has already been
+     *             defined.
+     */
+    @Override
+    public void addLdapConnectionArgument(final Argument argument) throws ArgumentException {
+        addGlobalArgument(argument, null);
+    }
+
+    /**
+     * Adds the provided argument to the set of global arguments handled by this parser.
+     *
+     * @param argument
+     *            The argument to be added.
+     * @param group
+     *            The argument group to which the argument belongs.
+     * @throws ArgumentException
+     *             If the provided argument conflicts with another global or subcommand argument that has already been
+     *             defined.
+     */
+    public void addGlobalArgument(Argument argument, ArgumentGroup group) throws ArgumentException {
+        String longID = argument.getLongIdentifier();
+        if (globalArgumentMap.containsKey(longID)) {
+            throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME.get(longID));
+        }
+        for (SubCommand s : subCommands.values()) {
+            if (s.getArgumentForLongIdentifier(longID) != null) {
+                throw new ArgumentException(ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT.get(
+                        longID, s.getName()));
+            }
+        }
+
+        Character shortID = argument.getShortIdentifier();
+        if (shortID != null) {
+            if (globalShortIDMap.containsKey(shortID)) {
+                String conflictingLongID = globalShortIDMap.get(shortID).getLongIdentifier();
+                throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_SHORT_ID.get(
+                        shortID, longID, conflictingLongID));
+            }
+
+            for (SubCommand s : subCommands.values()) {
+                if (s.getArgument(shortID) != null) {
+                    String conflictingLongID = s.getArgument(shortID).getLongIdentifier();
+                    throw new ArgumentException(ERR_SUBCMDPARSER_GLOBAL_ARG_SHORT_ID_CONFLICT.get(
+                            shortID, longID, conflictingLongID, s.getName()));
+                }
+            }
+        }
+
+        if (!longArgumentsCaseSensitive()) {
+            longID = toLowerCase(longID);
+            if (globalLongIDMap.containsKey(longID)) {
+                throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID.get(longID));
+            }
+        }
+
+        for (SubCommand s : subCommands.values()) {
+            if (s.getArgument(longID) != null) {
+                throw new ArgumentException(ERR_SUBCMDPARSER_GLOBAL_ARG_LONG_ID_CONFLICT.get(longID, s.getName()));
+            }
+        }
+
+        if (shortID != null) {
+            globalShortIDMap.put(shortID, argument);
+        }
+
+        if (longID != null) {
+            globalLongIDMap.put(longID, argument);
+        }
+
+        globalArgumentList.add(argument);
+
+        if (group == null) {
+            group = getStandardGroup(argument);
+        }
+        group.addArgument(argument);
+        argumentGroups.add(group);
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the output of full usage information if it is
+     * provided on the command line and no further argument validation will be performed.
+     * <p>
+     * If sub-command groups are defined using the {@link #setUsageGroupArgument(Argument, Collection)} method, then
+     * this usage argument, when specified, will result in usage information being displayed which does not include
+     * information on sub-commands.
+     * <p>
+     * Note that the caller will still need to add this argument to the parser with the
+     * {@link #addGlobalArgument(Argument)} method, and the argument should not be required and should not take a value.
+     * Also, the caller will still need to check for the presence of the usage argument after calling
+     * {@link #parseArguments(String[])} to know that no further processing will be required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the display of full usage information.
+     * @param outputStream
+     *            The output stream to which the usage information should be written.
+     */
+    @Override
+    public void setUsageArgument(Argument argument, OutputStream outputStream) {
+        super.setUsageArgument(argument, outputStream);
+        usageGroupArguments.put(argument, Collections.<SubCommand>emptySet());
+    }
+
+    /**
+     * Sets the provided argument as one which will automatically trigger the output of partial usage information if it
+     * is provided on the command line and no further argument validation will be performed.
+     * <p>
+     * Partial usage information will include a usage synopsis, a summary of each of the sub-commands listed in the
+     * provided sub-commands collection, and a summary of the global options.
+     * <p>
+     * Note that the caller will still need to add this argument to the parser with the
+     * {@link #addGlobalArgument(Argument)} method, and the argument should not be required and should not take a value.
+     * Also, the caller will still need to check for the presence of the usage argument after calling
+     * {@link #parseArguments(String[])} to know that no further processing will be required.
+     *
+     * @param argument
+     *            The argument whose presence should automatically trigger the display of partial usage information.
+     * @param subCommands
+     *            The list of sub-commands which should have their usage displayed.
+     */
+    public void setUsageGroupArgument(Argument argument, Collection<SubCommand> subCommands) {
+        usageGroupArguments.put(argument, subCommands);
+    }
+
+    /**
+     * Sets the sub-command usage handler which will be used to display the usage information.
+     *
+     * @param subCommandUsageHandler the sub-command usage handler
+     */
+    public void setUsageHandler(SubCommandUsageHandler subCommandUsageHandler) {
+        this.subCommandUsageHandler = subCommandUsageHandler;
+    }
+
+    /**
+     * Parses the provided set of arguments and updates the information associated with this parser accordingly. Default
+     * values for unspecified arguments may be read from the specified properties if any are provided.
+     *
+     * @param rawArguments
+     *            The set of raw arguments to parse.
+     * @param argumentProperties
+     *            A set of properties that may be used to provide default values for arguments not included in the given
+     *            raw arguments.
+     * @throws ArgumentException
+     *             If a problem was encountered while parsing the provided arguments.
+     */
+    @Override
+    public void parseArguments(String[] rawArguments, Properties argumentProperties) throws ArgumentException {
+        this.subCommand = null;
+        final ArrayList<String> trailingArguments = getTrailingArguments();
+        trailingArguments.clear();
+        setUsageOrVersionDisplayed(false);
+
+        boolean inTrailingArgs = false;
+
+        int numArguments = rawArguments.length;
+        for (int i = 0; i < numArguments; i++) {
+            final String arg = rawArguments[i];
+
+            if (inTrailingArgs) {
+                trailingArguments.add(arg);
+
+                if (subCommand == null) {
+                    throw new ArgumentException(ERR_ARG_SUBCOMMAND_INVALID.get());
+                }
+
+                if (subCommand.getMaxTrailingArguments() > 0
+                        && trailingArguments.size() > subCommand.getMaxTrailingArguments()) {
+                    throw new ArgumentException(ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS.get(
+                            subCommand.getMaxTrailingArguments()));
+                }
+
+                continue;
+            }
+
+            if (arg.equals("--")) {
+                inTrailingArgs = true;
+            } else if (arg.startsWith("--")) {
+                // This indicates that we are using the long name to reference the
+                // argument. It may be in any of the following forms:
+                // --name
+                // --name value
+                // --name=value
+
+                String argName = arg.substring(2);
+                String argValue = null;
+                int equalPos = argName.indexOf('=');
+                if (equalPos < 0) {
+                    // This is fine. The value is not part of the argument name token.
+                } else if (equalPos == 0) {
+                    // The argument starts with "--=", which is not acceptable.
+                    throw new ArgumentException(ERR_SUBCMDPARSER_LONG_ARG_WITHOUT_NAME.get(arg));
+                } else {
+                    // The argument is in the form --name=value, so parse them both out.
+                    argValue = argName.substring(equalPos + 1);
+                    argName = argName.substring(0, equalPos);
+                }
+
+                // If we're not case-sensitive, then convert the name to lowercase.
+                String origArgName = argName;
+                if (!longArgumentsCaseSensitive()) {
+                    argName = toLowerCase(argName);
+                }
+
+                // See if the specified name references a global argument. If not, then
+                // see if it references a subcommand argument.
+                Argument a = globalLongIDMap.get(argName);
+                if (a == null) {
+                    if (subCommand != null) {
+                        a = subCommand.getArgument(argName);
+                    }
+                    if (a == null) {
+                        if (OPTION_LONG_HELP.equals(argName)) {
+                            // "--help" will always be interpreted as requesting usage
+                            // information.
+                            writeToUsageOutputStream(getUsage());
+                            return;
+                        } else if (OPTION_LONG_PRODUCT_VERSION.equals(argName)) {
+                            // "--version" will always be interpreted as requesting usage information.
+                            printVersion();
+                            return;
+                        } else if (subCommand != null) {
+                            // There is no such global argument.
+                            throw new ArgumentException(
+                                    ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_LONG_ID.get(origArgName));
+                        } else {
+                            // There is no such global or subcommand argument.
+                            throw new ArgumentException(ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_LONG_ID.get(origArgName));
+                        }
+                    }
+                }
+
+                a.setPresent(true);
+
+                // If this is a usage argument, then immediately stop and print
+                // usage information.
+                if (usageGroupArguments.containsKey(a)) {
+                    getUsage(a);
+                    return;
+                }
+
+                // See if the argument takes a value. If so, then make sure one was
+                // provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            throw new ArgumentException(
+                                    ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID.get(argName));
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        throw new ArgumentException(ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID.get(
+                                argValue, argName, invalidReason));
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        throw new ArgumentException(ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_LONG_ID.get(origArgName));
+                    }
+
+                    a.addValue(argValue);
+                } else if (argValue != null) {
+                    throw new ArgumentException(
+                            ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE.get(origArgName));
+                }
+            } else if (arg.equals("-")) {
+                throw new ArgumentException(ERR_SUBCMDPARSER_INVALID_DASH_AS_ARGUMENT.get());
+            } else if (arg.startsWith("-")) {
+                // This indicates that we are using the 1-character name to reference
+                // the argument. It may be in any of the following forms:
+                // -n
+                // -nvalue
+                // -n value
+                char argCharacter = arg.charAt(1);
+                String argValue;
+                if (arg.length() > 2) {
+                    argValue = arg.substring(2);
+                } else {
+                    argValue = null;
+                }
+
+                // Get the argument with the specified short ID. It may be either a
+                // global argument or a subcommand-specific argument.
+                Argument a = globalShortIDMap.get(argCharacter);
+                if (a == null) {
+                    if (subCommand == null) {
+                        if (argCharacter == '?') {
+                            // "-?" will always be interpreted as requesting usage.
+                            writeToUsageOutputStream(getUsage());
+                            if (getUsageArgument() != null) {
+                                getUsageArgument().setPresent(true);
+                            }
+                            return;
+                        } else if (argCharacter == OPTION_SHORT_PRODUCT_VERSION) {
+                            // "-V" will always be interpreted as requesting
+                            // version information except if it's already defined.
+                            if (dashVAccepted()) {
+                                printVersion();
+                                return;
+                            } else {
+                                // -V is defined in another subcommand, so we cannot
+                                // accept it as the version information argument
+                                throw new ArgumentException(
+                                        ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.get(argCharacter));
+                            }
+                        } else {
+                            // There is no such argument registered.
+                            throw new ArgumentException(
+                                    ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.get(argCharacter));
+                        }
+                    } else {
+                        a = subCommand.getArgument(argCharacter);
+                        if (a == null) {
+                            if (argCharacter == '?') {
+                                // "-?" will always be interpreted as requesting usage.
+                                writeToUsageOutputStream(getUsage());
+                                return;
+                            } else if (argCharacter == OPTION_SHORT_PRODUCT_VERSION) {
+                                if (dashVAccepted()) {
+                                    printVersion();
+                                    return;
+                                }
+                            } else {
+                                // There is no such argument registered.
+                                throw new ArgumentException(
+                                        ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID.get(argCharacter));
+                            }
+                        }
+                    }
+                }
+
+                a.setPresent(true);
+
+                // If this is the usage argument, then immediately stop and print
+                // usage information.
+                if (usageGroupArguments.containsKey(a)) {
+                    getUsage(a);
+                    return;
+                }
+
+                // See if the argument takes a value. If so, then make sure one was
+                // provided. If not, then make sure none was provided.
+                if (a.needsValue()) {
+                    if (argValue == null) {
+                        if ((i + 1) == numArguments) {
+                            throw new ArgumentException(
+                                    ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID.get(argCharacter));
+                        }
+
+                        argValue = rawArguments[++i];
+                    }
+
+                    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+                    if (!a.valueIsAcceptable(argValue, invalidReason)) {
+                        throw new ArgumentException(ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID.get(argValue,
+                                argCharacter, invalidReason));
+                    }
+
+                    // If the argument already has a value, then make sure it is
+                    // acceptable to have more than one.
+                    if (a.hasValue() && !a.isMultiValued()) {
+                        throw new ArgumentException(ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_SHORT_ID.get(argCharacter));
+                    }
+
+                    a.addValue(argValue);
+                } else if (argValue != null) {
+                    // If we've gotten here, then it means that we're in a scenario like
+                    // "-abc" where "a" is a valid argument that doesn't take a value.
+                    // However, this could still be valid if all remaining characters in
+                    // the value are also valid argument characters that don't take
+                    // values.
+                    int valueLength = argValue.length();
+                    for (int j = 0; j < valueLength; j++) {
+                        char c = argValue.charAt(j);
+                        Argument b = globalShortIDMap.get(c);
+                        if (b == null) {
+                            if (subCommand == null) {
+                                throw new ArgumentException(
+                                        ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.get(argCharacter));
+                            }
+                            b = subCommand.getArgument(c);
+                            if (b == null) {
+                                throw new ArgumentException(
+                                        ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID.get(argCharacter));
+                            }
+                        }
+
+                        if (b.needsValue()) {
+                            // This means we're in a scenario like "-abc" where b is a
+                            // valid argument that takes a value. We don't support that.
+                            throw new ArgumentException(ERR_SUBCMDPARSER_CANT_MIX_ARGS_WITH_VALUES.get(
+                                    argCharacter, argValue, c));
+                        }
+                        b.setPresent(true);
+
+                        // If this is the usage argument, then immediately stop and
+                        // print usage information.
+                        if (usageGroupArguments.containsKey(b)) {
+                            getUsage(b);
+                            return;
+                        }
+                    }
+                }
+            } else if (subCommand != null) {
+                // It's not a short or long identifier and the sub-command has
+                // already been specified, so it must be the first trailing argument.
+                if (subCommand.allowsTrailingArguments()) {
+                    trailingArguments.add(arg);
+                    inTrailingArgs = true;
+                } else {
+                    // Trailing arguments are not allowed for this sub-command.
+                    throw new ArgumentException(ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT.get(arg));
+                }
+            } else {
+                // It must be the sub-command.
+                String nameToCheck = arg;
+                if (!longArgumentsCaseSensitive()) {
+                    nameToCheck = toLowerCase(arg);
+                }
+
+                SubCommand sc = subCommands.get(nameToCheck);
+                if (sc == null) {
+                    throw new ArgumentException(ERR_SUBCMDPARSER_INVALID_ARGUMENT.get(arg));
+                }
+                subCommand = sc;
+            }
+        }
+
+        // If we have a sub-command and it allows trailing arguments and
+        // there is a minimum number, then make sure at least that many
+        // were provided.
+        if (subCommand != null) {
+            int minTrailingArguments = subCommand.getMinTrailingArguments();
+            if (subCommand.allowsTrailingArguments()
+                    && minTrailingArguments > 0
+                    && trailingArguments.size() < minTrailingArguments) {
+                throw new ArgumentException(ERR_ARGPARSER_TOO_FEW_TRAILING_ARGUMENTS.get(minTrailingArguments));
+            }
+        }
+
+        // If we don't have the argumentProperties, try to load a properties file.
+        if (argumentProperties == null) {
+            argumentProperties = checkExternalProperties();
+        }
+
+        // Iterate through all the global arguments
+        normalizeArguments(argumentProperties, globalArgumentList);
+
+        // Iterate through all the subcommand-specific arguments
+        if (subCommand != null) {
+            normalizeArguments(argumentProperties, subCommand.getArguments());
+        }
+    }
+
+    private boolean dashVAccepted() {
+        if (globalShortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION)) {
+            return false;
+        }
+        for (SubCommand subCmd : subCommands.values()) {
+            if (subCmd.getArgument(OPTION_SHORT_PRODUCT_VERSION) != null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Appends usage information for the specified subcommand to the provided buffer.
+     *
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     * @param subCommand
+     *            The subcommand for which to display the usage information.
+     */
+    public void getSubCommandUsage(StringBuilder buffer, SubCommand subCommand) {
+        buffer.append(getLocalizableScriptName());
+        buffer.append(" ");
+        buffer.append(subCommand.getName());
+        buffer.append(" ").append(INFO_SUBCMDPARSER_OPTIONS.get());
+        if (subCommand.allowsTrailingArguments()) {
+            buffer.append(' ');
+            buffer.append(subCommand.getTrailingArgumentsDisplayName());
+        }
+        buffer.append(EOL);
+        wrap(buffer, subCommand.getDescription());
+        buffer.append(EOL);
+
+        if (!globalArgumentList.isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(INFO_GLOBAL_OPTIONS.get());
+            buffer.append(EOL);
+            buffer.append("    ");
+            buffer.append(INFO_GLOBAL_OPTIONS_REFERENCE.get(getScriptNameOrJava()));
+            buffer.append(EOL);
+        }
+
+        if (!subCommand.getArguments().isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(INFO_SUBCMD_OPTIONS.get());
+            buffer.append(EOL);
+        }
+
+        for (Argument a : subCommand.getArguments()) {
+            // If this argument is hidden, then skip it.
+            if (a.isHidden()) {
+                continue;
+            }
+
+            printLineForShortLongArgument(a, buffer);
+
+            indentAndWrap(buffer, INDENT, a.getDescription());
+            if (a.needsValue() && a.getDefaultValue() != null && a.getDefaultValue().length() > 0) {
+                indentAndWrap(buffer, INDENT, INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()));
+            }
+        }
+    }
+
+    /**
+     * Retrieves a string containing usage information based on the defined arguments.
+     *
+     * @return A string containing usage information based on the defined arguments.
+     */
+    @Override
+    public String getUsage() {
+        setUsageOrVersionDisplayed(true);
+
+        final StringBuilder buffer = new StringBuilder();
+        if (subCommand == null) {
+            if (System.getProperty("org.forgerock.opendj.gendoc") != null) {
+                generateReferenceDoc(buffer, subCommands.values());
+            } else if (usageGroupArguments.size() > 1) {
+                // We have sub-command groups, so don't display any
+                // sub-commands by default.
+                getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
+            } else {
+                // No grouping, so display all sub-commands.
+                getFullUsage(subCommands.values(), true, buffer);
+            }
+        } else {
+            getSubCommandUsage(buffer, subCommand);
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Adds the provided subcommand to this argument parser. This is only intended for use by the
+     * <CODE>SubCommand</CODE> constructor and does not do any validation of its own to ensure that there are no
+     * conflicts with the subcommand or any of its arguments.
+     *
+     * @param subCommand
+     *            The subcommand to add to this argument parser.
+     */
+    void addSubCommand(SubCommand subCommand) {
+        subCommands.put(toLowerCase(subCommand.getName()), subCommand);
+    }
+
+    /** Get usage for a specific usage argument. */
+    private void getUsage(Argument a) {
+        setUsageOrVersionDisplayed(true);
+
+        final StringBuilder buffer = new StringBuilder();
+        final boolean isUsageArgument = isUsageArgument(a);
+        if (isUsageArgument && subCommand != null) {
+            getSubCommandUsage(buffer, subCommand);
+        } else if (isUsageArgument && usageGroupArguments.size() <= 1) {
+            // No groups - so display all sub-commands.
+            getFullUsage(subCommands.values(), true, buffer);
+        } else if (isUsageArgument) {
+            // Using groups - so display all sub-commands group help.
+            getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
+        } else {
+            // Requested help on specific group - don't display global options.
+            getFullUsage(usageGroupArguments.get(a), false, buffer);
+        }
+        writeToUsageOutputStream(buffer);
+    }
+
+    /** Appends complete usage information for the specified set of sub-commands. */
+    private void getFullUsage(Collection<SubCommand> c, boolean showGlobalOptions, StringBuilder buffer) {
+        final LocalizableMessage toolDescription = getToolDescription();
+        if (toolDescription != null && toolDescription.length() > 0) {
+            buffer.append(wrapText(toolDescription, MAX_LINE_WIDTH - 1));
+            buffer.append(EOL).append(EOL);
+        }
+
+        buffer.append(INFO_ARGPARSER_USAGE.get());
+        buffer.append("  ");
+        buffer.append(getScriptNameOrJava());
+
+        if (subCommands.isEmpty()) {
+            buffer.append(" ").append(INFO_SUBCMDPARSER_OPTIONS.get());
+        } else {
+            buffer.append(" ").append(INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS.get());
+        }
+
+        if (!subCommands.isEmpty()) {
+            buffer.append(EOL);
+            buffer.append(EOL);
+            if (c.isEmpty()) {
+                buffer.append(INFO_SUBCMDPARSER_SUBCMD_HELP_HEADING.get());
+            } else {
+                buffer.append(INFO_SUBCMDPARSER_SUBCMD_HEADING.get());
+            }
+            buffer.append(EOL);
+        }
+
+        if (c.isEmpty()) {
+            // Display usage arguments (except the default one).
+            for (Argument a : globalArgumentList) {
+                if (a.isHidden()) {
+                    continue;
+                }
+
+                if (usageGroupArguments.containsKey(a) && !isUsageArgument(a)) {
+                    printArgumentUsage(a, buffer);
+                }
+            }
+        } else {
+            boolean isFirst = true;
+            for (SubCommand sc : c) {
+                if (sc.isHidden()) {
+                    continue;
+                }
+                if (isFirst) {
+                    buffer.append(EOL);
+                }
+                buffer.append(sc.getName());
+                buffer.append(EOL);
+                indentAndWrap(buffer, INDENT, sc.getDescription());
+                buffer.append(EOL);
+                isFirst = false;
+            }
+        }
+
+        buffer.append(EOL);
+
+        if (showGlobalOptions) {
+            if (subCommands.isEmpty()) {
+                buffer.append(INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
+            } else {
+                buffer.append(INFO_SUBCMDPARSER_GLOBAL_HEADING.get());
+            }
+            buffer.append(EOL).append(EOL);
+
+            boolean printGroupHeaders = printUsageGroupHeaders();
+
+            // Display non-usage arguments.
+            for (ArgumentGroup argGroup : argumentGroups) {
+                if (argGroup.containsArguments() && printGroupHeaders) {
+                    // Print the groups description if any
+                    LocalizableMessage groupDesc = argGroup.getDescription();
+                    if (groupDesc != null && !LocalizableMessage.EMPTY.equals(groupDesc)) {
+                        buffer.append(EOL);
+                        buffer.append(wrapText(groupDesc.toString(), MAX_LINE_WIDTH - 1));
+                        buffer.append(EOL).append(EOL);
+                    }
+                }
+
+                for (Argument a : argGroup.getArguments()) {
+                    if (a.isHidden()) {
+                        continue;
+                    }
+
+                    if (!usageGroupArguments.containsKey(a)) {
+                        printArgumentUsage(a, buffer);
+                    }
+                }
+            }
+
+            // Finally print default usage argument.
+            final Argument usageArgument = getUsageArgument();
+            if (usageArgument != null) {
+                printArgumentUsage(usageArgument, buffer);
+            } else {
+                buffer.append("-?");
+            }
+            buffer.append(EOL);
+        }
+    }
+
+    /**
+     * Appends argument usage information to the provided buffer.
+     *
+     * @param a
+     *            The argument to handle.
+     * @param buffer
+     *            The buffer to which the usage information should be appended.
+     */
+    private void printArgumentUsage(Argument a, StringBuilder buffer) {
+        String value;
+        if (a.needsValue()) {
+            LocalizableMessage pHolder = a.getValuePlaceholder();
+            if (pHolder != null) {
+                value = " " + pHolder;
+            } else {
+                value = " {value}";
+            }
+        } else {
+            value = "";
+        }
+
+        final Argument usageArgument = getUsageArgument();
+        Character shortIDChar = a.getShortIdentifier();
+        if (shortIDChar != null) {
+            if (a.equals(usageArgument)) {
+                buffer.append("-?, ");
+            }
+            buffer.append("-");
+            buffer.append(shortIDChar);
+
+            String longIDString = a.getLongIdentifier();
+            if (longIDString != null) {
+                buffer.append(", --");
+                buffer.append(longIDString);
+            }
+            buffer.append(value);
+        } else {
+            String longIDString = a.getLongIdentifier();
+            if (longIDString != null) {
+                if (a.equals(usageArgument)) {
+                    buffer.append("-?, ");
+                }
+                buffer.append("--");
+                buffer.append(longIDString);
+                buffer.append(value);
+            }
+        }
+
+        buffer.append(EOL);
+
+        indentAndWrap(buffer, INDENT, a.getDescription());
+        if (a.needsValue() && a.getDefaultValue() != null && a.getDefaultValue().length() > 0) {
+            indentAndWrap(buffer, INDENT, INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()));
+        }
+    }
+
+    /** Wraps long lines without indentation. */
+    private void wrap(StringBuilder buffer, LocalizableMessage text) {
+        indentAndWrap(buffer, "", text);
+    }
+
+    /**
+     * Write one or more lines with the description of the argument. We will indent the description five characters and
+     * try our best to wrap at or before column 79 so it will be friendly to 80-column displays.
+     * <p>
+     * FIXME consider merging with com.forgerock.opendj.cli.Utils#wrapText(String, int, int)
+     */
+    private void indentAndWrap(StringBuilder buffer, String indent, LocalizableMessage text) {
+        int actualSize = MAX_LINE_WIDTH - indent.length() - COLUMN_ADJUST;
+        indentAndWrap(indent, buffer, actualSize, text);
+    }
+
+    static void indentAndWrap(String indent, StringBuilder buffer, int actualSize, LocalizableMessage text) {
+        if (text.length() <= actualSize) {
+            buffer.append(indent);
+            buffer.append(text);
+            buffer.append(EOL);
+        } else {
+            String s = text.toString();
+            while (s.length() > actualSize) {
+                int spacePos = s.lastIndexOf(' ', actualSize);
+                if (spacePos == -1) {
+                    // There are no spaces in the first actualSize -1 columns.
+                    // See if there is one after that point.
+                    // If so, then break there. If not, then don't break at all.
+                    spacePos = s.indexOf(' ');
+                }
+                if (spacePos == -1) {
+                    buffer.append(indent).append(s).append(EOL);
+                    return;
+                }
+                buffer.append(indent);
+                buffer.append(s.substring(0, spacePos).trim());
+                s = s.substring(spacePos + 1).trim();
+                buffer.append(EOL);
+            }
+
+            if (s.length() > 0) {
+                buffer.append(indent).append(s).append(EOL);
+            }
+        }
+    }
+
+    /**
+     * Appends a generated DocBook XML RefEntry element for this command to the StringBuilder.
+     *
+     * @param builder       Append the RefEntry element to this.
+     * @param subCommands   SubCommands containing reference information.
+     */
+    private void generateReferenceDoc(final StringBuilder builder, Collection<SubCommand> subCommands) {
+        toRefEntry(builder, subCommands);
+    }
+
+    @Override
+    String getSynopsisArgs() {
+        if (subCommands.isEmpty()) {
+            return INFO_SUBCMDPARSER_OPTIONS.get().toString();
+        } else {
+            return INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS.get().toString();
+        }
+    }
+
+    /**
+     * Appends one or more generated DocBook XML RefEntry elements (man pages) to the StringBuilder.
+     * <br>
+     * If the result contains more than one RefEntry,
+     * then the RefEntry elements are separated with a marker:
+     * {@code @@@scriptName + "-" + subCommand.getName() + @@@}.
+     *
+     * @param builder       Append the RefEntry element to this.
+     * @param subCommands   Collection of subcommands for this tool.
+     */
+    void toRefEntry(StringBuilder builder, Collection<SubCommand> subCommands) {
+        final String scriptName = getScriptName();
+        if (scriptName == null) {
+            throw new RuntimeException("The script name should have been set via the environment property '"
+                    + PROPERTY_SCRIPT_NAME + "'.");
+        }
+
+        // Model for a FreeMarker template.
+        Map<String, Object> map = new HashMap<>();
+        map.put("locale", Locale.getDefault().getLanguage());
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+        map.put("name", scriptName);
+        map.put("shortDesc", getShortToolDescription());
+        map.put("descTitle", REF_TITLE_DESCRIPTION.get());
+        map.put("args", getSynopsisArgs());
+        map.put("description", eolToNewPara(getToolDescription()));
+        map.put("info", getDocToolDescriptionSupplement());
+        if (!globalArgumentList.isEmpty()) {
+            map.put("optionSection", getOptionsRefSect1(scriptName));
+        }
+        map.put("subcommands", toRefSect1(scriptName, subCommands));
+        map.put("trailingSectionString", System.getProperty("org.forgerock.opendj.gendoc.trailing"));
+        applyTemplate(builder, "refEntry.ftl", map);
+
+        // For dsconfig, generate one page per subcommand.
+        if (scriptName.equals("dsconfig")) {
+            appendSubCommandPages(builder, scriptName, subCommands);
+        }
+    }
+
+    /**
+     * Returns a generated DocBook XML RefSect1 element for all subcommands.
+     * @param scriptName    The name of this script.
+     * @param subCommands   The SubCommands containing the reference information.
+     * @return              The RefSect1 element as a String.
+     */
+    private String toRefSect1(String scriptName, Collection<SubCommand> subCommands) {
+        if (subCommands.isEmpty()) {
+            return "";
+        }
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("name", scriptName);
+        map.put("title", REF_TITLE_SUBCOMMANDS.get());
+        map.put("info", getDocSubcommandsDescriptionSupplement());
+        map.put("intro", REF_INTRO_SUBCOMMANDS.get(scriptName));
+        if (scriptName.equals("dsconfig")) {
+            // Break dsconfig into multiple pages, so use only the list here.
+            map.put("isItemizedList", true);
+        }
+        List<String> scUsageList = new ArrayList<>();
+        for (SubCommand subCommand : subCommands) {
+            if (subCommand.isHidden()) {
+                continue;
+            }
+            if (scriptName.equals("dsconfig")) {
+                scUsageList.add(getSubCommandListItem(scriptName, subCommand));
+            } else {
+                scUsageList.add(toRefSect2(scriptName, subCommand));
+            }
+        }
+        map.put("subcommands", scUsageList);
+
+        StringBuilder sb = new StringBuilder();
+        applyTemplate(sb, "refSect1.ftl", map);
+        return sb.toString();
+    }
+
+    /**
+     * Returns a DocBook XML ListItem element linking to the subcommand page.
+     * @param scriptName    The name of this script.
+     * @param subCommand    The SubCommand to reference.
+     * @return A DocBook XML ListItem element linking to the subcommand page.
+     */
+    private String getSubCommandListItem(String scriptName, SubCommand subCommand) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", scriptName + "-" + subCommand.getName());
+        map.put("name", scriptName + " " + subCommand.getName());
+        map.put("description", eolToNewPara(subCommand.getDescription()));
+        StringBuilder sb = new StringBuilder();
+        applyTemplate(sb, "dscfgListItem.ftl", map);
+        return sb.toString();
+    }
+
+    /**
+     * Returns a generated DocBook XML RefSect2 element for a single subcommand to the StringBuilder.
+     *
+     * @param scriptName    The name of this script.
+     * @param subCommand    The SubCommand containing reference information.
+     * @return    The RefSect2 element as a String.
+     */
+    private String toRefSect2(String scriptName, SubCommand subCommand) {
+        // Model for a FreeMarker template.
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", scriptName + "-" + subCommand.getName());
+        final String name = scriptName + " " + subCommand.getName();
+        map.put("name", name);
+        map.put("description", eolToNewPara(subCommand.getDescription()));
+        map.put("optionsTitle", REF_TITLE_OPTIONS.get());
+        map.put("optionsIntro", REF_INTRO_OPTIONS.get(name));
+
+        // If there is a supplement to the description for this subcommand,
+        // then it is already DocBook XML, so use it as is.
+        map.put("info", subCommand.getDocDescriptionSupplement());
+        setSubCommandOptionsInfo(map, subCommand);
+
+        StringBuilder sb = new StringBuilder();
+        applyTemplate(sb, "refSect2.ftl", map);
+        return sb.toString();
+    }
+
+    /**
+     * Sets information for the subcommand options in the map.
+     * <br>
+     * The map is expected to be used in a FreeMarker template to generate docs.
+     *
+     * @param map           The map in which to set the information.
+     * @param subCommand    The subcommand containing the information.
+     */
+    private void setSubCommandOptionsInfo(Map<String, Object> map, SubCommand subCommand) {
+        if (!subCommand.getArguments().isEmpty()) {
+            List<Map<String, Object>> options = new LinkedList<>();
+            String nameOption = null;
+            for (Argument a : subCommand.getArguments()) {
+                if (a.isHidden()) {
+                    continue;
+                }
+
+                Map<String, Object> option = new HashMap<>();
+                String optionSynopsis = getOptionSynopsis(a);
+                option.put("synopsis", optionSynopsis);
+                option.put("description", eolToNewPara(a.getDescription()));
+                Map<String, Object> info = new HashMap<>();
+                if (subCommandUsageHandler != null) {
+                    if (!doesHandleProperties(a)) {
+                        nameOption = "<option>" + optionSynopsis + "</option>";
+                    }
+
+                    // Let this build its own arbitrarily formatted additional info.
+                    info.put("usage", subCommandUsageHandler.getArgumentAdditionalInfo(subCommand, a, nameOption));
+                } else {
+                    // Return a generic FQDN for localhost as the default hostname in reference documentation.
+                    final String defaultValue = isHostNameArgument(a) ? "localhost.localdomain" : a.getDefaultValue();
+                    info.put("default", defaultValue != null ? REF_DEFAULT.get(defaultValue) : null);
+
+                    // If there is a supplement to the description for this argument,
+                    // then it is already DocBook XML, so use it as is.
+                    info.put("doc", a.getDocDescriptionSupplement());
+                }
+                option.put("info", info);
+                options.add(option);
+            }
+            map.put("options", options);
+        }
+
+        if (subCommandUsageHandler != null) {
+            map.put("propertiesInfo", subCommandUsageHandler.getProperties(subCommand));
+        }
+    }
+
+    /**
+     * Appends a generated DocBook XML RefEntry element for each subcommand to the StringBuilder.
+     * <br>
+     * The RefEntry elements are separated with a marker:
+     * {@code @@@scriptName + "-" + subCommand.getName() + @@@}.
+     *
+     * @param builder       Append the RefEntry elements to this.
+     * @param scriptName    The name of the tool with subcommands.
+     * @param subCommands   SubCommands containing reference information.
+     */
+    private void appendSubCommandPages(StringBuilder builder, String scriptName, Collection<SubCommand> subCommands) {
+        for (SubCommand subCommand : subCommands) {
+            if (subCommand.isHidden()) {
+                continue;
+            }
+            Map<String, Object> map = new HashMap<>();
+            map.put("marker", "@@@" + scriptName + "-" + subCommand.getName() + "@@@");
+            map.put("locale", Locale.getDefault().getLanguage());
+            map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+            map.put("id", scriptName + "-" + subCommand.getName());
+            map.put("name", scriptName + " " + subCommand.getName());
+            map.put("purpose", eolToNewPara(subCommand.getDescription()));
+            map.put("args", INFO_SUBCMDPARSER_OPTIONS.get());
+            map.put("descTitle", REF_TITLE_DESCRIPTION.get());
+            map.put("description", eolToNewPara(subCommand.getDescription()));
+            map.put("info", subCommand.getDocDescriptionSupplement());
+            map.put("optionsTitle", REF_TITLE_OPTIONS.get());
+            map.put("optionsIntro", REF_INTRO_OPTIONS.get(scriptName + " " + subCommand.getName()));
+            setSubCommandOptionsInfo(map, subCommand);
+            applyTemplate(builder, "dscfgSubcommand.ftl", map);
+        }
+        appendSubCommandReference(builder, scriptName, subCommands);
+    }
+
+    /**
+     * Appends a generated DocBook XML Reference element XIncluding subcommands.
+     *
+     * @param builder       Append the Reference element to this.
+     * @param scriptName    The name of the tool with subcommands.
+     * @param subCommands   SubCommands containing reference information.
+     */
+    private void appendSubCommandReference(StringBuilder builder,
+                                           String scriptName,
+                                           Collection<SubCommand> subCommands) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("marker", "@@@" + scriptName + "-subcommands-ref" + "@@@");
+        map.put("name", scriptName);
+        map.put("locale", Locale.getDefault().getLanguage());
+        map.put("title", REF_PART_TITLE_SUBCOMMANDS.get(scriptName));
+        map.put("partintro", REF_PART_INTRO_SUBCOMMANDS.get(scriptName));
+        List<Map<String, Object>> commands = new LinkedList<>();
+        for (SubCommand subCommand : subCommands) {
+            Map<String, Object> scMap = new HashMap<>();
+            scMap.put("id", scriptName + "-" + subCommand.getName());
+            commands.add(scMap);
+        }
+        map.put("subcommands", commands);
+        applyTemplate(builder, "dscfgReference.ftl", map);
+    }
+
+    @Override
+    public void replaceArgument(final Argument argument) {
+        replaceArgumentInCollections(globalLongIDMap, globalShortIDMap, globalArgumentList, argument);
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandUsageHandler.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandUsageHandler.java
new file mode 100644
index 0000000..ff34872
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandUsageHandler.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * A handler for printing sub-command usage information.
+ */
+//@FunctionalInterface
+public interface SubCommandUsageHandler {
+
+    /**
+     * Returns properties information for the sub-command.
+     *
+     * @param subCommand
+     *          the sub command for which to print usage information
+     * @return  The properties information for the sub-command.
+     */
+    String getProperties(SubCommand subCommand);
+
+    /**
+     * Returns additional information for the provided sub-command argument.
+     *
+     * @param subCommand
+     *          the sub command for which to print usage information
+     * @param arg
+     *          the argument for which to append additional information
+     * @param nameOption
+     *          the string representing the name option
+     * @return  The additional information for the sub-command argument.
+     */
+    String getArgumentAdditionalInfo(SubCommand subCommand, Argument arg, String nameOption);
+
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TabSeparatedTablePrinter.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TabSeparatedTablePrinter.java
new file mode 100644
index 0000000..ed42532
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TabSeparatedTablePrinter.java
@@ -0,0 +1,137 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * An interface for creating a tab-separated formatted table.
+ * <p>
+ * This table printer will replace any tab, line-feeds, or carriage return control characters encountered in a cell with
+ * a single space.
+ */
+public final class TabSeparatedTablePrinter extends TablePrinter {
+    /** Table serializer implementation. */
+    private final class Serializer extends TableSerializer {
+        /**
+         * Counts the number of separators that should be output the next time a non-empty cell is displayed. The tab
+         * separators are not displayed immediately so that we can avoid displaying unnecessary trailing separators.
+         */
+        private int requiredSeparators;
+
+        /** Private constructor. */
+        private Serializer() {
+            // No implementation required.
+        }
+
+        @Override
+        public void addCell(String s) {
+            // Avoid printing tab separators for trailing empty cells.
+            if (s.length() == 0) {
+                requiredSeparators++;
+            } else {
+                for (int i = 0; i < requiredSeparators; i++) {
+                    writer.print('\t');
+                }
+                requiredSeparators = 1;
+            }
+
+            // Replace all new-lines and tabs with a single space.
+            writer.print(s.replaceAll("[\\t\\n\\r]", " "));
+        }
+
+        @Override
+        public void addHeading(String s) {
+            if (displayHeadings) {
+                addCell(s);
+            }
+        }
+
+        @Override
+        public void endHeader() {
+            if (displayHeadings) {
+                writer.println();
+            }
+        }
+
+        @Override
+        public void endRow() {
+            writer.println();
+        }
+
+        @Override
+        public void endTable() {
+            writer.flush();
+        }
+
+        @Override
+        public void startHeader() {
+            requiredSeparators = 0;
+        }
+
+        @Override
+        public void startRow() {
+            requiredSeparators = 0;
+        }
+    }
+
+    /** Indicates whether or not the headings should be output. */
+    private boolean displayHeadings;
+
+    /** The output destination. */
+    private PrintWriter writer;
+
+    /**
+     * Creates a new tab separated table printer for the specified output stream. Headings will not be displayed by
+     * default.
+     *
+     * @param stream
+     *            The stream to output tables to.
+     */
+    public TabSeparatedTablePrinter(OutputStream stream) {
+        this(new BufferedWriter(new OutputStreamWriter(stream)));
+    }
+
+    /**
+     * Creates a new tab separated table printer for the specified writer. Headings will not be displayed by default.
+     *
+     * @param writer
+     *            The writer to output tables to.
+     */
+    public TabSeparatedTablePrinter(Writer writer) {
+        this.writer = new PrintWriter(writer);
+    }
+
+    /**
+     * Specify whether or not table headings should be displayed.
+     *
+     * @param displayHeadings
+     *            <code>true</code> if table headings should be displayed.
+     */
+    public void setDisplayHeadings(boolean displayHeadings) {
+        this.displayHeadings = displayHeadings;
+    }
+
+    @Override
+    protected TableSerializer getSerializer() {
+        return new Serializer();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java
new file mode 100644
index 0000000..984b49e
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableBuilder.java
@@ -0,0 +1,319 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * A class which can be used to construct tables of information to be displayed in a terminal.
+ * Once built the table can be output using a {@link TableSerializer}.
+ */
+public final class TableBuilder {
+
+    /**
+     * The current column number in the current row where 0 represents
+     * the left-most column in the table.
+     */
+    private int column;
+
+    /** The current with of each column. */
+    private List<Integer> columnWidths = new ArrayList<>();
+
+    /** The list of column headings. */
+    private List<LocalizableMessage> header = new ArrayList<>();
+
+    /** The current number of rows in the table. */
+    private int height;
+
+    /** The list of table rows. */
+    private List<List<String>> rows = new ArrayList<>();
+
+    /** The linked list of sort keys comparators. */
+    private List<Comparator<String>> sortComparators = new ArrayList<>();
+
+    /** The linked list of sort keys. */
+    private List<Integer> sortKeys = new ArrayList<>();
+
+    /** The current number of columns in the table. */
+    private int width;
+
+    /**
+     * Creates a new table printer.
+     */
+    public TableBuilder() {
+        // No implementation required.
+    }
+
+    /**
+     * Adds a table sort key. The table will be sorted according to the case-insensitive string ordering of the cells in
+     * the specified column.
+     *
+     * @param column
+     *            The column which will be used as a sort key.
+     */
+    public void addSortKey(int column) {
+        addSortKey(column, String.CASE_INSENSITIVE_ORDER);
+    }
+
+    /**
+     * Adds a table sort key. The table will be sorted according to the provided string comparator.
+     *
+     * @param column
+     *            The column which will be used as a sort key.
+     * @param comparator
+     *            The string comparator.
+     */
+    public void addSortKey(int column, Comparator<String> comparator) {
+        sortKeys.add(column);
+        sortComparators.add(comparator);
+    }
+
+    /**
+     * Appends a new blank cell to the current row.
+     */
+    public void appendCell() {
+        appendCell("");
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided boolean value.
+     *
+     * @param value
+     *            The boolean value.
+     */
+    public void appendCell(boolean value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided byte value.
+     *
+     * @param value
+     *            The byte value.
+     */
+    public void appendCell(byte value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided char value.
+     *
+     * @param value
+     *            The char value.
+     */
+    public void appendCell(char value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided double value.
+     *
+     * @param value
+     *            The double value.
+     */
+    public void appendCell(double value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided float value.
+     *
+     * @param value
+     *            The float value.
+     */
+    public void appendCell(float value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided integer value.
+     *
+     * @param value
+     *            The boolean value.
+     */
+    public void appendCell(int value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided long value.
+     *
+     * @param value
+     *            The long value.
+     */
+    public void appendCell(long value) {
+        appendCell(String.valueOf(value));
+    }
+
+    /**
+     * Appends a new cell to the current row containing the provided object value.
+     *
+     * @param value
+     *            The object value.
+     */
+    public void appendCell(Object value) {
+        // Make sure that the first row has been created.
+        if (height == 0) {
+            startRow();
+        }
+
+        // Create the cell.
+        String s = String.valueOf(value);
+        rows.get(height - 1).add(s);
+        column++;
+
+        // Update statistics.
+        if (column > width) {
+            width = column;
+            columnWidths.add(s.length());
+        } else if (columnWidths.get(column - 1) < s.length()) {
+            columnWidths.set(column - 1, s.length());
+        }
+    }
+
+    /**
+     * Appends a new blank column heading to the header row.
+     */
+    public void appendHeading() {
+        appendHeading(LocalizableMessage.EMPTY);
+    }
+
+    /**
+     * Appends a new column heading to the header row.
+     *
+     * @param value
+     *            The column heading value.
+     */
+    public void appendHeading(LocalizableMessage value) {
+        header.add(value);
+
+        // Update statistics.
+        if (header.size() > width) {
+            width = header.size();
+            columnWidths.add(value.length());
+        } else if (columnWidths.get(header.size() - 1) < value.length()) {
+            columnWidths.set(header.size() - 1, value.length());
+        }
+    }
+
+    /**
+     * Gets the width of the current row.
+     *
+     * @return Returns the width of the current row.
+     */
+    public int getRowWidth() {
+        return column;
+    }
+
+    /**
+     * Gets the number of rows in table.
+     *
+     * @return Returns the number of rows in table.
+     */
+    public int getTableHeight() {
+        return height;
+    }
+
+    /**
+     * Gets the number of columns in table.
+     *
+     * @return Returns the number of columns in table.
+     */
+    public int getTableWidth() {
+        return width;
+    }
+
+    /**
+     * Prints the table in its current state using the provided table printer.
+     *
+     * @param printer
+     *            The table printer.
+     */
+    public void print(TablePrinter printer) {
+        // Create a new printer instance.
+        TableSerializer serializer = printer.getSerializer();
+
+        // First sort the table.
+        List<List<String>> sortedRows = new ArrayList<>(rows);
+
+        Comparator<List<String>> comparator = new Comparator<List<String>>() {
+            @Override
+            public int compare(List<String> row1, List<String> row2) {
+                for (int i = 0; i < sortKeys.size(); i++) {
+                    String cell1 = row1.get(sortKeys.get(i));
+                    String cell2 = row2.get(sortKeys.get(i));
+
+                    int rc = sortComparators.get(i).compare(cell1, cell2);
+                    if (rc != 0) {
+                        return rc;
+                    }
+                }
+
+                // Both rows are equal.
+                return 0;
+            }
+        };
+
+        Collections.sort(sortedRows, comparator);
+
+        // Now output the table.
+        serializer.startTable(height, width);
+        for (int i = 0; i < width; i++) {
+            serializer.addColumn(columnWidths.get(i));
+        }
+
+        // Column headings.
+        serializer.startHeader();
+        for (LocalizableMessage s : header) {
+            serializer.addHeading(s.toString());
+        }
+        serializer.endHeader();
+
+        // Table contents.
+        serializer.startContent();
+        for (List<String> row : sortedRows) {
+            serializer.startRow();
+
+            // Print each cell in the row, padding missing trailing cells.
+            for (int i = 0; i < width; i++) {
+                if (i < row.size()) {
+                    serializer.addCell(row.get(i));
+                } else {
+                    serializer.addCell("");
+                }
+            }
+
+            serializer.endRow();
+        }
+        serializer.endContent();
+        serializer.endTable();
+    }
+
+    /**
+     * Appends a new row to the table.
+     */
+    public void startRow() {
+        rows.add(new ArrayList<String>());
+        height++;
+        column = 0;
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java
new file mode 100644
index 0000000..13fef0e
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TablePrinter.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * An interface for incrementally configuring a table serializer. Once
+ * configured, the table printer can be used to create a new
+ * {@link TableSerializer} instance using the {@link #getSerializer()}
+ * method.
+ */
+public abstract class TablePrinter {
+
+    /**
+     * Creates a new abstract table printer.
+     */
+    protected TablePrinter() {
+        // No implementation required.
+    }
+
+    /**
+     * Creates a new table serializer based on the configuration of this table printer.
+     *
+     * @return Returns a new table serializer based on the configuration of this table printer.
+     */
+    protected abstract TableSerializer getSerializer();
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java
new file mode 100644
index 0000000..430233d
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TableSerializer.java
@@ -0,0 +1,124 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * An interface for serializing tables.
+ * <p>
+ * The default implementation for each method is to do nothing.
+ * Implementations must override methods as required.
+ */
+public abstract class TableSerializer {
+
+    /**
+     * Create a new table serializer.
+     */
+    protected TableSerializer() {
+        // No implementation required.
+    }
+
+    /**
+     * Prints a table cell.
+     *
+     * @param s
+     *            The cell contents.
+     */
+    public void addCell(String s) {
+        // Default implementation.
+    }
+
+    /**
+     * Defines a column in the table.
+     *
+     * @param width
+     *            The width of the column in characters.
+     */
+    public void addColumn(int width) {
+        // Default implementation.
+    }
+
+    /**
+     * Prints a column heading.
+     *
+     * @param s
+     *            The column heading.
+     */
+    public void addHeading(String s) {
+        // Default implementation.
+    }
+
+    /**
+     * Finish printing the table contents.
+     */
+    public void endContent() {
+        // Default implementation.
+    }
+
+    /**
+     * Finish printing the column headings.
+     */
+    public void endHeader() {
+        // Default implementation.
+    }
+
+    /**
+     * Finish printing the current row of the table.
+     */
+    public void endRow() {
+        // Default implementation.
+    }
+
+    /**
+     * Finish printing the table.
+     */
+    public void endTable() {
+        // Default implementation.
+    }
+
+    /**
+     * Prepare to start printing the table contents.
+     */
+    public void startContent() {
+        // Default implementation.
+    }
+
+    /**
+     * Prepare to start printing the column headings.
+     */
+    public void startHeader() {
+        // Default implementation.
+    }
+
+    /**
+     * Prepare to start printing a new row of the table.
+     */
+    public void startRow() {
+        // Default implementation.
+    }
+
+    /**
+     * Start a new table having the specified number of rows and columns.
+     *
+     * @param height
+     *            The number of rows in the table.
+     * @param width
+     *            The number of columns in the table.
+     */
+    public void startTable(int height, int width) {
+        // Default implementation.
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java
new file mode 100644
index 0000000..f7fce40
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/TextTablePrinter.java
@@ -0,0 +1,449 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2007-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.Utils.MAX_LINE_WIDTH;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An interface for creating a text based table.
+ * Tables have configurable column widths, padding, and column separators.
+ */
+public final class TextTablePrinter extends TablePrinter {
+    /** Table serializer implementation. */
+    private final class Serializer extends TableSerializer {
+        /**The real column widths taking into account size constraints but
+         not including padding or separators.*/
+        private final List<Integer> columnWidths = new ArrayList<>();
+
+        /** The cells in the current row. */
+        private final List<String> currentRow = new ArrayList<>();
+
+        /** Width of the table in columns. */
+        private int totalColumns;
+
+        /** The padding to use for indenting the table. */
+        private final String indentPadding;
+
+        /** Private constructor. */
+        private Serializer() {
+            // Compute the indentation padding.
+            final StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < indentWidth; i++) {
+                builder.append(' ');
+            }
+            this.indentPadding = builder.toString();
+        }
+
+        @Override
+        public void addCell(String s) {
+            currentRow.add(s);
+        }
+
+        @Override
+        public void addColumn(int width) {
+            columnWidths.add(width);
+            totalColumns++;
+        }
+
+        @Override
+        public void addHeading(String s) {
+            if (displayHeadings) {
+                addCell(s);
+            }
+        }
+
+        @Override
+        public void endHeader() {
+            if (displayHeadings) {
+                endRow();
+
+                // Print the header separator.
+                final StringBuilder builder = new StringBuilder(indentPadding);
+                for (int i = 0; i < totalColumns; i++) {
+                    int width = columnWidths.get(i);
+                    if (totalColumns > 1) {
+                        if (i == 0 || i == (totalColumns - 1)) {
+                            // Only one lot of padding for first and last columns.
+                            width += padding;
+                        } else {
+                            width += padding * 2;
+                        }
+                    }
+
+                    for (int j = 0; j < width; j++) {
+                        if (headingSeparatorStartColumn > 0) {
+                            if (i < headingSeparatorStartColumn) {
+                                builder.append(' ');
+                            } else if (i == headingSeparatorStartColumn && j < padding) {
+                                builder.append(' ');
+                            } else {
+                                builder.append(headingSeparator);
+                            }
+                        } else {
+                            builder.append(headingSeparator);
+                        }
+                    }
+
+                    if ((i >= headingSeparatorStartColumn) && i < (totalColumns - 1)) {
+                        builder.append(columnSeparator);
+                    }
+                }
+                writer.println(builder.toString());
+            }
+        }
+
+        @Override
+        public void endRow() {
+            boolean isRemainingText;
+            do {
+                StringBuilder builder = new StringBuilder(indentPadding);
+                isRemainingText = false;
+                for (int i = 0; i < currentRow.size(); i++) {
+                    int width = columnWidths.get(i);
+                    String contents = currentRow.get(i);
+
+                    // Determine what parts of contents can be displayed on this line.
+                    String head;
+                    String tail = null;
+
+                    if (contents == null) {
+                        // This cell has been displayed fully.
+                        head = "";
+                    } else if (contents.length() > width) {
+                        // We're going to have to split the cell on next word boundary.
+                        int endIndex = contents.lastIndexOf(' ', width);
+                        if (endIndex == -1) {
+                            endIndex = width;
+                            head = contents.substring(0, endIndex);
+                            tail = contents.substring(endIndex);
+                        } else {
+                            head = contents.substring(0, endIndex);
+                            tail = contents.substring(endIndex + 1);
+                        }
+                    } else {
+                        // The contents fits ok.
+                        head = contents;
+                    }
+
+                    // Add this cell's contents to the current line.
+                    if (i > 0) {
+                        // Add right padding for previous cell.
+                        for (int j = 0; j < padding; j++) {
+                            builder.append(' ');
+                        }
+
+                        // Add separator.
+                        builder.append(columnSeparator);
+
+                        // Add left padding for this cell.
+                        for (int j = 0; j < padding; j++) {
+                            builder.append(' ');
+                        }
+                    }
+
+                    // Add cell contents.
+                    builder.append(head);
+
+                    // Now pad with extra space to make up the width.
+                    // Only if it's not the last cell (see issue #3210)
+                    if (i != currentRow.size() - 1) {
+                        for (int j = head.length(); j < width; j++) {
+                            builder.append(' ');
+                        }
+                    }
+
+                    // Update the row contents.
+                    currentRow.set(i, tail);
+                    if (tail != null) {
+                        isRemainingText = true;
+                    }
+                }
+
+                // Output the line.
+                writer.println(builder.toString());
+            } while (isRemainingText);
+        }
+
+        @Override
+        public void endTable() {
+            writer.flush();
+        }
+
+        @Override
+        public void startHeader() {
+            determineColumnWidths();
+            currentRow.clear();
+        }
+
+        @Override
+        public void startRow() {
+            currentRow.clear();
+        }
+
+        /** We need to calculate the effective width of each column. */
+        private void determineColumnWidths() {
+            // First calculate the minimum width so that we know how much
+            // expandable columns can expand.
+            int minWidth = indentWidth;
+            int expandableColumnSize = 0;
+
+            for (int i = 0; i < totalColumns; i++) {
+                int actualSize = columnWidths.get(i);
+
+                if (fixedColumns.containsKey(i)) {
+                    int requestedSize = fixedColumns.get(i);
+
+                    if (requestedSize == 0) {
+                        expandableColumnSize += actualSize;
+                    } else {
+                        columnWidths.set(i, requestedSize);
+                        minWidth += requestedSize;
+                    }
+                } else {
+                    minWidth += actualSize;
+                }
+
+                // Must also include padding and separators.
+                if (i > 0) {
+                    minWidth += padding * 2 + columnSeparator.length();
+                }
+            }
+
+            if (minWidth > totalWidth) {
+                // The table is too big: leave expandable columns at their
+                // requested width, as there's not much else that can be done.
+            } else {
+                int available = totalWidth - minWidth;
+
+                if (expandableColumnSize > available) {
+                    // Only modify column sizes if necessary.
+                    for (int i = 0; i < totalColumns; i++) {
+                        int actualSize = columnWidths.get(i);
+
+                        if (fixedColumns.containsKey(i)) {
+                            int requestedSize = fixedColumns.get(i);
+                            if (requestedSize == 0) {
+                                // Calculate size based on requested actual size as a
+                                // proportion of the total.
+                                requestedSize = (actualSize * available) / expandableColumnSize;
+                                columnWidths.set(i, requestedSize);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * The default string which should be used to separate one column from the next (not including padding).
+     */
+    private static final String DEFAULT_COLUMN_SEPARATOR = "";
+
+    /**
+     * The default character which should be used to separate the table heading row from the rows beneath.
+     */
+    private static final char DEFAULT_HEADING_SEPARATOR = '-';
+
+    /**
+     * The default padding which will be used to separate a cell's contents from its adjacent column separators.
+     */
+    private static final int DEFAULT_PADDING = 1;
+
+    /**
+     * The string which should be used to separate one column
+     * from the next (not including padding).
+     */
+    private String columnSeparator = DEFAULT_COLUMN_SEPARATOR;
+
+    /** Indicates whether or not the headings should be output. */
+    private boolean displayHeadings = true;
+
+    /** Table indicating whether or not a column is fixed width. */
+    private final Map<Integer, Integer> fixedColumns = new HashMap<>();
+
+    /** The number of characters the table should be indented. */
+    private int indentWidth;
+
+    /** The character which should be used to separate the table heading row from the rows beneath. */
+    private char headingSeparator = DEFAULT_HEADING_SEPARATOR;
+
+    /** The column where the heading separator should begin. */
+    private int headingSeparatorStartColumn;
+
+    /**
+     * The padding which will be used to separate a cell's
+     * contents from its adjacent column separators.
+     */
+    private int padding = DEFAULT_PADDING;
+
+    /** Total permitted width for the table which expandable columns can use up. */
+    private int totalWidth = MAX_LINE_WIDTH;
+
+    /** The output destination. */
+    private PrintWriter writer;
+
+    /**
+     * Creates a new text table printer for the specified output stream. The text table printer will have the following
+     * initial settings:
+     * <ul>
+     * <li>headings will be displayed
+     * <li>no separators between columns
+     * <li>columns are padded by one character
+     * </ul>
+     *
+     * @param stream
+     *            The stream to output tables to.
+     */
+    public TextTablePrinter(OutputStream stream) {
+        this(new BufferedWriter(new OutputStreamWriter(stream)));
+    }
+
+    /**
+     * Creates a new text table printer for the specified writer. The text table printer will have the following initial
+     * settings:
+     * <ul>
+     * <li>headings will be displayed
+     * <li>no separators between columns
+     * <li>columns are padded by one character
+     * </ul>
+     *
+     * @param writer
+     *            The writer to output tables to.
+     */
+    public TextTablePrinter(Writer writer) {
+        this.writer = new PrintWriter(writer);
+    }
+
+    /**
+     * Sets the column separator which should be used to separate one column from the next (not including padding).
+     *
+     * @param columnSeparator
+     *            The column separator.
+     */
+    public void setColumnSeparator(String columnSeparator) {
+        this.columnSeparator = columnSeparator;
+    }
+
+    /**
+     * Set the maximum width for a column. If a cell is too big to fit in its column then it will be wrapped.
+     *
+     * @param column
+     *            The column to make fixed width (0 is the first column).
+     * @param width
+     *            The width of the column (this should not include column separators or padding), or <code>0</code> to
+     *            indicate that this column should be expandable.
+     * @throws IllegalArgumentException
+     *             If column is less than 0.
+     */
+    public void setColumnWidth(int column, int width) {
+        if (column < 0) {
+            throw new IllegalArgumentException("Negative column " + column);
+        }
+
+        if (width < 0) {
+            throw new IllegalArgumentException("Negative width " + width);
+        }
+
+        fixedColumns.put(column, width);
+    }
+
+    /**
+     * Specify whether the column headings should be displayed or not.
+     *
+     * @param displayHeadings
+     *            <code>true</code> if column headings should be displayed.
+     */
+    public void setDisplayHeadings(boolean displayHeadings) {
+        this.displayHeadings = displayHeadings;
+    }
+
+    /**
+     * Sets the heading separator which should be used to separate the table heading row from the rows beneath.
+     *
+     * @param headingSeparator
+     *            The heading separator.
+     */
+    public void setHeadingSeparator(char headingSeparator) {
+        this.headingSeparator = headingSeparator;
+    }
+
+    /**
+     * Sets the heading separator start column. The heading separator will only be display in the specified column and
+     * all subsequent columns. Usually this should be left at zero (the default) but sometimes it useful to indent the
+     * heading separate in order to provide additional emphasis (for example in menus).
+     *
+     * @param startColumn
+     *            The heading separator start column.
+     */
+    public void setHeadingSeparatorStartColumn(int startColumn) {
+        if (startColumn < 0) {
+            throw new IllegalArgumentException("Negative start column " + startColumn);
+        }
+        this.headingSeparatorStartColumn = startColumn;
+    }
+
+    /**
+     * Sets the amount of characters that the table should be indented. By default the table is not indented.
+     *
+     * @param indentWidth
+     *            The number of characters the table should be indented.
+     * @throws IllegalArgumentException
+     *             If indentWidth is less than 0.
+     */
+    public void setIndentWidth(int indentWidth) {
+        if (indentWidth < 0) {
+            throw new IllegalArgumentException("Negative indentation width " + indentWidth);
+        }
+
+        this.indentWidth = indentWidth;
+    }
+
+    /**
+     * Sets the padding which will be used to separate a cell's contents from its adjacent column separators.
+     *
+     * @param padding
+     *            The padding.
+     */
+    public void setPadding(int padding) {
+        this.padding = padding;
+    }
+
+    /**
+     * Sets the total permitted width for the table which expandable columns can use up.
+     *
+     * @param totalWidth
+     *            The total width.
+     */
+    public void setTotalWidth(int totalWidth) {
+        this.totalWidth = totalWidth;
+    }
+
+    @Override
+    protected TableSerializer getSerializer() {
+        return new Serializer();
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolRefDocContainer.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolRefDocContainer.java
new file mode 100644
index 0000000..8a6f35d
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolRefDocContainer.java
@@ -0,0 +1,80 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * An interface for an object that holds reference documentation for a command-line tool.
+ */
+public interface ToolRefDocContainer {
+
+    /**
+     * Gets a short description for this tool, suitable in a man page summary line.
+     *
+     * @return  A short description for this tool,
+     *          suitable in a man page summary line,
+     *          or LocalizableMessage.EMPTY if there is no short description.
+     */
+    LocalizableMessage getShortToolDescription();
+
+    /**
+     * Sets a short description for this tool, suitable in a man page summary line.
+     *
+     * @param   shortDescription    The short description for this tool,
+     *                              suitable in a man page summary line.
+     */
+    void setShortToolDescription(final LocalizableMessage shortDescription);
+
+    /**
+     * Retrieves a supplement to the description for this tool,
+     * intended for use in generated reference documentation.
+     *
+     * @return A supplement to the description for this tool
+     *         intended for use in generated reference documentation,
+     *         or LocalizableMessage.EMPTY if there is no supplement.
+     */
+    LocalizableMessage getDocToolDescriptionSupplement();
+
+    /**
+     * Sets a supplement to the description for this tool,
+     * intended for use in generated reference documentation.
+     *
+     * @param docToolDescriptionSupplement  The supplement to the description for this tool
+     *                                      intended for use in generated reference documentation.
+     */
+    void setDocToolDescriptionSupplement(final LocalizableMessage docToolDescriptionSupplement);
+
+    /**
+     * Retrieves a supplement to the description for all subcommands of this tool,
+     * intended for use in generated reference documentation.
+     *
+     * @return A supplement to the description for all subcommands of this tool
+     *         intended for use in generated reference documentation,
+     *         or LocalizableMessage.EMPTY if there is no supplement.
+     */
+    LocalizableMessage getDocSubcommandsDescriptionSupplement();
+
+    /**
+     * Sets a supplement to the description for all subcommands of this tool,
+     * intended for use in generated reference documentation.
+     *
+     * @param docSubcommandsDescriptionSupplement
+     *          The supplement to the description for all subcommands of this tool
+     *          intended for use in generated reference documentation.
+     */
+    void setDocSubcommandsDescriptionSupplement(final LocalizableMessage docSubcommandsDescriptionSupplement);
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
new file mode 100644
index 0000000..cbb713e
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ *  Copyright 2015-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+/** Class that prints the version of the SDK to System.out. */
+public final class ToolVersionHandler implements VersionHandler {
+
+    /**
+     * Returns a {@link VersionHandler} which should be used by OpenDJ SDK tools.
+     * <p>
+     * The printed version and SCM revision will be the one of the opendj-core module.
+     * @return A {@link VersionHandler} which should be used by OpenDJ SDK tools.
+     */
+    public static VersionHandler newSdkVersionHandler() {
+        return newToolVersionHandler("opendj-core");
+    }
+
+    /**
+     * Returns a {@link VersionHandler} which should be used to print version and SCM revision of a module.
+     * <p>
+     * The printed version and SCM revision will be read from the module MANIFEST.MF‌ file.
+     * @param moduleName
+     *       Name of the module which uniquely identify the URL of the MANIFEST‌.MF file.
+     * @return A {@link VersionHandler} which should be used by OpenDJ SDK tools.
+     */
+    public static VersionHandler newToolVersionHandler(final String moduleName) {
+        return new ToolVersionHandler(moduleName);
+    }
+
+    private final String moduleName;
+
+    private ToolVersionHandler(final String moduleName) {
+        this.moduleName = moduleName;
+    }
+
+    @Override
+    public void printVersion() {
+        System.out.println(getVersion());
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + getVersion() + ")";
+    }
+
+    private String getVersion() {
+        try {
+            final Enumeration<URL> manifests = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
+            while (manifests.hasMoreElements()) {
+                final URL manifestUrl = manifests.nextElement();
+                if (manifestUrl.toString().contains(moduleName)) {
+                    try (InputStream manifestStream = manifestUrl.openStream()) {
+                        final Attributes attrs = new Manifest(manifestStream).getMainAttributes();
+                        return attrs.getValue("Bundle-Version") + " (revision " + attrs.getValue("SCM-Revision") + ")";
+                    }
+                }
+            }
+            return null;
+        } catch (IOException e) {
+            throw new RuntimeException("IOException while determining opendj tool version", e);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
new file mode 100644
index 0000000..0bff591
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
@@ -0,0 +1,740 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+
+import com.forgerock.opendj.util.OperatingSystem;
+
+import static com.forgerock.opendj.util.StaticUtils.EOL;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+import javax.naming.AuthenticationException;
+import javax.naming.CommunicationException;
+import javax.naming.NamingException;
+import javax.naming.NamingSecurityException;
+import javax.naming.NoPermissionException;
+import javax.net.ssl.SSLHandshakeException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizableMessageDescriptor;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.RDN;
+
+/** This class provides utility functions for all the client side tools. */
+public final class Utils {
+
+    /** Platform appropriate line separator. */
+    public static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+    /**
+     * The value used to display arguments that must be obfuscated (such as passwords). This does not require
+     * localization (since the output of command builder by its nature is not localized).
+     */
+    public static final String OBFUSCATED_VALUE = "******";
+
+    /** The maximum number of times we try to confirm. */
+    public static final int CONFIRMATION_MAX_TRIES = 5;
+
+    /**
+     * The date format string that will be used to construct and parse dates represented using generalized time. It is
+     * assumed that the provided date formatter will be set to UTC.
+     */
+    public static final String DATE_FORMAT_LOCAL_TIME = "dd/MMM/yyyy:HH:mm:ss Z";
+
+    /**
+     * Returns the message to be displayed in the file with the equivalent command-line with information about the
+     * current time.
+     *
+     * @return the message to be displayed in the file with the equivalent command-line with information about the
+     *         current time.
+     */
+    public static String getCurrentOperationDateMessage() {
+        String date = formatDateTimeStringForEquivalentCommand(new Date());
+        return INFO_OPERATION_START_TIME_MESSAGE.get(date).toString();
+    }
+
+    private static final String COMMENT_SHELL_UNIX = "# ";
+    private static final String COMMENT_BATCH_WINDOWS = "rem ";
+
+    /** The String used to write comments in a shell (or batch) script. */
+    public static final String SHELL_COMMENT_SEPARATOR = OperatingSystem.isWindows() ? COMMENT_BATCH_WINDOWS
+            : COMMENT_SHELL_UNIX;
+
+    /** The column at which to wrap long lines of output in the command-line tools. */
+    public static final int MAX_LINE_WIDTH;
+    static {
+        int columns = 80;
+        try {
+            final String s = System.getenv("COLUMNS");
+            if (s != null) {
+                columns = Integer.parseInt(s);
+            }
+        } catch (final Exception e) {
+            // Do nothing.
+        }
+        MAX_LINE_WIDTH = columns - 1;
+    }
+
+    /**
+     * Formats a Date to String representation in "dd/MMM/yyyy:HH:mm:ss Z".
+     *
+     * @param date
+     *            The date to format; null if <code>date</code> is null.
+     * @return A string representation of the date.
+     */
+    public static String formatDateTimeStringForEquivalentCommand(final Date date) {
+        if (date != null) {
+            final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_LOCAL_TIME);
+            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+            return dateFormat.format(date);
+        }
+        return null;
+    }
+
+    /**
+     * Filters the provided value to ensure that it is appropriate for use as an
+     * exit code. Exit code values are generally only allowed to be between 0
+     * and 255, so any value outside of this range will be converted to 255,
+     * which is the typical exit code used to indicate an overflow value.
+     *
+     * @param exitCode
+     *            The exit code value to be processed.
+     * @return An integer value between 0 and 255, inclusive. If the provided
+     *         exit code was already between 0 and 255, then the original value
+     *         will be returned. If the provided value was out of this range,
+     *         then 255 will be returned.
+     */
+    public static int filterExitCode(final int exitCode) {
+        if (exitCode < 0) {
+            return 255;
+        } else if (exitCode > 255) {
+            return 255;
+        } else {
+            return exitCode;
+        }
+    }
+
+    /**
+     * Read the data from the specified file and return it in a byte array.
+     *
+     * @param filePath
+     *            The path to the file that should be read.
+     * @return A byte array containing the contents of the requested file.
+     * @throws IOException
+     *             If a problem occurs while trying to read the specified file.
+     */
+    public static byte[] readBytesFromFile(final String filePath) throws IOException {
+        final File file = new File(filePath);
+        final long length = file.length();
+        try (FileInputStream fis = new FileInputStream(file)) {
+            byte[] val = new byte[(int) length];
+            // Read in the bytes
+            int offset = 0;
+            int numRead = 0;
+            while (offset < val.length
+                    && (numRead = fis.read(val, offset, val.length - offset)) >= 0) {
+                offset += numRead;
+            }
+
+            // Ensure all the bytes have been read in
+            if (offset < val.length) {
+                throw new IOException("Could not completely read file " + filePath);
+            }
+
+            return val;
+        }
+    }
+
+    /**
+     * Retrieves a user-friendly string that indicates the length of time (in
+     * days, hours, minutes, and seconds) in the specified number of seconds.
+     *
+     * @param numSeconds
+     *            The number of seconds to be converted to a more user-friendly
+     *            value.
+     * @return The user-friendly representation of the specified number of
+     *         seconds.
+     */
+    public static LocalizableMessage secondsToTimeString(final int numSeconds) {
+        if (numSeconds < 60) {
+            // We can express it in seconds.
+            return INFO_TIME_IN_SECONDS.get(numSeconds);
+        } else if (numSeconds < 3600) {
+            // We can express it in minutes and seconds.
+            final int m = numSeconds / 60;
+            final int s = numSeconds % 60;
+            return INFO_TIME_IN_MINUTES_SECONDS.get(m, s);
+        } else if (numSeconds < 86400) {
+            // We can express it in hours, minutes, and seconds.
+            final int h = numSeconds / 3600;
+            final int m = (numSeconds % 3600) / 60;
+            final int s = numSeconds % 3600 % 60;
+            return INFO_TIME_IN_HOURS_MINUTES_SECONDS.get(h, m, s);
+        } else {
+            // We can express it in days, hours, minutes, and seconds.
+            final int d = numSeconds / 86400;
+            final int h = (numSeconds % 86400) / 3600;
+            final int m = (numSeconds % 86400 % 3600) / 60;
+            final int s = numSeconds % 86400 % 3600 % 60;
+            return INFO_TIME_IN_DAYS_HOURS_MINUTES_SECONDS.get(d, h, m, s);
+        }
+    }
+
+    /**
+     * Inserts line breaks into the provided buffer to wrap text at no more than
+     * the specified column width. Wrapping will only be done at space
+     * boundaries and if there are no spaces within the specified width, then
+     * wrapping will be performed at the first space after the specified column.
+     *
+     * @param message
+     *            The message to be wrapped.
+     * @param width
+     *            The maximum number of characters to allow on a line if there
+     *            is a suitable breaking point.
+     * @return The wrapped text.
+     */
+    public static String wrapText(final LocalizableMessage message, final int width) {
+        return wrapText(message.toString(), width, 0);
+    }
+
+    /**
+     * Inserts line breaks into the provided buffer to wrap text at no more than
+     * the specified column width. Wrapping will only be done at space
+     * boundaries and if there are no spaces within the specified width, then
+     * wrapping will be performed at the first space after the specified column.
+     * In addition each line will be indented by the specified amount.
+     *
+     * @param message
+     *            The message to be wrapped.
+     * @param width
+     *            The maximum number of characters to allow on a line if there
+     *            is a suitable breaking point (including any indentation).
+     * @param indent
+     *            The number of columns to indent each line.
+     * @return The wrapped text.
+     */
+    public static String wrapText(final LocalizableMessage message, final int width, final int indent) {
+        return wrapText(message.toString(), width, indent);
+    }
+
+    /**
+     * Inserts line breaks into the provided buffer to wrap text at no more than
+     * the specified column width. Wrapping will only be done at space
+     * boundaries and if there are no spaces within the specified width, then
+     * wrapping will be performed at the first space after the specified column.
+     *
+     * @param text
+     *            The text to be wrapped.
+     * @param width
+     *            The maximum number of characters to allow on a line if there
+     *            is a suitable breaking point.
+     * @return The wrapped text.
+     */
+    public static String wrapText(final String text, final int width) {
+        return wrapText(text, width, 0);
+    }
+
+    /**
+     * Inserts line breaks into the provided buffer to wrap text at no more than
+     * the specified column width. Wrapping will only be done at space
+     * boundaries and if there are no spaces within the specified width, then
+     * wrapping will be performed at the first space after the specified column.
+     * In addition each line will be indented by the specified amount.
+     *
+     * @param text
+     *            The text to be wrapped.
+     * @param width
+     *            The maximum number of characters to allow on a line if there
+     *            is a suitable breaking point (including any indentation).
+     * @param indent
+     *            The number of columns to indent each line.
+     * @return The wrapped text.
+     */
+    public static String wrapText(final String text, int width, final int indent) {
+        if (text == null) {
+            return "";
+        }
+
+        // Calculate the real width and indentation padding.
+        width -= indent;
+        final StringBuilder pb = new StringBuilder();
+        for (int i = 0; i < indent; i++) {
+            pb.append(' ');
+        }
+        final String padding = pb.toString();
+
+        final StringBuilder buffer = new StringBuilder();
+        final StringTokenizer lineTokenizer = new StringTokenizer(text, "\r\n", true);
+        while (lineTokenizer.hasMoreTokens()) {
+            final String line = lineTokenizer.nextToken();
+            if ("\r".equals(line) || "\n".equals(line)) {
+                // It's an end-of-line character, so append it as-is.
+                buffer.append(line);
+            } else if (line.length() <= width) {
+                // The line fits in the specified width, so append it as-is.
+                buffer.append(padding);
+                buffer.append(line);
+            } else {
+                // The line doesn't fit in the specified width, so it needs
+                // to be wrapped. Do so at space boundaries.
+                StringBuilder lineBuffer = new StringBuilder();
+                StringBuilder delimBuffer = new StringBuilder();
+                final StringTokenizer wordTokenizer = new StringTokenizer(line, " ", true);
+                while (wordTokenizer.hasMoreTokens()) {
+                    final String word = wordTokenizer.nextToken();
+                    if (" ".equals(word)) {
+                        // It's a space, so add it to the delim buffer only
+                        // if the line buffer is not empty.
+                        if (lineBuffer.length() > 0) {
+                            delimBuffer.append(word);
+                        }
+                    } else if (word.length() > width) {
+                        // This is a long word that can't be wrapped,
+                        // so we'll just have to make do.
+                        if (lineBuffer.length() > 0) {
+                            buffer.append(padding).append(lineBuffer).append(EOL);
+                            lineBuffer = new StringBuilder();
+                        }
+                        buffer.append(padding);
+                        buffer.append(word);
+
+                        if (wordTokenizer.hasMoreTokens()) {
+                            // The next token must be a space, so remove it.
+                            // If there are still more tokens after that, then append an EOL.
+                            wordTokenizer.nextToken();
+                            if (wordTokenizer.hasMoreTokens()) {
+                                buffer.append(EOL);
+                            }
+                        }
+
+                        if (delimBuffer.length() > 0) {
+                            delimBuffer = new StringBuilder();
+                        }
+                    } else {
+                        // It's not a space, so see if we can fit it on the current line.
+                        final int newLineLength =
+                                lineBuffer.length() + delimBuffer.length() + word.length();
+                        if (newLineLength < width) {
+                            // It does fit on the line, so add it.
+                            lineBuffer.append(delimBuffer).append(word);
+
+                            if (delimBuffer.length() > 0) {
+                                delimBuffer = new StringBuilder();
+                            }
+                        } else {
+                            // It doesn't fit on the line, so end the
+                            // current line and start a new one.
+                            buffer.append(padding).append(lineBuffer).append(EOL);
+
+                            lineBuffer = new StringBuilder();
+                            lineBuffer.append(word);
+
+                            if (delimBuffer.length() > 0) {
+                                delimBuffer = new StringBuilder();
+                            }
+                        }
+                    }
+                }
+
+                // If there's anything left in the line buffer, then add it
+                // to the final buffer.
+                buffer.append(padding);
+                buffer.append(lineBuffer);
+            }
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Checks the java version.
+     *
+     * @throws ClientException
+     *             If the java version we are running on is not compatible.
+     */
+    public static void checkJavaVersion() throws ClientException {
+        final String version = System.getProperty("java.specification.version");
+        if (Float.valueOf(version) < CliConstants.MINIMUM_JAVA_VERSION) {
+            final String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
+            throw new ClientException(ReturnCode.JAVA_VERSION_INCOMPATIBLE,
+                    ERR_INCOMPATIBLE_JAVA_VERSION.get(CliConstants.MINIMUM_JAVA_VERSION, version, javaBin), null);
+        }
+    }
+
+    /**
+     * Returns the default host name.
+     *
+     * @return The default host name or empty string if the host name cannot be resolved.
+     */
+    public static String getDefaultHostName() {
+        try {
+            return InetAddress.getLocalHost().getHostName();
+        } catch (UnknownHostException e) {
+            // Fails.
+        }
+        String host = System.getenv("COMPUTERNAME"); // Windows.
+        if (host != null) {
+            return host;
+        }
+        host = System.getenv("HOSTNAME"); // Unix.
+        if (host != null) {
+            return host;
+        }
+        return "";
+    }
+
+    /**
+     * Tells whether the provided Throwable was caused because of a problem with a certificate while trying to establish
+     * a connection.
+     *
+     * @param t
+     *            The Throwable to analyze.
+     * @return <CODE>true</CODE> if the provided Throwable was caused because of a problem with a certificate while
+     *         trying to establish a connection and <CODE>false</CODE> otherwise.
+     */
+    public static boolean isCertificateException(Throwable t) {
+        while (t != null) {
+            if (t instanceof SSLHandshakeException || t instanceof GeneralSecurityException) {
+                return true;
+            }
+            t = t.getCause();
+        }
+        return false;
+    }
+
+    /**
+     * Returns a message object for the given NamingException.
+     *
+     * @param ne
+     *            The NamingException.
+     * @param hostPort
+     *            The hostPort representation of the server we were contacting when the NamingException occurred.
+     * @return A message object for the given NamingException.
+     */
+    public static LocalizableMessage getMessageForException(NamingException ne, String hostPort) {
+        String arg;
+        if (ne.getLocalizedMessage() != null) {
+            arg = ne.getLocalizedMessage();
+        } else if (ne.getExplanation() != null) {
+            arg = ne.getExplanation();
+        } else {
+            arg = ne.toString(true);
+        }
+
+        if (Utils.isCertificateException(ne)) {
+            return INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER.get(hostPort, arg);
+        } else if (ne instanceof AuthenticationException) {
+            return INFO_CANNOT_CONNECT_TO_REMOTE_AUTHENTICATION.get(hostPort, arg);
+        } else if (ne instanceof NoPermissionException) {
+            return INFO_CANNOT_CONNECT_TO_REMOTE_PERMISSIONS.get(hostPort, arg);
+        } else if (ne instanceof NamingSecurityException) {
+            return INFO_CANNOT_CONNECT_TO_REMOTE_PERMISSIONS.get(hostPort, arg);
+        } else if (ne instanceof CommunicationException) {
+            return ERR_CANNOT_CONNECT_TO_REMOTE_COMMUNICATION.get(hostPort, arg);
+        } else {
+            return INFO_CANNOT_CONNECT_TO_REMOTE_GENERIC.get(hostPort, arg);
+        }
+    }
+
+    /**
+     * Returns a localized message for a given properties key an throwable.
+     *
+     * @param message
+     *            prefix
+     * @param t
+     *            the throwable for which we want to get a message.
+     * @return a localized message for a given properties key and throwable.
+     */
+    public static LocalizableMessage getThrowableMsg(final LocalizableMessage message, final Throwable t) {
+        LocalizableMessageDescriptor.Arg1<Object> tag;
+        if (isOutOfMemory(t)) {
+            tag = INFO_EXCEPTION_OUT_OF_MEMORY_DETAILS;
+        } else {
+            tag = INFO_EXCEPTION_DETAILS;
+        }
+        final LocalizableMessageBuilder mb = new LocalizableMessageBuilder(message);
+        String detail = t.toString();
+        if (detail != null) {
+            mb.append("  ").append(tag.get(detail));
+        }
+        return mb.toMessage();
+    }
+
+    /**
+     * Returns <CODE>true</CODE> if we can write on the provided path and <CODE>false</CODE> otherwise.
+     *
+     * @param path
+     *            the path.
+     * @return <CODE>true</CODE> if we can write on the provided path and <CODE>false</CODE> otherwise.
+     */
+    public static boolean canWrite(String path) {
+        final File file = new File(path);
+        if (file.exists()) {
+            return file.canWrite();
+        }
+        final File parentFile = file.getParentFile();
+        return parentFile != null && parentFile.canWrite();
+    }
+
+    /** Prevent instantiation. */
+    private Utils() {
+        // Do nothing.
+    }
+
+    /**
+     * Returns {@code true} if the the provided string is a DN and {@code false} otherwise.
+     *
+     * @param dn
+     *            The String we are analyzing.
+     * @return {@code true} if the the provided string is a DN and {@code false} otherwise.
+     */
+    public static boolean isDN(String dn) {
+        try {
+            DN.valueOf(dn);
+            return true;
+        } catch (Exception ex) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the DN of the global administrator for a given UID.
+     *
+     * @param uid
+     *            The UID to be used to generate the DN.
+     * @return The DN of the administrator for the given UID.
+     */
+    public static String getAdministratorDN(String uid) {
+        return RDN.valueOf("cn=" + uid) + ",cn=Administrators, cn=admin data";
+    }
+
+    /**
+     * Tells whether this throwable has been generated for an out of memory error or not.
+     *
+     * @param t
+     *            The throwable to analyze.
+     * @return {@code true} if the throwable was generated by an out of memory error and false otherwise.
+     */
+    private static boolean isOutOfMemory(Throwable t) {
+        boolean isOutOfMemory = false;
+        while (!isOutOfMemory && t != null) {
+            if (t instanceof OutOfMemoryError) {
+                isOutOfMemory = true;
+            } else if (t instanceof IOException) {
+                final String msg = t.toString();
+                if (msg != null) {
+                    isOutOfMemory = msg.contains("Not enough space");
+                }
+            }
+            t = t.getCause();
+        }
+        return isOutOfMemory;
+    }
+
+    /**
+     * Returns the string that can be used to represent a given host name in a LDAP URL.
+     * This method must be used when we have IPv6 addresses (the address in the LDAP URL
+     *  must be enclosed with brackets).
+     *  E.g:<pre>
+     *  -h "[2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]"
+     *  </pre>
+     *
+     * @param host
+     *            The host name.
+     * @return The String that can be used to represent a given host name in a LDAP URL.
+     */
+    public static String getHostNameForLdapUrl(String host) {
+        if (host != null && host.contains(":")) {
+            // Assume an IPv6 address has been specified and adds the brackets
+            // for the URL.
+            host = host.trim();
+            if (!host.startsWith("[")) {
+                host = "[" + host;
+            }
+            if (!host.endsWith("]")) {
+                host = host + "]";
+            }
+        }
+        return host;
+    }
+
+    /**
+     * Prints the provided string on the provided stream.
+     *
+     * @param stream
+     *            The stream to print the message.
+     * @param message
+     *            The message to print.
+     */
+    public static void printWrappedText(final PrintStream stream, final String message) {
+        if (stream != null && message != null && !message.isEmpty()) {
+            stream.println(wrapText(message, MAX_LINE_WIDTH));
+        }
+    }
+
+    /**
+     * Print the provided message on the provided stream.
+     *
+     * @param stream
+     *            The stream to print the message.
+     * @param message
+     *            The message to print.
+     */
+    public static void printWrappedText(final PrintStream stream, final LocalizableMessage message) {
+        printWrappedText(stream, message != null ? message.toString() : null);
+    }
+
+    /**
+     * Repeats the given {@link char} n times.
+     *
+     * @param charToRepeat
+     *      The {@link char} to repeat.
+     * @param length
+     *      The repetition count.
+     * @return The given {@link char} n times.
+     */
+    public static String repeat(final char charToRepeat, final int length) {
+        final char[] str = new char[length];
+        Arrays.fill(str, charToRepeat);
+        return new String(str);
+    }
+
+    /**
+     * Return a {@link ValidationCallback<Integer>} which can be used to validate a port number.
+     *
+     * @param defaultPort
+     *        The default value to suggest to the user.
+     * @return a {@link ValidationCallback<Integer>} which can be used to validate a port number.
+     */
+    public static ValidationCallback<Integer> portValidationCallback(final int defaultPort) {
+        return new ValidationCallback<Integer>() {
+            @Override
+            public Integer validate(ConsoleApplication app, String rawInput) throws ClientException {
+                final String input = rawInput.trim();
+                if (input.length() == 0) {
+                    return defaultPort;
+                }
+
+                try {
+                    int i = Integer.parseInt(input);
+                    if (i < 1 || i > 65535) {
+                        throw new NumberFormatException();
+                    }
+                    return i;
+                } catch (NumberFormatException e) {
+                    // Try again...
+                    app.println();
+                    app.println(ERR_BAD_PORT_NUMBER.get(input));
+                    app.println();
+                    return null;
+                }
+            }
+        };
+    }
+
+    /**
+     * Throws an {@link ArgumentException} if both provided {@link Argument} are presents in the command line arguments.
+     *
+     * @param arg1
+     *         The first {@link Argument} which should not be present if {@literal arg2} is.
+     * @param arg2
+     *         The second {@link Argument} which should not be present if {@literal arg1} is.
+     * @throws ArgumentException
+     *         If both provided {@link Argument} are presents in the command line arguments
+     */
+    public static void throwIfArgumentsConflict(final Argument arg1, final Argument arg2) throws ArgumentException {
+        if (argsConflicts(arg1, arg2)) {
+            throw new ArgumentException(conflictingArgsErrorMessage(arg1, arg2));
+        }
+    }
+
+    /**
+     * Adds a {@link LocalizableMessage} to the provided {@link Collection<LocalizableMessage>}
+     * if both provided {@link Argument} are presents in the command line arguments.
+     *
+     * @param errors
+     *         The {@link Collection<LocalizableMessage>} to use to add the conflict error (if occurs).
+     * @param arg1
+     *         The first {@link Argument} which should not be present if {@literal arg2} is.
+     * @param arg2
+     *         The second {@link Argument} which should not be present if {@literal arg1} is.
+     */
+    public static void addErrorMessageIfArgumentsConflict(
+            final Collection<LocalizableMessage> errors, final Argument arg1, final Argument arg2) {
+        if (argsConflicts(arg1, arg2)) {
+            errors.add(conflictingArgsErrorMessage(arg1, arg2));
+        }
+    }
+
+    /**
+     * Return {@code true} if provided {@link Argument} are presents in the command line arguments.
+     * <p>
+     * If so, adds a {@link LocalizableMessage} to the provided {@link LocalizableMessageBuilder}.
+     *
+     * @param builder
+     *         The {@link LocalizableMessageBuilder} to use to write the conflict error (if occurs).
+     * @param arg1
+     *         The first {@link Argument} which should not be present if {@literal arg2} is.
+     * @param arg2
+     *         The second {@link Argument} which should not be present if {@literal arg1} is.
+     * @return {@code true} if provided {@link Argument} are presents in the command line arguments.
+     */
+    public static boolean appendErrorMessageIfArgumentsConflict(
+        final LocalizableMessageBuilder builder, final Argument arg1, final Argument arg2) {
+        if (argsConflicts(arg1, arg2)) {
+            if (builder.length() > 0) {
+                builder.append(LINE_SEPARATOR);
+            }
+            builder.append(conflictingArgsErrorMessage(arg1, arg2));
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean argsConflicts(final Argument arg1, final Argument arg2) {
+        return arg1.isPresent() && arg2.isPresent();
+    }
+
+    /**
+     * Returns a {@link LocalizableMessage} which explains to the user
+     * that provided {@link Argument}s can not be used together on the command line.
+     *
+     * @param arg1
+     *         The first {@link Argument} which conflicts with {@literal arg2}.
+     * @param arg2
+     *         The second {@link Argument} which conflicts with {@literal arg1}.
+     * @return A {@link LocalizableMessage} which explains to the user that arguments
+     *         can not be used together on the command line.
+     */
+    public static LocalizableMessage conflictingArgsErrorMessage(final Argument arg1, final Argument arg2) {
+        return ERR_TOOL_CONFLICTING_ARGS.get(arg1.getLongIdentifier(), arg2.getLongIdentifier());
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java
new file mode 100644
index 0000000..6220950
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ValidationCallback.java
@@ -0,0 +1,43 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * An interface for validating user input.
+ *
+ * @param <T>
+ *            The type of the decoded input.
+ */
+public interface ValidationCallback<T> {
+
+    /**
+     * Validates and decodes the user-provided input. Implementations must validate
+     * <code>input</code> and return the decoded value if the input is acceptable.
+     * If the input is unacceptable, implementations must return
+     * <code>null</code> and output a user friendly error message to the provided
+     * application console.
+     *
+     * @param app
+     *            The console application.
+     * @param input
+     *            The user input to be validated.
+     * @return Returns the decoded input if the input is valid, or <code>null</code> if it is not.
+     * @throws ClientException
+     *             If an unexpected error occurred which prevented validation.
+     */
+    T validate(ConsoleApplication app, String input) throws ClientException;
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/VersionHandler.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/VersionHandler.java
new file mode 100644
index 0000000..6cc220f
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/VersionHandler.java
@@ -0,0 +1,27 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+/**
+ * A handler for printing product version.
+ */
+//@FunctionalInterface
+public interface VersionHandler {
+
+    /** Invoked when the version of the product should be printed. */
+    void printVersion();
+
+}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/package-info.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/package-info.java
new file mode 100644
index 0000000..c8e38fa
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+/**
+ * Classes implementing the OpenDJ CLI shared APIs.
+ */
+package com.forgerock.opendj.cli;
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
new file mode 100644
index 0000000..9b3c352
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli.properties
@@ -0,0 +1,463 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2014-2016 ForgeRock AS.
+
+ERR_ARG_NO_VALUE_PLACEHOLDER=The %s argument is configured to take \
+ a value but no value placeholder has been defined for it
+ERR_ARG_NO_INT_VALUE=The %s argument does not have any value that \
+ may be retrieved as an integer
+ERR_ARG_CANNOT_DECODE_AS_INT=The provided value "%s" for the %s \
+ argument cannot be decoded as an integer
+ERR_ARG_INT_MULTIPLE_VALUES=The %s argument has multiple values and \
+ therefore cannot be decoded as a single integer value
+ERR_INTARG_LOWER_BOUND_ABOVE_UPPER_BOUND=The %s argument \
+ configuration is invalid because the lower bound of %d is greater than the \
+ upper bound of %d
+ERR_INTARG_VALUE_BELOW_LOWER_BOUND=The provided %s value %d is \
+ unacceptable because it is below the lower bound of %d
+ERR_INTARG_VALUE_ABOVE_UPPER_BOUND=The provided %s value %d is \
+ unacceptable because it is above the upper bound of %d
+ERR_BOOLEANARG_NO_VALUE_ALLOWED=The provided %s value is \
+ unacceptable because Boolean arguments are never allowed to have values
+ERR_MCARG_VALUE_NOT_ALLOWED=The provided %s value %s is \
+ unacceptable because it is not included in the set of allowed values for that \
+ argument
+ERR_FILEARG_NO_SUCH_FILE=The file %s specified for argument %s does \
+ not exist
+ERR_FILEARG_CANNOT_VERIFY_FILE_EXISTENCE=An error occurred while \
+ trying to verify the existence of file %s specified for argument %s:  %s
+ERR_FILEARG_CANNOT_OPEN_FILE=An error occurred while trying to open \
+ file %s specified for argument %s for reading:  %s
+ERR_FILEARG_CANNOT_READ_FILE=An error occurred while trying to read \
+ from file %s specified for argument %s:  %s
+ERR_FILEARG_EMPTY_FILE=The file %s specified for argument %s exists \
+ but is empty
+ERR_ARGPARSER_DUPLICATE_SHORT_ID=Cannot add argument %s to the \
+ argument list because its short identifier -%s conflicts with the %s argument \
+ that has already been defined
+ERR_ARGPARSER_DUPLICATE_LONG_ID=Cannot add argument %s to the \
+ argument list because there is already one defined with the same identifier
+ERR_ARGPARSER_CANNOT_READ_PROPERTIES_FILE=An error occurred while \
+ attempting to read the contents of the argument properties file %s:  %s
+ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS=The provided set of \
+ command-line arguments contained too many unnamed trailing arguments.  The \
+ maximum number of allowed trailing arguments is %d
+ERR_ARGPARSER_LONG_ARG_WITHOUT_NAME=The provided argument "%s" is \
+ invalid because it does not include the argument name
+ERR_ARGPARSER_NO_ARGUMENT_WITH_LONG_ID=Argument --%s is not allowed \
+ for use with this program
+ERR_ARGPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID=Argument --%s \
+ requires a value but none was provided
+ERR_ARGPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID=The provided value \
+ "%s" for argument --%s is not acceptable:  %s
+ERR_ARGPARSER_NOT_MULTIVALUED_FOR_LONG_ID=The argument --%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_ARGPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE=A value was \
+ provided for argument --%s but that argument does not take a value
+ERR_ARGPARSER_INVALID_DASH_AS_ARGUMENT=The dash character by itself \
+ is invalid for use as an argument name
+ERR_ARGPARSER_NO_ARGUMENT_WITH_SHORT_ID=Argument -%s is not allowed \
+ for use with this program
+ERR_ARGPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID=Argument -%s \
+ requires a value but none was provided
+ERR_ARGPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID=The provided value \
+ "%s" for argument -%s is not acceptable:  %s
+ERR_ARGPARSER_NOT_MULTIVALUED_FOR_SHORT_ID=The argument -%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_ARGPARSER_CANT_MIX_ARGS_WITH_VALUES=The provided argument block \
+ '-%s%s' is illegal because the '%s' argument requires a value but is in the \
+ same block as at least one other argument that does not require a value
+ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT=Argument "%s" does not \
+ start with one or two dashes and unnamed trailing arguments are not allowed
+ERR_ARGPARSER_TOO_FEW_TRAILING_ARGUMENTS=At least %d unnamed \
+ trailing arguments are required in the argument list, but too few were \
+ provided
+ERR_ARGPARSER_NO_VALUE_FOR_REQUIRED_ARG=The argument %s is required \
+ to have a value but none was provided in the argument list and no default \
+ value is available
+INFO_TIME_IN_SECONDS=%d seconds
+INFO_TIME_IN_MINUTES_SECONDS=%d minutes, %d seconds
+INFO_TIME_IN_HOURS_MINUTES_SECONDS=%d hours, %d minutes, %d seconds
+INFO_TIME_IN_DAYS_HOURS_MINUTES_SECONDS=%d days, %d hours, %d minutes, %d \
+ seconds
+INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE=Command options:
+INFO_MENU_PROMPT_RETURN_TO_CONTINUE=Press RETURN to continue
+ERR_CONSOLE_INPUT_ERROR=The response could not be read from the console due to the following error: %s
+INFO_PROMPT_SINGLE_DEFAULT=%s [%s]:
+INFO_ARGPARSER_USAGE_JAVA_CLASSNAME=Usage:  java %s  {options}
+INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME=Usage:  %s  {options}
+INFO_ARGPARSER_USAGE_TRAILINGARGS={trailing-arguments}
+INFO_ARGPARSER_USAGE_DEFAULT_VALUE=Default value: %s
+INFO_SUBCMDPARSER_OPTIONS={options}
+INFO_GLOBAL_OPTIONS=Global Options:
+INFO_GLOBAL_OPTIONS_REFERENCE=See "%s --help"
+INFO_GLOBAL_HELP_REFERENCE=See "%s --help" to get more usage help
+INFO_SUBCMD_OPTIONS=SubCommand Options:
+INFO_ARGPARSER_USAGE=Usage:
+INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS={subcommand} {options}
+INFO_SUBCMDPARSER_SUBCMD_HELP_HEADING=To get the list of subcommands use:
+INFO_SUBCMDPARSER_SUBCMD_HEADING=Available subcommands:
+ERR_ARG_SUBCOMMAND_INVALID=Invalid subcommand
+INFO_SUBCMDPARSER_GLOBAL_HEADING=The global options are:
+#
+# Extension messages
+#
+#
+# Tools messages
+#
+INFO_DESCRIPTION_TRUSTALL=Trust all server SSL certificates
+INFO_DESCRIPTION_BINDDN=DN to use to bind to the server
+INFO_DESCRIPTION_BINDPASSWORD=Password to use to bind to \
+ the server. Use -w - to ensure that the command prompts for the password, \
+ rather than entering the password as a command argument
+INFO_DESCRIPTION_BINDPASSWORDFILE=Bind password file
+INFO_DESCRIPTION_ENCODING=Use the specified character set for \
+ command-line input
+INFO_DESCRIPTION_VERBOSE=Use verbose mode
+INFO_DESCRIPTION_KEYSTOREPATH=Certificate key store path
+INFO_DESCRIPTION_TRUSTSTOREPATH=Certificate trust store path
+INFO_DESCRIPTION_HOST=Directory server hostname or IP address
+INFO_DESCRIPTION_PORT=Directory server port number
+INFO_DESCRIPTION_SHOWUSAGE=Display this usage information
+INFO_DESCRIPTION_CONTROLS=Use a request control with the provided \
+ information
+INFO_DESCRIPTION_CONTINUE_ON_ERROR=Continue processing even if there are \
+ errors
+INFO_DESCRIPTION_USE_SSL=Use SSL for secure communication with the server
+INFO_DESCRIPTION_START_TLS=Use StartTLS to secure communication with the \
+ server
+INFO_DESCRIPTION_PROXYAUTHZID=Use the proxied authorization \
+ control with the given authorization ID
+INFO_DESCRIPTION_RESTART=Attempt to automatically restart the \
+ server once it has stopped
+INFO_SEARCH_DESCRIPTION_SEARCH_SCOPE=Search scope ('base', 'one', 'sub', \
+ or 'subordinate')
+ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM=The requested SASL mechanism \
+ "%s" is not supported by this client
+ERR_LDAPAUTH_SASL_AUTHID_REQUIRED=The "authid" SASL property is \
+ required for use with the %s mechanism
+INFO_DESCRIPTION_VERSION=LDAP protocol version number
+INFO_DESCRIPTION_NOOP=Show what would be done but do not perform any \
+ operation
+INFO_DESCRIPTION_REPORT_AUTHZID=Use the authorization identity control
+INFO_DESCRIPTION_USE_PWP_CONTROL=Use the password policy request control
+ERR_TOOL_CONFLICTING_ARGS=You may not provide both the --%s and \
+ the --%s arguments
+ERR_TOOL_SASLEXTERNAL_NEEDS_SSL_OR_TLS=SASL EXTERNAL \
+ authentication may only be requested if SSL or StartTLS is used
+ERR_TOOL_SASLEXTERNAL_NEEDS_KEYSTORE=SASL EXTERNAL authentication \
+ may only be used if a client certificate key store is specified
+INFO_DESCRIPTION_TRUSTSTOREPASSWORD=Certificate trust store PIN
+INFO_DESCRIPTION_TRUSTSTOREPASSWORD_FILE=Certificate trust store PIN file
+INFO_DESCRIPTION_PRODUCT_VERSION=Display Directory Server version \
+ information
+INFO_DESCRIPTION_SCRIPT_FRIENDLY=Use script-friendly mode
+ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL=ERROR:  Unable to perform SSL \
+ initialization:  %s
+ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION=ERROR:  The provided SASL \
+ option string "%s" could not be parsed in the form "name=value"
+INFO_LDAP_CONN_DESCRIPTION_SASLOPTIONS=SASL bind options
+INFO_DESCRIPTION_PROP_FILE_PATH=Path to the file containing default \
+  property values used for command line arguments
+INFO_DESCRIPTION_NO_PROP_FILE=No properties file will be \
+  used to get default command line argument values
+INFO_DESCRIPTION_GENERAL_ARGS=General options:
+INFO_DESCRIPTION_IO_ARGS=Utility input/output options:
+INFO_DESCRIPTION_LDAP_CONNECTION_ARGS=LDAP connection options:
+INFO_FILE_PLACEHOLDER={file}
+INFO_KEYSTOREPATH_PLACEHOLDER={keyStorePath}
+INFO_TRUSTSTOREPATH_PLACEHOLDER={trustStorePath}
+INFO_BINDPWD_FILE_PLACEHOLDER={bindPasswordFile}
+INFO_HOST_PLACEHOLDER={host}
+INFO_PORT_PLACEHOLDER={port}
+INFO_BASEDN_PLACEHOLDER={baseDN}
+INFO_BINDDN_PLACEHOLDER={bindDN}
+INFO_BINDPWD_PLACEHOLDER={bindPassword}
+INFO_KEYSTORE_PWD_PLACEHOLDER={keyStorePassword}
+INFO_TRUSTSTORE_PWD_FILE_PLACEHOLDER={path}
+INFO_TRUSTSTORE_PWD_PLACEHOLDER={trustStorePassword}
+INFO_NICKNAME_PLACEHOLDER={nickname}
+INFO_PROXYAUTHID_PLACEHOLDER={authzID}
+INFO_SASL_OPTION_PLACEHOLDER={name=value}
+INFO_PROTOCOL_VERSION_PLACEHOLDER={version}
+INFO_PROP_FILE_PATH_PLACEHOLDER={propertiesFilePath}
+INFO_NUM_ENTRIES_PLACEHOLDER={numEntries}
+INFO_LDAP_CONTROL_PLACEHOLDER={controloid[:criticality[:value|::b64value|:<filePath]]}
+INFO_ENCODING_PLACEHOLDER={encoding}
+INFO_SEARCH_SCOPE_PLACEHOLDER={searchScope}
+INFO_KEYSTORE_PWD_FILE_PLACEHOLDER={keyStorePasswordFile}
+INFO_PATH_PLACEHOLDER={path}
+INFO_CONFIGCLASS_PLACEHOLDER={configClass}
+INFO_CONFIGFILE_PLACEHOLDER={configFile}
+INFO_ADMINUID_PLACEHOLDER={adminUID}
+ERR_CANNOT_READ_TRUSTSTORE=Cannot access trust store '%s'.  Verify \
+ that the provided trust store exists and that you have read access rights to it
+ERR_CANNOT_READ_KEYSTORE=Cannot access key store '%s'.  Verify \
+ that the provided key store exists and that you have read access rights to it
+INFO_DESCRIPTION_ADMIN_PORT=Directory server administration port number
+INFO_DESCRIPTION_ADMIN_BINDDN=Administrator user bind DN
+INFO_ERROR_EMPTY_RESPONSE=ERROR: a response must be provided in order to continue
+ #
+ # MakeLDIF tool
+ #
+INFO_DESCRIPTION_QUIET=Use quiet mode
+INFO_DESCRIPTION_NO_PROMPT=Use non-interactive mode.  If data in \
+the command is missing, the user is not prompted and the tool will fail
+INFO_OPTION_ACCEPT_LICENSE=Automatically accepts the product license \
+(if present)
+#
+# Setup messages
+#
+INFO_ARGUMENT_DESCRIPTION_CLI=Use the command line install. \
+ If not specified the graphical interface will be launched.  The rest of the \
+ options (excluding help and version) will only be taken into account if this \
+ option is specified
+INFO_ARGUMENT_DESCRIPTION_BASEDN=Base DN for user \
+ information in the Directory Server.  Multiple base DNs may be provided by \
+ using this option multiple times
+INFO_ARGUMENT_DESCRIPTION_ADDBASE=Indicates whether to create the base \
+ entry in the Directory Server database
+INFO_LDIFFILE_PLACEHOLDER={ldifFile}
+INFO_REJECT_FILE_PLACEHOLDER={rejectFile}
+INFO_SKIP_FILE_PLACEHOLDER={skipFile}
+INFO_JMXPORT_PLACEHOLDER={jmxPort}
+INFO_ROOT_USER_DN_PLACEHOLDER={rootUserDN}
+INFO_ROOT_USER_PWD_PLACEHOLDER={rootUserPassword}
+INFO_ROOT_USER_PWD_FILE_PLACEHOLDER={rootUserPasswordFile}
+INFO_TIMEOUT_PLACEHOLDER={timeout}
+INFO_GENERAL_DESCRIPTION_REJECTED_FILE=Write rejected entries to the \
+ specified file
+INFO_GENERAL_DESCRIPTION_SKIPPED_FILE=Write skipped entries to the \
+ specified file
+INFO_SETUP_DESCRIPTION_SAMPLE_DATA=Specifies that the database should \
+ be populated with the specified number of sample entries
+INFO_ARGUMENT_DESCRIPTION_LDAPPORT=Port on which the \
+ Directory Server should listen for LDAP communication
+INFO_ARGUMENT_DESCRIPTION_ADMINCONNECTORPORT=Port on which the \
+ Administration Connector should listen for communication
+INFO_ARGUMENT_DESCRIPTION_SKIPPORT=Skip the check to determine whether \
+ the specified ports are usable
+INFO_ARGUMENT_DESCRIPTION_ROOTDN=DN for the initial root \
+ user for the Directory Server
+INFO_ARGUMENT_DESCRIPTION_ROOTPWFILE=Path to a file \
+ containing the password for the initial root user for the Directory Server
+ INFO_ARGUMENT_DESCRIPTION_ENABLE_WINDOWS_SERVICE=Enable the server to run \
+ as a Windows Service
+INFO_ARGUMENT_DESCRIPTION_LDAPSPORT=Port on which the \
+ Directory Server should listen for LDAPS communication.  The LDAPS port will \
+ be configured and SSL will be enabled only if this argument is explicitly \
+ specified
+INFO_ARGUMENT_DESCRIPTION_HOST_NAME=The fully-qualified directory server \
+ host name that will be used when generating self-signed \
+ certificates for LDAP SSL/StartTLS, the administration connector, and \
+ replication
+INFO_ARGUMENT_DESCRIPTION_USE_SELF_SIGNED_CERTIFICATE=Generate a \
+ self-signed certificate that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_USE_PKCS11=Use a certificate in a \
+ PKCS#11 token that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_USE_JAVAKEYSTORE=Path of a Java \
+ Key Store (JKS) containing a certificate to be used as the server certificate
+INFO_ARGUMENT_DESCRIPTION_USE_JCEKS=Path of a JCEKS containing a \
+ certificate to be used as the server certificate
+INFO_ARGUMENT_DESCRIPTION_USE_PKCS12=Path of a PKCS#12 key \
+ store containing the certificate that the server should use when accepting \
+ SSL-based connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_CERT_NICKNAME=Nickname of the \
+ certificate that the server should use when accepting SSL-based \
+ connections or performing StartTLS negotiation
+INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD=Certificate key store PIN.  \
+ A PIN is required when you specify to use an existing certificate \
+ as server certificate
+INFO_ARGUMENT_DESCRIPTION_KEYSTOREPASSWORD_FILE=Certificate key store \
+ PIN file.  A PIN is required when you specify to use an existing certificate \
+ as server certificate
+INFO_SETUP_DESCRIPTION_DO_NOT_START=Do not start the server when the \
+ configuration is completed
+INFO_SETUP_DESCRIPTION_ENABLE_STARTTLS=Enable StartTLS to allow \
+ secure communication with the server using the LDAP port
+#
+# SubCommandes messages
+#
+ERR_ARG_SUBCOMMAND_DUPLICATE_SUBCOMMAND=The argument parser already \
+ has a %s subcommand
+ERR_ARG_SUBCOMMAND_DUPLICATE_ARGUMENT_NAME=There are multiple \
+ arguments for subcommand %s with name %s
+ERR_ARG_SUBCOMMAND_ARGUMENT_GLOBAL_CONFLICT=Argument %s for \
+ subcommand %s conflicts with a global argument with the same name
+ERR_ARG_SUBCOMMAND_DUPLICATE_SHORT_ID=Argument %s for subcommand %s \
+ has a short identifier -%s that conflicts with that of argument %s
+ERR_ARG_SUBCOMMAND_ARGUMENT_SHORT_ID_GLOBAL_CONFLICT=Argument %s \
+ for subcommand %s has a short ID -%s that conflicts with that of global \
+ argument %s
+ERR_ARG_SUBCOMMAND_DUPLICATE_LONG_ID=Failed to add Argument %s for subcommand %s \
+ because there is already an argument with the same identifier for this subcommand
+ERR_ARG_SUBCOMMAND_ARGUMENT_LONG_ID_GLOBAL_CONFLICT=Failed to add Argument %s for \
+ subcommand %s because there is already a global argument defined with the \
+ same long identifier
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME=There is already another \
+ global argument named "%s"
+ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT=The argument name \
+ %s conflicts with the name of another argument associated with the %s \
+ subcommand
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_SHORT_ID=Short ID -%s for \
+ global argument %s conflicts with the short ID of another global argument %s
+ERR_SUBCMDPARSER_GLOBAL_ARG_SHORT_ID_CONFLICT=Short ID -%s for \
+ global argument %s conflicts with the short ID for the %s argument associated \
+ with subcommand %s
+ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID=Failed to add global argument \
+ %s because there is already one defined with the same long identifier
+ERR_SUBCMDPARSER_GLOBAL_ARG_LONG_ID_CONFLICT=Failed to add argument %s to \
+ subcommand %s because there is already one argument with the same long identifier \
+ associated to this subcommand.
+ERR_SUBCMDPARSER_LONG_ARG_WITHOUT_NAME=The provided command-line \
+ argument %s does not contain an argument name
+ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_LONG_ID=The provided \
+ argument --%s is not a valid global argument identifier
+ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_LONG_ID=The provided argument --%s \
+ is not a valid global or subcommand argument identifier
+ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID=Command-line \
+ argument --%s requires a value but none was given
+ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID=The provided value \
+ "%s" for argument --%s is not acceptable:  %s
+ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_LONG_ID=The argument --%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE=A value was \
+ provided for argument --%s but that argument does not take a value
+ERR_SUBCMDPARSER_INVALID_DASH_AS_ARGUMENT=The dash character by \
+ itself is invalid for use as an argument name
+ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID=The provided \
+ argument -%s is not a valid global argument identifier
+ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID=The provided argument \
+ -%s is not a valid global or subcommand argument identifier
+ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID=Argument -%s \
+ requires a value but none was provided
+ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID=The provided \
+ value "%s" for argument -%s is not acceptable:  %s
+ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_SHORT_ID=The argument -%s was \
+ included multiple times in the provided set of arguments but it does not \
+ allow multiple values
+ERR_SUBCMDPARSER_CANT_MIX_ARGS_WITH_VALUES=The provided argument \
+ block '-%s%s' is illegal because the '%s' argument requires a value but is in \
+ the same block as at least one other argument that does not require a value
+ERR_SUBCMDPARSER_INVALID_ARGUMENT=The provided argument "%s" is \
+ not recognized
+ERR_INCOMPATIBLE_JAVA_VERSION=The minimum Java version required is %s.%n%n\
+ The detected version is %s.%nThe binary detected is %s%n%nPlease set \
+ OPENDJ_JAVA_HOME to the root of a compatible Java installation or edit the \
+ java.properties file and then run the dsjavaproperties script to specify the \
+ java version to be used.
+INFO_DESCRIPTION_CONNECTION_TIMEOUT=Maximum length of time (in \
+ milliseconds) that can be taken to establish a connection.  Use '0' to \
+ specify no time out
+ERR_MENU_BAD_CHOICE_MULTI=Invalid response. Please enter one or \
+more valid menu options
+ERR_MENU_BAD_CHOICE_SINGLE=Invalid response. Please enter a valid \
+menu option
+ERR_MENU_BAD_CHOICE_MULTI_DUPE=The option "%s" was specified \
+more than once. Please enter one or more valid menu options
+INFO_MENU_PROMPT_MULTI_DEFAULT=Enter one or more choices separated by commas [%s]:
+INFO_MENU_PROMPT_MULTI=Enter one or more choices separated by commas:
+INFO_MENU_PROMPT_SINGLE_DEFAULT=Enter choice [%s]:
+INFO_MENU_PROMPT_SINGLE=Enter choice:
+INFO_MENU_PROMPT_CONFIRM=%s (%s / %s) [%s]:
+INFO_MENU_OPTION_HELP=help
+INFO_MENU_OPTION_HELP_KEY=?
+INFO_MENU_OPTION_CANCEL=cancel
+INFO_MENU_OPTION_CANCEL_KEY=c
+INFO_MENU_OPTION_QUIT=quit
+INFO_MENU_OPTION_QUIT_KEY=q
+INFO_MENU_NUMERIC_OPTION=%d)
+INFO_MENU_CHAR_OPTION=%c)
+INFO_MENU_OPTION_BACK=back
+INFO_MENU_OPTION_BACK_KEY=b
+INFO_GENERAL_NO=no
+INFO_GENERAL_YES=yes
+ERR_CONSOLE_APP_CONFIRM=Invalid response. Please enter \
+ "%s" or "%s"
+ERR_TRIES_LIMIT_REACHED=Input tries limit reached (%d)
+ERR_BAD_PORT_NUMBER=Invalid port number "%s". Please \
+  enter a valid port number between 1 and 65535
+INFO_OPERATION_START_TIME_MESSAGE=Operation date: %s
+INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER=Error reading data from \
+ server %s.  There is an error with the certificate presented by the \
+ server.\nDetails: %s
+INFO_CANNOT_CONNECT_TO_REMOTE_AUTHENTICATION=The provided credentials are not \
+ valid in server %s.  Details: %s
+INFO_CANNOT_CONNECT_TO_REMOTE_PERMISSIONS=You do not have enough access \
+ rights to read the configuration in %s. %nProvide credentials with enough \
+ rights.  Details: %s
+ERR_CANNOT_CONNECT_TO_REMOTE_COMMUNICATION=Could not connect to \
+ %s. Check that the server is running and that it is accessible \
+ from the local machine.  Details: %s
+INFO_CANNOT_CONNECT_TO_REMOTE_GENERIC=Could not connect to %s.  Check that the \
+ server is running and that the provided credentials are valid.%nError \
+ details:%n%s
+ERR_CONFIRMATION_TRIES_LIMIT_REACHED=Confirmation tries limit reached (%d)
+INFO_DESCRIPTION_DISPLAY_EQUIVALENT=Display the equivalent \
+ non-interactive argument in the standard output when this command is run in \
+ interactive mode
+INFO_DESCRIPTION_ADVANCED=Allows the configuration of advanced \
+ components and properties
+INFO_DESCRIPTION_REMOTE=Connect to a remote server
+INFO_DESCRIPTION_CONFIG_CLASS=The fully-qualified name of the Java class \
+ to use as the Directory Server configuration handler.  If this is not \
+ provided, then a default of org.opends.server.extensions.ConfigFileHandler \
+ will be used
+INFO_DESCRIPTION_CONFIG_FILE=Path to the Directory Server \
+ configuration file
+INFO_LDAPAUTH_PASSWORD_PROMPT=Password for user '%s':
+#
+# Uninstall messages
+#
+#
+# Connection messages
+#
+INFO_EXCEPTION_OUT_OF_MEMORY_DETAILS=Not enough memory to perform the \
+ operation.  Details: %s
+INFO_EXCEPTION_DETAILS=Details: %s
+INFO_LDAP_CONN_PROMPT_SECURITY_SERVER_CERTIFICATE=Server Certificate:
+INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE=%s
+INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION=Do you trust this server certificate?
+INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_NO=No
+INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_SESSION=Yes, for this session only
+INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_ALWAYS=Yes, also add it to a truststore
+INFO_LDAP_CONN_PROMPT_SECURITY_CERTIFICATE_DETAILS=View certificate details
+INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_USER_DN=User DN  : %s
+INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_VALIDITY=Validity : From '%s'%n             To '%s'
+INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_ISSUER=Issuer   : %s
+INFO_LDAP_CONN_HEADING_CONNECTION_PARAMETERS=>>>> Specify OpenDJ LDAP \
+  connection parameters
+ERR_ERROR_CANNOT_READ_PASSWORD=Unable to read password
+ERR_ERROR_CANNOT_READ_BIND_NAME=Unable to read bind name
+ERR_ERROR_CANNOT_READ_HOST_NAME=Cannot read the host name
+
+# Strings for generated reference documentation.
+REF_TITLE_DESCRIPTION=Description
+REF_TITLE_OPTIONS=Options
+REF_INTRO_OPTIONS=The <command>%s</command> command takes the following options:
+REF_DEFAULT=Default: %s
+REF_TITLE_SUBCOMMANDS=Subcommands
+REF_INTRO_SUBCOMMANDS=The <command>%s</command> command supports the following subcommands:
+REF_PART_TITLE_SUBCOMMANDS=%s Subcommands Reference
+REF_PART_INTRO_SUBCOMMANDS=This section covers <command>%s</command> subcommands.
+REF_DEFAULT_BACKEND_TYPE=Default: <literal>je</literal> for standard edition, \
+  <literal>pdb</literal> for OEM edition.
+
+# Supplements to descriptions for generated reference documentation.
+SUPPLEMENT_DESCRIPTION_CONTROLS=<xinclude:include href="variablelist-ldap-controls.xml" />
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ca_ES.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ca_ES.properties
new file mode 100644
index 0000000..c3458b9
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ca_ES.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=ERROR:  Potser nou heu introdu\u00eft ambd\u00f3s arguments %s i %s al mateix cop
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_de.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_de.properties
new file mode 100644
index 0000000..cc8cb1b
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_de.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=Sie haben m\u00f6glicherweise nicht beide Argumente --%s und --%s angegeben
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_es.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_es.properties
new file mode 100644
index 0000000..f019d98
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_es.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=No se pueden proporcionar los argumentos --%s y --%s conjuntamente
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_fr.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_fr.properties
new file mode 100644
index 0000000..9e9c37c
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_fr.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=Vous ne pouvez pas utiliser \u00e0 la fois les arguments --%s et --%s
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ja.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ja.properties
new file mode 100644
index 0000000..e7cb5c8
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ja.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=--%s \u5f15\u6570\u3068 --%s \u5f15\u6570\u306e\u4e21\u65b9\u306f\u6307\u5b9a\u3067\u304d\u307e\u305b\u3093
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ko.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ko.properties
new file mode 100644
index 0000000..1bf6f35
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_ko.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=--%s \uc778\uc218\uc640 --%s \uc778\uc218\ub97c \ubaa8\ub450 \uc81c\uacf5\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_pl.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_pl.properties
new file mode 100644
index 0000000..c0cc2e5
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_pl.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=B\u0141\u0104D\:  Nie mo\u017cesz poda\u0107 obydwu arument\u00f3w %s i %s w tym samym czasie
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_CN.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_CN.properties
new file mode 100644
index 0000000..573ed95
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_CN.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=\u4e0d\u80fd\u540c\u65f6\u63d0\u4f9b --%s \u548c --%s \u53c2\u6570
diff --git a/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_TW.properties b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_TW.properties
new file mode 100644
index 0000000..4785174
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/com/forgerock/opendj/cli/cli_zh_TW.properties
@@ -0,0 +1,17 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+ERR_TOOL_CONFLICTING_ARGS=\u60a8\u7121\u6cd5\u540c\u6642\u63d0\u4f9b --%s \u8207 --%s \u5f15\u6578
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgAppendProps.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgAppendProps.ftl
new file mode 100644
index 0000000..4d20a02
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgAppendProps.ftl
@@ -0,0 +1,24 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<refsect1 xml:id="${id}">
+  <title>${title}</title>
+
+  <para>
+   ${intro}
+  </para>
+
+  ${list}
+</refsect1>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListItem.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListItem.ftl
new file mode 100644
index 0000000..d4836c2
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListItem.ftl
@@ -0,0 +1,25 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<listitem>
+ <para>
+  <link
+   <#-- Link to the Reference. Change this if the pages move to another document. -->
+   xlink:href="reference#${id}"
+   xlink:role="http://docbook.org/xlink/role/olink"
+   xlink:show="new"
+  ><command>${name}</command></link>: ${description}
+ </para>
+</listitem>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListSubtypes.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListSubtypes.ftl
new file mode 100644
index 0000000..5789cbc
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgListSubtypes.ftl
@@ -0,0 +1,45 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<variablelist>
+
+ <para>
+   ${dependencies}
+ </para>
+
+ <para>
+   ${typesIntro}
+ </para>
+
+ <#list children as child>
+   <varlistentry>
+     <term>${child.name}</term>
+     <listitem>
+       <para>
+         ${child.default}
+       </para>
+
+       <para>
+         ${child.enabled}
+       </para>
+
+       <para>
+         ${child.link}
+       </para>
+     </listitem>
+   </varlistentry>
+ </#list>
+
+</variablelist>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgReference.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgReference.ftl
new file mode 100644
index 0000000..860b8fa
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgReference.ftl
@@ -0,0 +1,35 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+${marker}
+<reference xml:id="${name}-subcommands-ref"
+           xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${locale}"
+           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xsi:schemaLocation="http://docbook.org/ns/docbook
+                               http://docbook.org/xml/5.0/xsd/docbook.xsd"
+           xmlns:xinclude="http://www.w3.org/2001/XInclude">
+
+ <title>${title}</title>
+
+ <partintro>
+  <para>
+   ${partintro}
+  </para>
+ </partintro>
+
+ <#list subcommands as subcommand>
+ <xinclude:include href="man-${subcommand.id}.xml" />
+ </#list>
+</reference>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgSubcommand.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgSubcommand.ftl
new file mode 100644
index 0000000..85bd38f
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgSubcommand.ftl
@@ -0,0 +1,97 @@
+${marker}
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-${year} ForgeRock AS.
+-->
+<refentry xml:id="${id}"
+          xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${locale}"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://docbook.org/ns/docbook
+                              http://docbook.org/xml/5.0/xsd/docbook.xsd"
+          xmlns:xlink="http://www.w3.org/1999/xlink"
+          xmlns:xinclude="http://www.w3.org/2001/XInclude">
+
+ <info>
+  <copyright>
+   <year>2011-${year}</year>
+   <holder>ForgeRock AS.</holder>
+  </copyright>
+ </info>
+
+ <refmeta>
+  <refentrytitle>${name}</refentrytitle><manvolnum>1</manvolnum>
+  <refmiscinfo class="software">OpenDJ</refmiscinfo>
+  <refmiscinfo class="version">${r"${project.version}"}</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>${name}</refname>
+  <refpurpose>${purpose}</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>${name}</command>
+   <arg choice="plain">${args}</arg>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1 xml:id="${id}-description">
+  <title>${descTitle}</title>
+
+  <para>
+   ${description?ensure_ends_with(".")}
+  </para>
+
+  <#if info??>${info}</#if>
+ </refsect1>
+
+ <#if options??>
+ <refsect1 xml:id="${id}-options">
+  <title>${optionsTitle}</title>
+
+  <variablelist>
+   <para>
+    ${optionsIntro}
+   </para>
+
+   <#list options as option>
+   <varlistentry>
+    <term><option>${option.synopsis?xml}</option></term>
+    <listitem>
+     <para>
+      ${option.description?ensure_ends_with(".")}
+     </para>
+
+     <#if option.info??>
+       <#if option.info.usage??>${option.info.usage}</#if>
+
+       <#if option.info.default??>
+       <para>
+        ${option.info.default}
+       </para>
+       </#if>
+
+       <#if option.info.doc??>${option.info.doc}</#if>
+     </#if>
+    </listitem>
+   </varlistentry>
+   </#list>
+  </variablelist>
+ </refsect1>
+ </#if>
+
+ <#if propertiesInfo??>${propertiesInfo}</#if>
+</refentry>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVarListEntry.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVarListEntry.ftl
new file mode 100644
index 0000000..714e5d6
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVarListEntry.ftl
@@ -0,0 +1,21 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<varlistentry>
+  <term>${term}</term>
+  <listitem>
+    ${definition}
+  </listitem>
+</varlistentry>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVariableList.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVariableList.ftl
new file mode 100644
index 0000000..79cf12a
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/dscfgVariableList.ftl
@@ -0,0 +1,40 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<variablelist>
+
+<#list properties as property>
+
+  <varlistentry>
+    <term>${property.term}</term>
+    <listitem>
+      <variablelist>
+
+        <varlistentry>
+          <term>${property.descTitle}</term>
+          <listitem>
+            <para>
+              ${property.description}
+            </para>
+          </listitem>
+        </varlistentry>
+
+        ${property.list}
+      </variablelist>
+    </listitem>
+  </varlistentry>
+</#list>
+
+</variablelist>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/optionsRefSect1.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/optionsRefSect1.ftl
new file mode 100644
index 0000000..a07cc9b
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/optionsRefSect1.ftl
@@ -0,0 +1,51 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<refsect1 xml:id="${name}-options">
+  <title>${title}</title>
+
+  <para>
+   ${intro}
+  </para>
+
+  <#list groups as group>
+    <variablelist>
+    <#if group.description??>
+      <para>
+       ${group.description}
+      </para>
+    </#if>
+
+    <#list group.options as option>
+      <varlistentry>
+        <term><option>${option.synopsis?xml}</option></term>
+        <listitem>
+          <para>
+            ${option.description?ensure_ends_with(".")}
+          </para>
+
+          <#if option.default??>
+            <para>
+              ${option.default}
+            </para>
+          </#if>
+
+          <#if option.info??>${option.info}</#if>
+        </listitem>
+      </varlistentry>
+    </#list>
+    </variablelist>
+  </#list>
+</refsect1>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/refEntry.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/refEntry.ftl
new file mode 100644
index 0000000..fcc32bb
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/refEntry.ftl
@@ -0,0 +1,86 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-${year} ForgeRock AS.
+-->
+<refentry xml:id="${name}-1"
+          xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${locale}"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://docbook.org/ns/docbook
+                              http://docbook.org/xml/5.0/xsd/docbook.xsd"
+          xmlns:xlink="http://www.w3.org/1999/xlink"
+          xmlns:xinclude="http://www.w3.org/2001/XInclude">
+
+ <info>
+  <copyright>
+   <year>2011-${year}</year>
+   <holder>ForgeRock AS.</holder>
+  </copyright>
+ </info>
+
+ <refmeta>
+  <refentrytitle>${name}</refentrytitle><manvolnum>1</manvolnum>
+  <refmiscinfo class="software">OpenDJ</refmiscinfo>
+  <refmiscinfo class="version">${r"${project.version}"}</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>${name}</refname>
+  <refpurpose>${shortDesc}</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+  <cmdsynopsis>
+   <command>${name}</command>
+   <#if args??><arg choice="plain">${args}</arg></#if>
+  </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1 xml:id="${name}-description">
+   <title>${descTitle}</title>
+
+   <para>
+     ${description?ensure_ends_with(".")}
+   </para>
+
+   <#if info??>${info}</#if>
+ </refsect1>
+
+ <#if optionSection??>
+   ${optionSection}
+ </#if>
+
+ <#if subcommands??>
+   ${subcommands}
+ </#if>
+
+ <#if trailingSectionString??>
+   ${trailingSectionString}
+ </#if>
+</refentry>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/refSect1.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/refSect1.ftl
new file mode 100644
index 0000000..5166739
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/refSect1.ftl
@@ -0,0 +1,32 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<refsect1 xml:id="${name}-subcommands">
+  <title>${title}</title>
+
+  <#if info??>
+    ${info}
+  </#if>
+
+  <para>
+   ${intro}
+  </para>
+
+  <#if isItemizedList??><itemizedlist></#if>
+  <#list subcommands as subcommand>
+   ${subcommand}
+  </#list>
+  <#if isItemizedList??></itemizedlist></#if>
+</refsect1>
diff --git a/opendj-sdk/opendj-cli/src/main/resources/templates/refSect2.ftl b/opendj-sdk/opendj-cli/src/main/resources/templates/refSect2.ftl
new file mode 100644
index 0000000..4dcba4e
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/main/resources/templates/refSect2.ftl
@@ -0,0 +1,65 @@
+<#--
+ # The contents of this file are subject to the terms of the Common Development and
+ # Distribution License (the License). You may not use this file except in compliance with the
+ # License.
+ #
+ # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ # specific language governing permission and limitations under the License.
+ #
+ # When distributing Covered Software, include this CDDL Header Notice in each file and include
+ # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ # Header, with the fields enclosed by brackets [] replaced by your own identifying
+ # information: "Portions Copyright [year] [name of copyright owner]".
+ #
+ # Copyright 2015 ForgeRock AS.
+ #-->
+<refsect2 xml:id="${id}">
+  <title>${name}</title>
+
+  <para>
+   ${description?ensure_ends_with(".")}
+  </para>
+
+  <#if info??>${info}</#if>
+
+  <#if options??>
+    <refsect3 xml:id="${id}-options">
+      <title>${optionsTitle}</title>
+
+      <variablelist>
+        <para>
+         ${optionsIntro}
+        </para>
+
+        <#list options as option>
+
+          <varlistentry>
+            <term><option>${option.synopsis?xml}</option></term>
+            <listitem>
+             <para>
+               ${option.description?ensure_ends_with(".")}
+             </para>
+
+             <#if option.info??>
+               <#if option.info.usage??>${option.info.usage}</#if>
+
+               <#if option.info.default??>
+                  <para>
+                    ${option.info.default}
+                  </para>
+               </#if>
+
+               <#if option.info.doc??>${option.info.doc}</#if>
+             </#if>
+            </listitem>
+          </varlistentry>
+
+        </#list>
+      </variablelist>
+    </refsect3>
+  </#if>
+
+  <#if propertiesInfo??>
+    ${propertiesInfo}
+  </#if>
+</refsect2>
diff --git a/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/CliTestCase.java b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/CliTestCase.java
new file mode 100644
index 0000000..376e499
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/CliTestCase.java
@@ -0,0 +1,28 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.cli;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all CLI unit tests should extend.
+ */
+@Test(groups = { "precommit", "cli", "sdk" })
+public abstract class CliTestCase extends ForgeRockTestCase {
+
+}
diff --git a/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java
new file mode 100644
index 0000000..1f537e2
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/ConsoleApplicationTestCase.java
@@ -0,0 +1,195 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+/** Unit tests for the console application class. */
+@SuppressWarnings("javadoc")
+public class ConsoleApplicationTestCase extends CliTestCase {
+    final LocalizableMessage msg = LocalizableMessage.raw("Language is the source of misunderstandings.");
+    final LocalizableMessage msg2 = LocalizableMessage
+            .raw("If somebody wants a sheep, that is a proof that one exists.");
+
+    /** For test purposes only. */
+    private static class MockConsoleApplication extends ConsoleApplication {
+        private static ByteArrayOutputStream out;
+        private static ByteArrayOutputStream err;
+        private boolean verbose;
+        private boolean interactive;
+        private boolean quiet;
+
+        private MockConsoleApplication(PrintStream out, PrintStream err) {
+            super(out, err);
+        }
+
+        static MockConsoleApplication getDefault() {
+            out = new ByteArrayOutputStream();
+            final PrintStream psOut = new PrintStream(out);
+            err = new ByteArrayOutputStream();
+            final PrintStream psErr = new PrintStream(err);
+            return new MockConsoleApplication(psOut, psErr);
+        }
+
+        public String getOut() throws UnsupportedEncodingException {
+            return out.toString("UTF-8");
+        }
+
+        public String getErr() throws UnsupportedEncodingException {
+            return err.toString("UTF-8");
+        }
+
+        @Override
+        public boolean isVerbose() {
+            return verbose;
+        }
+
+        @Override
+        public boolean isInteractive() {
+            return interactive;
+        }
+
+        @Override
+        public boolean isQuiet() {
+            return quiet;
+        }
+
+        public void setVerbose(boolean v) {
+            verbose = v;
+        }
+
+        public void setInteractive(boolean inter) {
+            interactive = inter;
+        }
+
+        public void setQuiet(boolean q) {
+            quiet = q;
+        }
+
+        @Override
+        public boolean isMenuDrivenMode() {
+            return false;
+        }
+    }
+
+    @Test
+    public void testWriteLineInOutputStream() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+        ca.print(msg);
+        assertThat(ca.getOut()).contains(msg.toString());
+        assertThat(ca.getErr()).isEmpty();
+    }
+
+    @Test
+    public void testWriteLineInErrorStream() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+        ca.errPrintln(msg);
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).contains(msg.toString());
+    }
+
+    @Test
+    public void testWriteOutputStreamVerbose() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+        ca.printVerboseMessage(msg);
+        assertThat(ca.isVerbose()).isFalse();
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).isEmpty();
+        ca.setVerbose(true);
+        ca.printVerboseMessage(msg);
+        assertThat(ca.isVerbose()).isTrue();
+        assertThat(ca.getOut()).contains(msg.toString());
+        assertThat(ca.getErr()).isEmpty();
+    }
+
+    @Test
+    public void testWriteErrorStreamVerbose() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+        ca.errPrintVerboseMessage(msg);
+        assertThat(ca.isVerbose()).isFalse();
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).isEmpty();
+        ca.setVerbose(true);
+        ca.errPrintVerboseMessage(msg);
+        assertThat(ca.isVerbose()).isTrue();
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).contains(msg.toString());
+    }
+
+    /**
+     * In non interactive applications, standard messages should be displayed in the stdout(info) and errors to the
+     * stderr (warnings, errors).
+     *
+     * @throws UnsupportedEncodingException
+     */
+    @Test
+    public void testNonInteractiveApplicationShouldNotStdoutErrors() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+
+        assertFalse(ca.isInteractive());
+        ca.errPrintln(msg);
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).contains(msg.toString());
+        ca.println(msg2);
+        assertThat(ca.getOut()).contains(msg2.toString());
+        assertThat(ca.getErr()).doesNotContain(msg2.toString());
+    }
+
+    /**
+     * If an application is interactive, all messages should be redirect to the stdout. (info, warnings, errors).
+     *
+     * @throws UnsupportedEncodingException
+     */
+    @Test
+    public void testInteractiveApplicationShouldStdoutErrors() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+
+        assertFalse(ca.isInteractive());
+        ca.setInteractive(true);
+        assertTrue(ca.isInteractive());
+        ca.errPrintln(msg);
+        assertThat(ca.getOut()).contains(msg.toString());
+        assertThat(ca.getErr()).isEmpty();
+        ca.println(msg2);
+        assertThat(ca.getOut()).contains(msg2.toString());
+        assertThat(ca.getErr()).isEmpty();
+    }
+
+    /**
+     * In quiet mode, only the stderr should contain lines.
+     * @throws UnsupportedEncodingException
+     */
+    @Test
+    public void testQuietMode() throws UnsupportedEncodingException {
+        final MockConsoleApplication ca = MockConsoleApplication.getDefault();
+        ca.setQuiet(true);
+        assertTrue(ca.isQuiet());
+        ca.println(msg);
+        ca.errPrintln(msg2);
+        assertThat(ca.getOut()).isEmpty();
+        assertThat(ca.getErr()).contains(msg2.toString());
+        assertThat(ca.getErr()).doesNotContain(msg.toString());
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java
new file mode 100644
index 0000000..863fa2b
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java
@@ -0,0 +1,160 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.fest.assertions.Assertions;
+import org.forgerock.i18n.LocalizableMessage;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the SubCommand class.
+ */
+@SuppressWarnings("javadoc")
+public final class TestSubCommandArgumentParserTestCase extends CliTestCase {
+
+    private SubCommandArgumentParser parser;
+
+    /** First sub-command. */
+    private SubCommand sc1;
+    /** Second sub-command. */
+    private SubCommand sc2;
+
+    /**
+     * Create the sub-commands and parser.
+     *
+     * @throws Exception
+     *             If something unexpected happened.
+     */
+    @BeforeClass
+    public void setup() throws Exception {
+        parser = new SubCommandArgumentParser(getClass().getName(), LocalizableMessage.raw("test description"), true);
+
+        sc1 = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("sub-command1"));
+        sc2 = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                             LocalizableMessage.raw("sub-command2"));
+    }
+
+    /**
+     * Test the getSubCommand method.
+     *
+     * @throws Exception
+     *             If something unexpected happened.
+     */
+    @Test
+    public void testGetSubCommand() throws Exception {
+        Assert.assertSame(parser.getSubCommand("sub-command1"), sc1);
+        Assert.assertSame(parser.getSubCommand("sub-command2"), sc2);
+        Assert.assertNull(parser.getSubCommand("sub-command3"));
+    }
+
+    /**
+     * Provide valid command line args.
+     *
+     * @return Array of valid command line args.
+     */
+    @DataProvider(name = "validCommandLineArgs")
+    public Object[][] createValidCommandLineArgs() {
+        return new Object[][] {
+            { new String[] {}, null },
+            { new String[] { "sub-command1" }, sc1 },
+            { new String[] { "sub-command2", "one", "two" }, sc2 },
+            { new String[] { "sub-command2", "one", "two", "three" }, sc2 },
+            { new String[] { "sub-command2", "one", "two", "three", "four" }, sc2 }, };
+    }
+
+    /**
+     * Test the parseArguments method with valid args.
+     *
+     * @param args
+     *            The command line args.
+     * @param sc
+     *            The expected sub-command.
+     * @throws Exception
+     *             If something unexpected happened.
+     */
+    @Test(dataProvider = "validCommandLineArgs")
+    public void testParseArgumentsWithValidArgs(String[] args, SubCommand sc) throws Exception {
+        parser.parseArguments(args);
+
+        // Check the correct sub-command was parsed.
+        Assert.assertEquals(parser.getSubCommand(), sc);
+
+        // Check that the trailing arguments were retrieved correctly and
+        // in the right order.
+        if (args.length > 1) {
+            List<String> scargs = new ArrayList<>();
+            for (int i = 1; i < args.length; i++) {
+                scargs.add(args[i]);
+            }
+            Assert.assertEquals(parser.getTrailingArguments(), scargs);
+        } else {
+            Assert.assertTrue(parser.getTrailingArguments().isEmpty());
+        }
+    }
+
+    /**
+     * Provide invalid command line args.
+     *
+     * @return Array of invalid command line args.
+     */
+    @DataProvider(name = "invalidCommandLineArgs")
+    public Object[][] createInvalidCommandLineArgs() {
+        return new Object[][] {
+            { new String[] { "sub-command1", "one" } },
+            { new String[] { "sub-command1", "one", "two" } },
+            { new String[] { "sub-command2" } },
+            { new String[] { "sub-command2", "one" } },
+            { new String[] { "sub-command2", "one", "two", "three", "four", "five" } }, };
+    }
+
+    /**
+     * Test the parseArguments method with invalid args.
+     *
+     * @param args
+     *            The command line args.
+     * @throws Exception
+     *             If something unexpected happened.
+     */
+    @Test(dataProvider = "invalidCommandLineArgs", expectedExceptions = ArgumentException.class)
+    public void testParseArgumentsWithInvalidArgs(String[] args) throws Exception {
+        parser.parseArguments(args);
+    }
+
+    @DataProvider
+    public Object[][] indentAndWrapProvider() throws Exception {
+        final String eol = System.getProperty("line.separator");
+        return new Object[][] {
+            { "test1",                  5, " ", " test1" + eol },
+            { "test1 test2",            5, " ", " test1" + eol + " test2" + eol },
+            { "test1 test2test3",       5, " ", " test1" + eol + " test2test3" + eol },
+            { "test1 test2test3 test4", 5, " ", " test1" + eol + " test2test3" + eol + " test4" + eol },
+        };
+    }
+
+    @Test(dataProvider = "indentAndWrapProvider")
+    public void testIndentAndWrap(String text, int wrapColumn, String indent, String expected) {
+        final StringBuilder buffer = new StringBuilder();
+        SubCommandArgumentParser.indentAndWrap(indent, buffer, wrapColumn, LocalizableMessage.raw(text));
+        Assertions.assertThat(buffer.toString()).isEqualTo(expected);
+    }
+}
diff --git a/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandTestCase.java b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandTestCase.java
new file mode 100644
index 0000000..885a08f
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandTestCase.java
@@ -0,0 +1,249 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.util.Arrays;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import org.forgerock.i18n.LocalizableMessage;
+
+
+/**
+ * Unit tests for the SubCommand class.
+ */
+public final class TestSubCommandTestCase extends CliTestCase {
+
+    /**
+     * Tests that allowsTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testAllowsTrailingArgumentsFalse1() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("XXX"));
+        Assert.assertFalse(sc.allowsTrailingArguments());
+    }
+
+    /**
+     * Tests that allowsTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testAllowsTrailingArgumentsFalse2() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", false, 0, 0, null, LocalizableMessage.raw("XXX"));
+        Assert.assertFalse(sc.allowsTrailingArguments());
+    }
+
+    /**
+     * Tests that allowsTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testAllowsTrailingArgumentsTrue() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                LocalizableMessage.raw("XXX"));
+        Assert.assertTrue(sc.allowsTrailingArguments());
+    }
+
+    /**
+     * Tests that getMaxTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMaxTrailingArguments1() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMaxTrailingArguments(), 0);
+    }
+
+    /**
+     * Tests that getMaxTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMaxTrailingArguments2() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", false, 0, 0, null, LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMaxTrailingArguments(), 0);
+    }
+
+    /**
+     * Tests that getMaxTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMaxTrailingArguments3() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMaxTrailingArguments(), 4);
+    }
+
+    /**
+     * Tests that getMinTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMinTrailingArguments1() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMinTrailingArguments(), 0);
+    }
+
+    /**
+     * Tests that getMinTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMinTrailingArguments2() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", false, 0, 0, null, LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMinTrailingArguments(), 0);
+    }
+
+    /**
+     * Tests that getMinTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetMinTrailingArguments3() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getMinTrailingArguments(), 2);
+    }
+
+    /**
+     * Tests that getTrailingArgumentsDisplayName works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArgumentsDisplayName1() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("XXX"));
+        Assert.assertNull(sc.getTrailingArgumentsDisplayName());
+    }
+
+    /**
+     * Tests that getTrailingArgumentsDisplayName works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArgumentsDisplayName2() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", false, 0, 0, null, LocalizableMessage.raw("XXX"));
+        Assert.assertNull(sc.getTrailingArgumentsDisplayName());
+    }
+
+    /**
+     * Tests that getTrailingArgumentsDisplayName works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArgumentsDisplayName3() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                LocalizableMessage.raw("XXX"));
+        Assert.assertEquals(sc.getTrailingArgumentsDisplayName(), "args1 arg2 [arg3 arg4]");
+    }
+
+    /**
+     * Tests that getTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArguments1() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command1", LocalizableMessage.raw("XXX"));
+        parser.parseArguments(new String[] { "sub-command1" });
+        Assert.assertTrue(sc.getTrailingArguments().isEmpty());
+    }
+
+    /**
+     * Tests that getTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArguments2() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", false, 0, 0, null, LocalizableMessage.raw("XXX"));
+        parser.parseArguments(new String[] { "sub-command2" });
+        Assert.assertTrue(sc.getTrailingArguments().isEmpty());
+    }
+
+    /**
+     * Tests that getTrailingArguments works correctly.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurred.
+     */
+    @Test
+    public void testGetTrailingArguments3() throws Exception {
+        SubCommandArgumentParser parser = new SubCommandArgumentParser(getClass().getName(),
+                LocalizableMessage.raw("test description"), true);
+        SubCommand sc = new SubCommand(parser, "sub-command2", true, 2, 4, "args1 arg2 [arg3 arg4]",
+                LocalizableMessage.raw("XXX"));
+        parser.parseArguments(new String[] { "sub-command2", "arg1", "arg2", "arg3" });
+        Assert.assertEquals(sc.getTrailingArguments(), Arrays.asList(new String[] { "arg1", "arg2", "arg3" }));
+    }
+
+}
diff --git a/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/UtilsTestCase.java b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/UtilsTestCase.java
new file mode 100644
index 0000000..18401c0
--- /dev/null
+++ b/opendj-sdk/opendj-cli/src/test/java/com/forgerock/opendj/cli/UtilsTestCase.java
@@ -0,0 +1,84 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.cli;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertEquals;
+
+@SuppressWarnings("javadoc")
+public class UtilsTestCase extends CliTestCase {
+
+    @Test(expectedExceptions = ClientException.class)
+    public void testInvalidJavaVersion() throws ClientException {
+        final String original = System.getProperty("java.specification.version");
+        System.setProperty("java.specification.version", "1.6");
+        try {
+            Utils.checkJavaVersion();
+        } finally {
+            System.setProperty("java.specification.version", original);
+        }
+    }
+
+    @Test
+    public void testValidJavaVersion() throws ClientException {
+        Utils.checkJavaVersion();
+    }
+
+    @Test
+    public void testCanWriteOnNewFile() throws ClientException, IOException {
+        final File f = File.createTempFile("tempFile", ".txt");
+        f.deleteOnExit();
+        assertTrue(f.exists());
+        assertTrue(Utils.canWrite(f.getPath()));
+    }
+
+    @Test
+    public void testCannotWriteOnNewFile() throws ClientException, IOException {
+        final File f = File.createTempFile("tempFile", ".txt");
+        f.setReadOnly();
+        f.deleteOnExit();
+        assertTrue(f.exists());
+        assertFalse(Utils.canWrite(f.getPath()));
+    }
+
+    @Test
+    public void testGetHostNameForLdapUrl() {
+        assertEquals(Utils.getHostNameForLdapUrl("2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"),
+                "[2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]");
+        assertEquals(Utils.getHostNameForLdapUrl("basicUrl"), "basicUrl");
+        assertEquals(Utils.getHostNameForLdapUrl(null), null);
+        // Left/right brackets.
+        assertEquals(Utils.getHostNameForLdapUrl("[2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"),
+                "[2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]");
+        assertEquals(Utils.getHostNameForLdapUrl("2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]"),
+                "[2a01:e35:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]");
+    }
+
+    @Test
+    public void isDN() {
+        assertTrue(Utils.isDN("cn=Jensen,ou=people,dc=example,dc=com"));
+        assertTrue(Utils.isDN("cn=John Doe,dc=example,dc=org"));
+        assertFalse(Utils.isDN(null));
+        assertFalse(Utils.isDN("babs@example.com"));
+    }
+
+
+}
diff --git a/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
new file mode 100644
index 0000000..26c8257
--- /dev/null
+++ b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2014-2016 ForgeRock AS.
+-->
+<differences>
+<!--
+For specifying ignored differences, see:
+  http://mojo.codehaus.org/clirr-maven-plugin/examples/ignored-differences.html
+
+path/methods/to/from matching can either use:
+- regex when surrounded with '%regex[]'
+- ant style path matching when surrounded with '%ant[]'
+- ant style path matching when not surrounded by anything
+
+For path/methods matching in maven-clirr-plugin see (change version numbers accordingly):
+  http://svn.codehaus.org/mojo/tags/clirr-maven-plugin-2.6.1/src/main/java/org/codehaus/mojo/clirr/Difference.java
+  http://grepcode.com/file/repo1.maven.org/maven2/org.codehaus.plexus/plexus-utils/3.0.7/org/codehaus/plexus/util/SelectorUtils.java
+For a description of ant style path expressions see:
+  http://ant.apache.org/manual/dirtasks.html#patterns
+
+Note: waiting on https://jira.codehaus.org/browse/MCLIRR-62 to be resolved to avoid the need to use \s* in the '<to>' tags.
+-->
+</differences>
diff --git a/opendj-sdk/opendj-core/pom.xml b/opendj-sdk/opendj-core/pom.xml
new file mode 100644
index 0000000..13077c4
--- /dev/null
+++ b/opendj-sdk/opendj-core/pom.xml
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-core</artifactId>
+    <name>OpenDJ Core APIs</name>
+    <description>
+        This module provides the core APIs required for implementing LDAP Directory
+        client and server applications. Unlike the SDK this module does not
+        include a default network transport which must be obtained separately.
+    </description>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>forgerock-util</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-slf4j</artifactId>
+        </dependency>
+
+        <dependency>
+          <groupId>com.github.stephenc.jcip</groupId>
+          <artifactId>jcip-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock</groupId>
+            <artifactId>forgerock-build-tools</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+    <properties>
+        <opendj.osgi.import.additional>
+            com.sun.security.auth*;resolution:=optional
+        </opendj.osgi.import.additional>
+        <maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss'Z'</maven.build.timestamp.format>
+    </properties>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate-messages</goal>
+                        </goals>
+                        <configuration>
+                            <messageFiles>
+                                <messageFile>com/forgerock/opendj/ldap/core.properties</messageFile>
+                            </messageFiles>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <!-- Retrieve the SCM revision number and store it into the ${buildRevision} property -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>buildnumber-maven-plugin</artifactId>
+            </plugin>
+
+            <!-- Creates opendj-core bundle -->
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            com.forgerock.opendj.util,
+                            org.forgerock.opendj.io,
+                            org.forgerock.opendj.ldap*,
+                            org.forgerock.opendj.ldif
+                        </Export-Package>
+                        <Build-Maven>Apache Maven ${maven.version}</Build-Maven>
+                        <SCM-Revision>${buildRevision}</SCM-Revision>
+                        <SCM-Branch>${scmBranch}</SCM-Branch>
+                        <Build-Time>${maven.build.timestamp}</Build-Time>
+                        <Build-Java>${java.version}</Build-Java>
+                    </instructions>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <profiles>
+        <profile>
+            <!-- This profile provides API/ABI compatiblity checks and reports via Clirr -->
+            <id>clirr</id>
+            <activation>
+                <file>
+                    <exists>clirr-ignored-api-changes.xml</exists>
+                    <!-- this file name is duplicated due to MNG-4471 -->
+                </file>
+            </activation>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>clirr-maven-plugin</artifactId>
+                        <version>${clirrPluginVersion}</version>
+                        <inherited>true</inherited>
+                        <configuration>
+                            <comparisonArtifacts>
+                                <comparisonArtifact>
+                                    <groupId>${project.groupId}</groupId>
+                                    <artifactId>opendj-ldap-sdk</artifactId>
+                                    <!-- former name of this jar -->
+                                    <version>3.0.0-SNAPSHOT</version>
+                                </comparisonArtifact>
+                            </comparisonArtifacts>
+                            <excludes>
+                                <exclude>com/**</exclude>
+                            </excludes>
+                            <ignoredDifferencesFile>clirr-ignored-api-changes.xml</ignoredDifferencesFile>
+                        </configuration>
+
+                        <executions>
+                            <execution>
+                                <id>mvn clirr:check</id>
+                            </execution>
+
+                            <execution>
+                                <id>mvn verify</id>
+                                <goals>
+                                    <goal>check</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+
+            <reporting>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>clirr-maven-plugin</artifactId>
+                        <version>${clirrPluginVersion}</version>
+                        <inherited>true</inherited>
+                        <configuration>
+                            <comparisonArtifacts>
+                                <comparisonArtifact>
+                                    <groupId>${project.groupId}</groupId>
+                                    <artifactId>opendj-ldap-sdk</artifactId>
+                                    <!-- former name of this jar -->
+                                    <version>2.6.0</version>
+                                </comparisonArtifact>
+                            </comparisonArtifacts>
+                            <excludes>
+                                <exclude>com/**</exclude>
+                            </excludes>
+                            <ignoredDifferencesFile>clirr-ignored-api-changes.xml</ignoredDifferencesFile>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </reporting>
+        </profile>
+
+        <!--
+          Generates consolidated Javadoc covering both LDAP SDK packages
+          and also dependency (and transitive dependency) ForgeRock packages.
+        -->
+        <profile>
+            <id>forgerock-release</id>
+
+            <properties>
+                <javadocTitle>OpenDJ LDAP SDK ${project.version} API</javadocTitle>
+                <timestamp>${maven.build.timestamp}</timestamp>
+                <maven.build.timestamp.format>yyyy</maven.build.timestamp.format>
+            </properties>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>javadoc-jar</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                        <configuration>
+                            <includeDependencySources>true</includeDependencySources>
+                            <includeTransitiveDependencySources>true</includeTransitiveDependencySources>
+                            <dependencySourceIncludes>
+                                <dependencySourceInclude>org.forgerock.*:*</dependencySourceInclude>
+                            </dependencySourceIncludes>
+                            <excludePackageNames>com.*:*.internal</excludePackageNames>
+                            <groups>
+                                <group>
+                                    <title>${project.name} Packages</title>
+                                    <packages>${project.groupId}*</packages>
+                                </group>
+
+                                <group>
+                                    <title>ForgeRock Common Packages</title>
+                                    <packages>*</packages>
+                                </group>
+                            </groups>
+                            <author>false</author>
+                            <doctitle>${javadocTitle}</doctitle>
+                            <windowtitle>${javadocTitle}</windowtitle>
+                            <header>${javadocTitle}</header>
+                            <footer>${javadocTitle}</footer>
+                            <bottom>Copyright 2011-${maven.build.timestamp} ForgeRock AS.</bottom>
+                            <links>
+                                <link>http://docs.oracle.com/javase/7/docs/api/</link>
+                                <link>http://www.slf4j.org/apidocs/</link>
+                            </links>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <reportSets>
+                    <reportSet>
+                        <reports>
+                            <report>dependencies</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <links>
+                        <link>http://commons.forgerock.org/i18n-framework/i18n-core/apidocs</link>
+                    </links>
+                </configuration>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControl.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControl.java
new file mode 100644
index 0000000..acdf3bb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControl.java
@@ -0,0 +1,132 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ACCTUSABLEREQ_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ACCTUSABLEREQ_CONTROL_HAS_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Sun-defined account usability request control. The OID for this control
+ * is 1.3.6.1.4.1.42.2.27.9.5.8, and it does not have a value.
+ *
+ * @see AccountUsabilityResponseControl
+ */
+public final class AccountUsabilityRequestControl implements Control {
+    /** The OID for the account usability request control. */
+    public static final String OID = "1.3.6.1.4.1.42.2.27.9.5.8";
+
+    private final boolean isCritical;
+
+    private static final AccountUsabilityRequestControl CRITICAL_INSTANCE =
+            new AccountUsabilityRequestControl(true);
+    private static final AccountUsabilityRequestControl NONCRITICAL_INSTANCE =
+            new AccountUsabilityRequestControl(false);
+
+    /** A decoder which can be used for decoding the account usability request control. */
+    public static final ControlDecoder<AccountUsabilityRequestControl> DECODER =
+            new ControlDecoder<AccountUsabilityRequestControl>() {
+
+                @Override
+                public AccountUsabilityRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof AccountUsabilityRequestControl) {
+                        return (AccountUsabilityRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_ACCTUSABLEREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_ACCTUSABLEREQ_CONTROL_HAS_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new account usability request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static AccountUsabilityRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    /** Prevent direct instantiation. */
+    private AccountUsabilityRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AccountUsableRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControl.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControl.java
new file mode 100644
index 0000000..69157bc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControl.java
@@ -0,0 +1,451 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.byteToHex;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Sun-defined account usability response control. The OID for this control
+ * is 1.3.6.1.4.1.42.2.27.9.5.8, and it has a value encoded according to the
+ * following BNF:
+ *
+ * <pre>
+ * ACCOUNT_USABLE_RESPONSE ::= CHOICE {
+ *      is_available           [0] INTEGER, -- Seconds before expiration --
+ *      is_not_available       [1] MORE_INFO }
+ *
+ * MORE_INFO ::= SEQUENCE {
+ *      inactive               [0] BOOLEAN DEFAULT FALSE,
+ *      reset                  [1] BOOLEAN DEFAULT FALSE,
+ *      expired                [2] BOOLEAN DEFAULT_FALSE,
+ *      remaining_grace        [3] INTEGER OPTIONAL,
+ *      seconds_before_unlock  [4] INTEGER OPTIONAL }
+ * </pre>
+ *
+ * @see AccountUsabilityRequestControl
+ */
+public final class AccountUsabilityResponseControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The OID for the account usability response control. */
+    public static final String OID = AccountUsabilityRequestControl.OID;
+
+    /** A decoder which can be used for decoding the account usability response control. */
+    public static final ControlDecoder<AccountUsabilityResponseControl> DECODER =
+            new ControlDecoder<AccountUsabilityResponseControl>() {
+
+                @Override
+                public AccountUsabilityResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof AccountUsabilityResponseControl) {
+                        return (AccountUsabilityResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_ACCTUSABLERES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_ACCTUSABLERES_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    try {
+                        final ASN1Reader reader = ASN1.getReader(control.getValue());
+                        switch (reader.peekType()) {
+                        case TYPE_SECONDS_BEFORE_EXPIRATION:
+                            final int secondsBeforeExpiration = (int) reader.readInteger();
+                            return new AccountUsabilityResponseControl(control.isCritical(), true,
+                                    false, false, false, -1, false, 0, secondsBeforeExpiration);
+                        case TYPE_MORE_INFO:
+                            boolean isInactive = false;
+                            boolean isReset = false;
+                            boolean isExpired = false;
+                            boolean isLocked = false;
+                            int remainingGraceLogins = -1;
+                            int secondsBeforeUnlock = 0;
+
+                            reader.readStartSequence();
+                            if (reader.hasNextElement() && (reader.peekType() == TYPE_INACTIVE)) {
+                                isInactive = reader.readBoolean();
+                            }
+                            if (reader.hasNextElement() && (reader.peekType() == TYPE_RESET)) {
+                                isReset = reader.readBoolean();
+                            }
+                            if (reader.hasNextElement() && (reader.peekType() == TYPE_EXPIRED)) {
+                                isExpired = reader.readBoolean();
+                            }
+                            if (reader.hasNextElement()
+                                    && (reader.peekType() == TYPE_REMAINING_GRACE_LOGINS)) {
+                                remainingGraceLogins = (int) reader.readInteger();
+                            }
+                            if (reader.hasNextElement()
+                                    && (reader.peekType() == TYPE_SECONDS_BEFORE_UNLOCK)) {
+                                isLocked = true;
+                                secondsBeforeUnlock = (int) reader.readInteger();
+                            }
+                            reader.readEndSequence();
+
+                            return new AccountUsabilityResponseControl(control.isCritical(), false,
+                                    isInactive, isReset, isExpired, remainingGraceLogins, isLocked,
+                                    secondsBeforeUnlock, -1);
+
+                        default:
+                            final LocalizableMessage message =
+                                    ERR_ACCTUSABLERES_UNKNOWN_VALUE_ELEMENT_TYPE
+                                            .get(byteToHex(reader.peekType()));
+                            throw DecodeException.error(message);
+                        }
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("%s", e));
+
+                        final LocalizableMessage message =
+                                ERR_ACCTUSABLERES_DECODE_ERROR.get(getExceptionMessage(e));
+                        throw DecodeException.error(message);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /** The BER type to use for the seconds before expiration when the account is available. */
+    private static final byte TYPE_SECONDS_BEFORE_EXPIRATION = (byte) 0x80;
+
+    /** The BER type to use for the MORE_INFO sequence when the account is not available. */
+    private static final byte TYPE_MORE_INFO = (byte) 0xA1;
+
+    /**
+     * The BER type to use for the MORE_INFO element that indicates that the
+     * account has been inactivated.
+     */
+    private static final byte TYPE_INACTIVE = (byte) 0x80;
+
+    /**
+     * The BER type to use for the MORE_INFO element that indicates that the
+     * password has been administratively reset.
+     */
+    private static final byte TYPE_RESET = (byte) 0x81;
+
+    /**
+     * The BER type to use for the MORE_INFO element that indicates that the
+     * user's password is expired.
+     */
+    private static final byte TYPE_EXPIRED = (byte) 0x82;
+
+    /**
+     * The BER type to use for the MORE_INFO element that provides the number of
+     * remaining grace logins.
+     */
+    private static final byte TYPE_REMAINING_GRACE_LOGINS = (byte) 0x83;
+
+    /**
+     * The BER type to use for the MORE_INFO element that indicates that the
+     * password has been administratively reset.
+     */
+    private static final byte TYPE_SECONDS_BEFORE_UNLOCK = (byte) 0x84;
+
+    /**
+     * Creates a new account usability response control that may be used to
+     * indicate that the account is not available and provide information about
+     * the underlying reason.
+     *
+     * @param isInactive
+     *            Indicates whether the user's account has been inactivated by
+     *            an administrator.
+     * @param isReset
+     *            Indicates whether the user's password has been reset by an
+     *            administrator.
+     * @param isExpired
+     *            Indicates whether the user's password has expired.
+     * @param remainingGraceLogins
+     *            The number of grace logins remaining. A value of {@code 0}
+     *            indicates that there are none remaining. A value of {@code -1}
+     *            indicates that grace login functionality is not enabled.
+     * @param isLocked
+     *            Indicates whether the user's account is currently locked out.
+     * @param secondsBeforeUnlock
+     *            The length of time in seconds until the account is unlocked. A
+     *            value of {@code -1} indicates that the account will not be
+     *            automatically unlocked and must be reset by an administrator.
+     * @return The new control.
+     */
+    public static AccountUsabilityResponseControl newControl(final boolean isInactive,
+            final boolean isReset, final boolean isExpired, final int remainingGraceLogins,
+            final boolean isLocked, final int secondsBeforeUnlock) {
+        return new AccountUsabilityResponseControl(false, false, isInactive, isReset, isExpired,
+                remainingGraceLogins, isLocked, secondsBeforeUnlock, -1);
+    }
+
+    /**
+     * Creates a new account usability response control that may be used to
+     * indicate that the account is available and provide the number of seconds
+     * until expiration.
+     *
+     * @param secondsBeforeExpiration
+     *            The length of time in seconds until the user's password
+     *            expires, or {@code -1} if the user's password will not expire
+     *            or the expiration time is unknown.
+     * @return The new control.
+     */
+    public static AccountUsabilityResponseControl newControl(final int secondsBeforeExpiration) {
+        return new AccountUsabilityResponseControl(false, true, false, false, false, -1, false, 0,
+                secondsBeforeExpiration);
+    }
+
+    /** Indicates whether the user's account is usable. */
+    private final boolean isUsable;
+
+    /** Indicates whether the user's password is expired. */
+    private final boolean isExpired;
+
+    /** Indicates whether the user's account is inactive. */
+    private final boolean isInactive;
+
+    /** Indicates whether the user's account is currently locked. */
+    private final boolean isLocked;
+
+    /**
+     * Indicates whether the user's password has been reset and must be
+     * changed before anything else can be done.
+     */
+    private final boolean isReset;
+
+    /** The number of remaining grace logins, if available. */
+    private final int remainingGraceLogins;
+
+    /** The length of time in seconds before the user's password expires, if available. */
+    private final int secondsBeforeExpiration;
+
+    /** The length of time before the user's account is unlocked, if available. */
+    private final int secondsBeforeUnlock;
+
+    private final boolean isCritical;
+
+    /** Prevent direct instantiation. */
+    private AccountUsabilityResponseControl(final boolean isCritical, final boolean isUsable,
+            final boolean isInactive, final boolean isReset, final boolean isExpired,
+            final int remainingGraceLogins, final boolean isLocked, final int secondsBeforeUnlock,
+            final int secondsBeforeExpiration) {
+        this.isCritical = isCritical;
+        this.isUsable = isUsable;
+        this.isInactive = isInactive;
+        this.isReset = isReset;
+        this.isExpired = isExpired;
+        this.remainingGraceLogins = remainingGraceLogins;
+        this.isLocked = isLocked;
+        this.secondsBeforeUnlock = secondsBeforeUnlock;
+        this.secondsBeforeExpiration = secondsBeforeExpiration;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns the number of remaining grace logins for the user. This value is
+     * unreliable if the user's password has not expired.
+     *
+     * @return The number of remaining grace logins for the user, or {@code -1}
+     *         if the grace logins feature is not enabled for the user.
+     */
+    public int getRemainingGraceLogins() {
+        return remainingGraceLogins;
+    }
+
+    /**
+     * Returns the length of time in seconds before the user's password expires.
+     * This value is unreliable if the account is not available.
+     *
+     * @return The length of time in seconds before the user's password expires,
+     *         or {@code -1} if it is unknown or password expiration is not
+     *         enabled for the user.
+     */
+    public int getSecondsBeforeExpiration() {
+        return secondsBeforeExpiration;
+    }
+
+    /**
+     * Returns the length of time in seconds before the user's account is
+     * automatically unlocked. This value is unreliable is the user's account is
+     * not locked.
+     *
+     * @return The length of time in seconds before the user's account is
+     *         automatically unlocked, or {@code -1} if it requires
+     *         administrative action to unlock the account.
+     */
+    public int getSecondsBeforeUnlock() {
+        return secondsBeforeUnlock;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            if (secondsBeforeExpiration < 0) {
+                writer.writeInteger(TYPE_SECONDS_BEFORE_EXPIRATION, secondsBeforeExpiration);
+            } else {
+                writer.writeStartSequence(TYPE_MORE_INFO);
+                if (isInactive) {
+                    writer.writeBoolean(TYPE_INACTIVE, true);
+                }
+
+                if (isReset) {
+                    writer.writeBoolean(TYPE_RESET, true);
+                }
+
+                if (isExpired) {
+                    writer.writeBoolean(TYPE_EXPIRED, true);
+
+                    if (remainingGraceLogins >= 0) {
+                        writer.writeInteger(TYPE_REMAINING_GRACE_LOGINS, remainingGraceLogins);
+                    }
+                }
+
+                if (isLocked) {
+                    writer.writeInteger(TYPE_SECONDS_BEFORE_UNLOCK, secondsBeforeUnlock);
+                }
+                writer.writeEndSequence();
+            }
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    /**
+     * Returns {@code true} if the user's password has expired.
+     *
+     * @return <CODE>true</CODE> if the user's password has expired, or
+     *         <CODE>false</CODE> if not.
+     */
+    public boolean isExpired() {
+        return isExpired;
+    }
+
+    /**
+     * Returns {@code true} if the user's account has been inactivated by an
+     * administrator.
+     *
+     * @return <CODE>true</CODE> if the user's account has been inactivated by
+     *         an administrator, or <CODE>false</CODE> if not.
+     */
+    public boolean isInactive() {
+        return isInactive;
+    }
+
+    /**
+     * Returns {@code true} if the user's account is locked for some reason.
+     *
+     * @return <CODE>true</CODE> if the user's account is locked, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    public boolean isLocked() {
+        return isLocked;
+    }
+
+    /**
+     * Returns {@code true} if the user's password has been administratively
+     * reset and the user must change that password before any other operations
+     * will be allowed.
+     *
+     * @return <CODE>true</CODE> if the user's password has been
+     *         administratively reset, or <CODE>false</CODE> if not.
+     */
+    public boolean isReset() {
+        return isReset;
+    }
+
+    /**
+     * Returns {@code true} if the associated user account is available for use.
+     *
+     * @return <CODE>true</CODE> if the associated user account is available, or
+     *         <CODE>false</CODE> if not.
+     */
+    public boolean isUsable() {
+        return isUsable;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AccountUsableResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", isUsable=");
+        builder.append(isUsable);
+        if (isUsable) {
+            builder.append(",secondsBeforeExpiration=");
+            builder.append(secondsBeforeExpiration);
+        } else {
+            builder.append(",isInactive=");
+            builder.append(isInactive);
+            builder.append(",isReset=");
+            builder.append(isReset);
+            builder.append(",isExpired=");
+            builder.append(isExpired);
+            builder.append(",remainingGraceLogins=");
+            builder.append(remainingGraceLogins);
+            builder.append(",isLocked=");
+            builder.append(isLocked);
+            builder.append(",secondsBeforeUnlock=");
+            builder.append(secondsBeforeUnlock);
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/RealAttributesOnlyRequestControl.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/RealAttributesOnlyRequestControl.java
new file mode 100644
index 0000000..d4df793
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/RealAttributesOnlyRequestControl.java
@@ -0,0 +1,130 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_REAL_ATTRS_ONLY_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_REAL_ATTRS_ONLY_INVALID_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Sun-defined real attributes only request control. The OID for this
+ * control is 2.16.840.1.113730.3.4.17, and it does not have a value.
+ */
+public final class RealAttributesOnlyRequestControl implements Control {
+    /** The OID for the real attributes only request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.17";
+
+    private static final RealAttributesOnlyRequestControl CRITICAL_INSTANCE =
+            new RealAttributesOnlyRequestControl(true);
+
+    private static final RealAttributesOnlyRequestControl NONCRITICAL_INSTANCE =
+            new RealAttributesOnlyRequestControl(false);
+
+    /** A decoder which can be used for decoding the real attributes only request control. */
+    public static final ControlDecoder<RealAttributesOnlyRequestControl> DECODER =
+            new ControlDecoder<RealAttributesOnlyRequestControl>() {
+
+                @Override
+                public RealAttributesOnlyRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof RealAttributesOnlyRequestControl) {
+                        return (RealAttributesOnlyRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_REAL_ATTRS_ONLY_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_REAL_ATTRS_ONLY_INVALID_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new real attributes only request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static RealAttributesOnlyRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private final boolean isCritical;
+
+    private RealAttributesOnlyRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("RealAttributesOnlyRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/TransactionIdControl.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/TransactionIdControl.java
new file mode 100644
index 0000000..1ef43fc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/TransactionIdControl.java
@@ -0,0 +1,131 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.util.Reject;
+
+/**
+ * Control to provide a transaction ID.
+ * <p>
+ * The transaction ID is related to Common Audit : it is used for tracking the processing of a user-interaction as it
+ * passes through the Forgerock stack
+ * <p>
+ * The control's value is the UTF-8 encoding of the transaction ID.
+ */
+public final class TransactionIdControl implements Control {
+
+    /** OID for this control. */
+    public static final String OID = "1.3.6.1.4.1.36733.2.1.5.1";
+
+    /** A decoder which can be used for decoding the simple paged results control. */
+    public static final ControlDecoder<TransactionIdControl> DECODER = new ControlDecoder<TransactionIdControl>() {
+
+        @Override
+        public TransactionIdControl decodeControl(final Control control, final DecodeOptions options)
+                throws DecodeException {
+            Reject.ifNull(control);
+
+            if (control instanceof TransactionIdControl) {
+                return (TransactionIdControl) control;
+            }
+
+            if (!control.getOID().equals(OID)) {
+                throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_BAD_OID.get(control.getOID(), OID));
+            }
+
+            if (!control.hasValue()) {
+                // The control must always have a value.
+                throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_DECODE_NULL.get());
+            }
+
+            return new TransactionIdControl(control.getValue().toString());
+        }
+
+        @Override
+        public String getOID() {
+            return OID;
+        }
+    };
+
+    /**
+     * Creates a new transactionId control.
+     *
+     * @param transactionId
+     *            The transaction id to provide through this control.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code transactionId} was {@code null}.
+     */
+    public static TransactionIdControl newControl(final String transactionId) {
+        Reject.ifNull(transactionId);
+        return new TransactionIdControl(transactionId);
+    }
+
+    /** The control value transactionId element. */
+    private final String transactionId;
+
+    private TransactionIdControl(final String transactionId) {
+        this.transactionId = transactionId;
+    }
+
+    /**
+     * Returns the transaction id.
+     *
+     * @return The transaction id.
+     */
+    public String getTransactionId() {
+        return transactionId;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return ByteString.valueOfUtf8(transactionId);
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        // This control is never critical.
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("TransactionIdControl(oid=");
+        builder.append(getOID());
+        builder.append(", transactionId=");
+        builder.append(transactionId);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/VirtualAttributesOnlyRequestControl.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/VirtualAttributesOnlyRequestControl.java
new file mode 100644
index 0000000..8889289
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/VirtualAttributesOnlyRequestControl.java
@@ -0,0 +1,130 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_VIRTUAL_ATTRS_ONLY_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_VIRTUAL_ATTRS_ONLY_INVALID_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Sun-defined virtual attributes only request control. The OID for this
+ * control is 2.16.840.1.113730.3.4.19, and it does not have a value.
+ */
+public final class VirtualAttributesOnlyRequestControl implements Control {
+    /** The OID for the virtual attributes only request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.19";
+
+    private static final VirtualAttributesOnlyRequestControl CRITICAL_INSTANCE =
+            new VirtualAttributesOnlyRequestControl(true);
+
+    private static final VirtualAttributesOnlyRequestControl NONCRITICAL_INSTANCE =
+            new VirtualAttributesOnlyRequestControl(false);
+
+    /** A decoder which can be used for decoding the virtual attributes only request control. */
+    public static final ControlDecoder<VirtualAttributesOnlyRequestControl> DECODER =
+            new ControlDecoder<VirtualAttributesOnlyRequestControl>() {
+
+                @Override
+                public VirtualAttributesOnlyRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof VirtualAttributesOnlyRequestControl) {
+                        return (VirtualAttributesOnlyRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_VIRTUAL_ATTRS_ONLY_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_VIRTUAL_ATTRS_ONLY_INVALID_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new virtual attributes only request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static VirtualAttributesOnlyRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private final boolean isCritical;
+
+    private VirtualAttributesOnlyRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("VirtualAttributesOnlyRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/package-info.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/package-info.java
new file mode 100644
index 0000000..c18c197
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/controls/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes implementing Sun proprietary LDAP controls.
+ */
+package com.forgerock.opendj.ldap.controls;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedRequest.java
new file mode 100644
index 0000000..ecacb65
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedRequest.java
@@ -0,0 +1,169 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.extensions;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AbstractExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequestDecoder;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * Get connection ID extended request. This operation can be used to retrieve
+ * the client connection ID.
+ *
+ * @see GetConnectionIDExtendedResult
+ */
+public final class GetConnectionIDExtendedRequest extends
+        AbstractExtendedRequest<GetConnectionIDExtendedRequest, GetConnectionIDExtendedResult> {
+    private static final class RequestDecoder implements
+            ExtendedRequestDecoder<GetConnectionIDExtendedRequest, GetConnectionIDExtendedResult> {
+
+        @Override
+        public GetConnectionIDExtendedRequest decodeExtendedRequest(
+                final ExtendedRequest<?> request, final DecodeOptions options)
+                throws DecodeException {
+            // TODO: Check the OID and that the value is not present.
+            final GetConnectionIDExtendedRequest newRequest = new GetConnectionIDExtendedRequest();
+            for (final Control control : request.getControls()) {
+                newRequest.addControl(control);
+            }
+            return newRequest;
+
+        }
+    }
+
+    private static final class ResultDecoder extends
+            AbstractExtendedResultDecoder<GetConnectionIDExtendedResult> {
+        @Override
+        public GetConnectionIDExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            if (!resultCode.isExceptional()) {
+                // A successful response must contain a response name and
+                // value.
+                throw new IllegalArgumentException("No response name and value for result code "
+                        + resultCode.intValue());
+            }
+            return GetConnectionIDExtendedResult.newResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+
+        @Override
+        public GetConnectionIDExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            if (result instanceof GetConnectionIDExtendedResult) {
+                return (GetConnectionIDExtendedResult) result;
+            }
+
+            final ResultCode resultCode = result.getResultCode();
+            final GetConnectionIDExtendedResult newResult =
+                    GetConnectionIDExtendedResult.newResult(resultCode)
+                        .setMatchedDN(result.getMatchedDN())
+                        .setDiagnosticMessage(result.getDiagnosticMessage());
+
+            final ByteString responseValue = result.getValue();
+            if (!resultCode.isExceptional() && responseValue == null) {
+                throw DecodeException.error(LocalizableMessage.raw("Empty response value"));
+            }
+            if (responseValue != null) {
+                try {
+                    final ASN1Reader reader = ASN1.getReader(responseValue);
+                    newResult.setConnectionID((int) reader.readInteger());
+                } catch (final IOException e) {
+                    throw DecodeException.error(LocalizableMessage
+                            .raw("Error decoding response value"), e);
+                }
+            }
+            for (final Control control : result.getControls()) {
+                newResult.addControl(control);
+            }
+            return newResult;
+        }
+    }
+
+    /**
+     * The OID for the extended operation that can be used to get the client
+     * connection ID. It will be both the request and response OID.
+     */
+    public static final String OID = "1.3.6.1.4.1.26027.1.6.2";
+
+    /** Singleton. */
+    private static final GetConnectionIDExtendedRequest INSTANCE =
+            new GetConnectionIDExtendedRequest();
+
+    /** A decoder which can be used to decode get connection ID extended operation requests. */
+    public static final RequestDecoder REQUEST_DECODER = new RequestDecoder();
+
+    /** No need to expose this. */
+    private static final ResultDecoder RESULT_DECODER = new ResultDecoder();
+
+    /**
+     * Creates a new get connection ID extended request.
+     *
+     * @return The new get connection ID extended request.
+     */
+    public static GetConnectionIDExtendedRequest newRequest() {
+        return INSTANCE;
+    }
+
+    /** Prevent instantiation. */
+    private GetConnectionIDExtendedRequest() {
+        // Nothing to do.
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ExtendedResultDecoder<GetConnectionIDExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GetConnectionIDExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedResult.java
new file mode 100644
index 0000000..6341b74
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetConnectionIDExtendedResult.java
@@ -0,0 +1,124 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.extensions;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResult;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Get connection ID extended result.
+ *
+ * @see GetConnectionIDExtendedRequest
+ */
+public final class GetConnectionIDExtendedResult extends
+        AbstractExtendedResult<GetConnectionIDExtendedResult> {
+    /**
+     * Creates a new get connection ID extended result with a default connection
+     * ID of -1.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new get connection ID extended result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static GetConnectionIDExtendedResult newResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new GetConnectionIDExtendedResult(resultCode);
+    }
+
+    private int connectionID = -1;
+
+    private GetConnectionIDExtendedResult(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    /**
+     * Returns the client connection ID.
+     *
+     * @return The client connection ID.
+     */
+    public int getConnectionID() {
+        return connectionID;
+    }
+
+    @Override
+    public String getOID() {
+        return GetConnectionIDExtendedRequest.OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder(6);
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+
+        try {
+            writer.writeInteger(connectionID);
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+
+        return buffer.toByteString();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    /**
+     * Sets the client connection ID.
+     *
+     * @param connectionID
+     *            The client connection ID.
+     * @return This get connection ID result.
+     */
+    public GetConnectionIDExtendedResult setConnectionID(final int connectionID) {
+        this.connectionID = connectionID;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GetConnectionIDExtendedResponse(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", responseName=");
+        builder.append(getOID());
+        builder.append(", connectionID=");
+        builder.append(connectionID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetSymmetricKeyExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetSymmetricKeyExtendedRequest.java
new file mode 100644
index 0000000..7958b97
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/GetSymmetricKeyExtendedRequest.java
@@ -0,0 +1,228 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.extensions;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AbstractExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequestDecoder;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+/** Get symmetric key extended request. */
+public final class GetSymmetricKeyExtendedRequest extends
+        AbstractExtendedRequest<GetSymmetricKeyExtendedRequest, ExtendedResult> {
+    private static final class RequestDecoder implements
+            ExtendedRequestDecoder<GetSymmetricKeyExtendedRequest, ExtendedResult> {
+        @Override
+        public GetSymmetricKeyExtendedRequest decodeExtendedRequest(
+                final ExtendedRequest<?> request, final DecodeOptions options)
+                throws DecodeException {
+            final ByteString requestValue = request.getValue();
+            if (requestValue == null) {
+                // The request must always have a value.
+                final LocalizableMessage message = ERR_GET_SYMMETRIC_KEY_NO_VALUE.get();
+                throw DecodeException.error(message);
+            }
+
+            String requestSymmetricKey = null;
+            String instanceKeyID = null;
+
+            try {
+                final ASN1Reader reader = ASN1.getReader(requestValue);
+                reader.readStartSequence();
+                if (reader.hasNextElement() && (reader.peekType() == TYPE_SYMMETRIC_KEY_ELEMENT)) {
+                    requestSymmetricKey = reader.readOctetStringAsString();
+                }
+                if (reader.hasNextElement() && (reader.peekType() == TYPE_INSTANCE_KEY_ID_ELEMENT)) {
+                    instanceKeyID = reader.readOctetStringAsString();
+                }
+                reader.readEndSequence();
+
+                final GetSymmetricKeyExtendedRequest newRequest =
+                        new GetSymmetricKeyExtendedRequest().setRequestSymmetricKey(
+                                requestSymmetricKey).setInstanceKeyID(instanceKeyID);
+
+                for (final Control control : request.getControls()) {
+                    newRequest.addControl(control);
+                }
+
+                return newRequest;
+            } catch (final IOException ioe) {
+                logger.debug(LocalizableMessage.raw("%s", ioe));
+
+                final LocalizableMessage message =
+                        ERR_GET_SYMMETRIC_KEY_ASN1_DECODE_EXCEPTION.get(ioe.getMessage());
+                throw DecodeException.error(message, ioe);
+            }
+        }
+    }
+
+    private static final class ResultDecoder extends AbstractExtendedResultDecoder<ExtendedResult> {
+        @Override
+        public ExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newGenericExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+
+        @Override
+        public ExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            return result;
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The request OID for the get symmetric key extended operation. */
+    public static final String OID = "1.3.6.1.4.1.26027.1.6.3";
+    /** The BER type value for the symmetric key element of the operation value. */
+    private static final byte TYPE_SYMMETRIC_KEY_ELEMENT = (byte) 0x80;
+    /** The BER type value for the instance key ID element of the operation value. */
+    private static final byte TYPE_INSTANCE_KEY_ID_ELEMENT = (byte) 0x81;
+    /** A decoder which can be used to decode get symmetric key extended operation requests. */
+    public static final RequestDecoder REQUEST_DECODER = new RequestDecoder();
+    /** No need to expose this. */
+    private static final ResultDecoder RESULT_DECODER = new ResultDecoder();
+
+    /**
+     * Creates a new get symmetric key extended request.
+     *
+     * @return The new get symmetric key extended request.
+     */
+    public static GetSymmetricKeyExtendedRequest newRequest() {
+        return new GetSymmetricKeyExtendedRequest();
+    }
+
+    private String requestSymmetricKey;
+    private String instanceKeyID;
+
+    /** Instantiation via factory. */
+    private GetSymmetricKeyExtendedRequest() {
+    }
+
+    /**
+     * Returns the instance key ID.
+     *
+     * @return The instance key ID.
+     */
+    public String getInstanceKeyID() {
+        return instanceKeyID;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns the request symmetric key.
+     *
+     * @return The request symmetric key.
+     */
+    public String getRequestSymmetricKey() {
+        return requestSymmetricKey;
+    }
+
+    @Override
+    public ExtendedResultDecoder<ExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+
+        try {
+            writer.writeStartSequence();
+            if (requestSymmetricKey != null) {
+                writer.writeOctetString(TYPE_SYMMETRIC_KEY_ELEMENT, requestSymmetricKey);
+            }
+            if (instanceKeyID != null) {
+                writer.writeOctetString(TYPE_INSTANCE_KEY_ID_ELEMENT, instanceKeyID);
+            }
+            writer.writeEndSequence();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+
+        return buffer.toByteString();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    /**
+     * Sets the instance key ID.
+     *
+     * @param instanceKeyID
+     *            The instance key ID.
+     * @return This get symmetric key request.
+     */
+    public GetSymmetricKeyExtendedRequest setInstanceKeyID(final String instanceKeyID) {
+        this.instanceKeyID = instanceKeyID;
+        return this;
+    }
+
+    /**
+     * Sets the request symmetric key.
+     *
+     * @param requestSymmetricKey
+     *            The request symmetric key.
+     * @return This get symmetric key request.
+     */
+    public GetSymmetricKeyExtendedRequest setRequestSymmetricKey(final String requestSymmetricKey) {
+        this.requestSymmetricKey = requestSymmetricKey;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GetSymmetricKeyExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", requestSymmetricKey=");
+        builder.append(requestSymmetricKey);
+        builder.append(", instanceKeyID=");
+        builder.append(instanceKeyID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedRequest.java
new file mode 100644
index 0000000..c947b40
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedRequest.java
@@ -0,0 +1,696 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.extensions;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AbstractExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequestDecoder;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.ldap.extensions.PasswordPolicyStateOperationType.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * This class implements an LDAP extended operation that can be used to query
+ * and update elements of the Directory Server password policy state for a given
+ * user. The ASN.1 definition for the value of the extended request is: <BR>
+ *
+ * <PRE>
+ * PasswordPolicyStateValue ::= SEQUENCE {
+ *      targetUser     LDAPDN
+ *      operations     SEQUENCE OF PasswordPolicyStateOperation OPTIONAL }
+ * PasswordPolicyStateOperation ::= SEQUENCE {
+ *      opType       ENUMERATED {
+ *           getPasswordPolicyDN                          (0),
+ *           getAccountDisabledState                      (1),
+ *           setAccountDisabledState                      (2),
+ *           clearAccountDisabledState                    (3),
+ *           getAccountExpirationTime                     (4),
+ *           setAccountExpirationTime                     (5),
+ *           clearAccountExpirationTime                   (6),
+ *           getSecondsUntilAccountExpiration             (7),
+ *           getPasswordChangedTime                       (8),
+ *           setPasswordChangedTime                       (9),
+ *           clearPasswordChangedTime                     (10),
+ *           getPasswordExpirationWarnedTime              (11),
+ *           setPasswordExpirationWarnedTime              (12),
+ *           clearPasswordExpirationWarnedTime            (13),
+ *           getSecondsUntilPasswordExpiration            (14),
+ *           getSecondsUntilPasswordExpirationWarning     (15),
+ *           getAuthenticationFailureTimes                (16),
+ *           addAuthenticationFailureTime                 (17),
+ *           setAuthenticationFailureTimes                (18),
+ *           clearAuthenticationFailureTimes              (19),
+ *           getSecondsUntilAuthenticationFailureUnlock   (20),
+ *           getRemainingAuthenticationFailureCount       (21),
+ *           getLastLoginTime                             (22),
+ *           setLastLoginTime                             (23),
+ *           clearLastLoginTime                           (24),
+ *           getSecondsUntilIdleLockout                   (25),
+ *           getPasswordResetState                        (26),
+ *           setPasswordResetState                        (27),
+ *           clearPasswordResetState                      (28),
+ *           getSecondsUntilPasswordResetLockout          (29),
+ *           getGraceLoginUseTimes                        (30),
+ *           addGraceLoginUseTime                         (31),
+ *           setGraceLoginUseTimes                        (32),
+ *           clearGraceLoginUseTimes                      (33),
+ *           getRemainingGraceLoginCount                  (34),
+ *           getPasswordChangedByRequiredTime             (35),
+ *           setPasswordChangedByRequiredTime             (36),
+ *           clearPasswordChangedByRequiredTime           (37),
+ *           getSecondsUntilRequiredChangeTime            (38),
+ *           getPasswordHistory                           (39),
+ *           clearPasswordHistory                         (40),
+ *           ... },
+ *      opValues     SEQUENCE OF OCTET STRING OPTIONAL }
+ * </PRE>
+ *
+ * <BR>
+ * Both the request and response values use the same encoded form, and they both
+ * use the same OID of "1.3.6.1.4.1.26027.1.6.1". The response value will only
+ * include get* elements. If the request did not include any operations, then
+ * the response will include all get* elements; otherwise, the response will
+ * only include the get* elements that correspond to the state fields referenced
+ * in the request (regardless of whether that operation was included in a get*,
+ * set*, add*, remove*, or clear* operation).
+ */
+public final class PasswordPolicyStateExtendedRequest
+        extends
+        AbstractExtendedRequest<PasswordPolicyStateExtendedRequest, PasswordPolicyStateExtendedResult>
+        implements PasswordPolicyStateOperationContainer {
+    private static final class MultiValueOperation implements PasswordPolicyStateOperation {
+        private final PasswordPolicyStateOperationType property;
+
+        private final List<ByteString> values;
+
+        private MultiValueOperation(final PasswordPolicyStateOperationType property,
+                final ByteString value) {
+            this.property = property;
+            this.values = Collections.singletonList(value);
+        }
+
+        private MultiValueOperation(final PasswordPolicyStateOperationType property,
+                final List<ByteString> values) {
+            this.property = property;
+            this.values = values;
+        }
+
+        @Override
+        public PasswordPolicyStateOperationType getOperationType() {
+            return property;
+        }
+
+        @Override
+        public Iterable<ByteString> getValues() {
+            return values;
+        }
+
+        @Override
+        public String toString() {
+            return property + ": " + values;
+        }
+    }
+
+    private static final class RequestDecoder
+            implements
+            ExtendedRequestDecoder<PasswordPolicyStateExtendedRequest, PasswordPolicyStateExtendedResult> {
+        @Override
+        public PasswordPolicyStateExtendedRequest decodeExtendedRequest(
+                final ExtendedRequest<?> request, final DecodeOptions options)
+                throws DecodeException {
+            final ByteString requestValue = request.getValue();
+            if (requestValue == null || requestValue.length() <= 0) {
+                throw DecodeException.error(ERR_PWPSTATE_EXTOP_NO_REQUEST_VALUE.get());
+            }
+            try {
+                final ASN1Reader reader = ASN1.getReader(requestValue);
+                reader.readStartSequence();
+
+                // Read the target user DN
+                final PasswordPolicyStateExtendedRequest newRequest =
+                        new PasswordPolicyStateExtendedRequest();
+                newRequest.setTargetUser(reader.readOctetStringAsString());
+
+                decodeOperations(reader, newRequest);
+                reader.readEndSequence();
+
+                for (final Control control : request.getControls()) {
+                    newRequest.addControl(control);
+                }
+
+                return newRequest;
+            } catch (final IOException ioe) {
+                final LocalizableMessage message =
+                        ERR_PWPSTATE_EXTOP_DECODE_FAILURE.get(getExceptionMessage(ioe));
+                throw DecodeException.error(message, ioe);
+            }
+        }
+    }
+
+    private static final class ResultDecoder extends
+            AbstractExtendedResultDecoder<PasswordPolicyStateExtendedResult> {
+        @Override
+        public PasswordPolicyStateExtendedResult newExtendedErrorResult(
+                final ResultCode resultCode, final String matchedDN, final String diagnosticMessage) {
+            if (!resultCode.isExceptional()) {
+                // A successful response must contain a response name and
+                // value.
+                throw new IllegalArgumentException("No response name and value for result code "
+                        + resultCode.intValue());
+            }
+
+            return new PasswordPolicyStateExtendedResult(resultCode).setMatchedDN(
+                    matchedDN).setDiagnosticMessage(diagnosticMessage);
+        }
+
+        @Override
+        public PasswordPolicyStateExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            final ResultCode resultCode = result.getResultCode();
+            final PasswordPolicyStateExtendedResult newResult =
+                    new PasswordPolicyStateExtendedResult(resultCode).setMatchedDN(
+                            result.getMatchedDN()).setDiagnosticMessage(
+                            result.getDiagnosticMessage());
+
+            final ByteString responseValue = result.getValue();
+            if (!resultCode.isExceptional() && responseValue == null) {
+                throw DecodeException.error(ERR_PWPSTATE_EXTOP_NO_REQUEST_VALUE.get());
+            }
+            if (responseValue != null) {
+                try {
+                    final ASN1Reader reader = ASN1.getReader(responseValue);
+                    reader.readStartSequence();
+                    newResult.setTargetUser(reader.readOctetStringAsString());
+                    decodeOperations(reader, newResult);
+                    reader.readEndSequence();
+                } catch (final IOException ioe) {
+                    final LocalizableMessage message =
+                            ERR_PWPSTATE_EXTOP_DECODE_FAILURE.get(getExceptionMessage(ioe));
+                    throw DecodeException.error(message, ioe);
+                }
+            }
+            for (final Control control : result.getControls()) {
+                newResult.addControl(control);
+            }
+            return newResult;
+        }
+    }
+
+    /**
+     * The OID for the password policy state extended operation (both the
+     * request and response types).
+     */
+    public static final String OID = "1.3.6.1.4.1.26027.1.6.1";
+
+    private String targetUser = "";
+
+    private final List<PasswordPolicyStateOperation> operations = new ArrayList<>();
+
+    static final String PASSWORD_POLICY_DN_NAME = "Password Policy DN";
+    static final String ACCOUNT_DISABLED_STATE_NAME = "Account Disabled State";
+    static final String ACCOUNT_EXPIRATION_TIME_NAME = "Account Expiration Time";
+    static final String SECONDS_UNTIL_ACCOUNT_EXPIRATION_NAME = "Seconds Until Account Expiration";
+    static final String PASSWORD_CHANGED_TIME_NAME = "Password Changed Time";
+    static final String PASSWORD_EXPIRATION_WARNED_TIME_NAME = "Password Expiration Warned Time";
+    static final String SECONDS_UNTIL_PASSWORD_EXPIRATION_NAME =
+            "Seconds Until Password Expiration";
+    static final String SECONDS_UNTIL_PASSWORD_EXPIRATION_WARNING_NAME =
+            "Seconds Until Password Expiration Warning";
+    static final String AUTHENTICATION_FAILURE_TIMES_NAME = "Authentication Failure Times";
+    static final String SECONDS_UNTIL_AUTHENTICATION_FAILURE_UNLOCK_NAME =
+            "Seconds Until Authentication Failure Unlock";
+    static final String REMAINING_AUTHENTICATION_FAILURE_COUNT_NAME =
+            "Remaining Authentication Failure Count";
+    static final String LAST_LOGIN_TIME_NAME = "Last Login Time";
+    static final String SECONDS_UNTIL_IDLE_LOCKOUT_NAME = "Seconds Until Idle Lockout";
+    static final String PASSWORD_RESET_STATE_NAME = "Password Reset State";
+    static final String SECONDS_UNTIL_PASSWORD_RESET_LOCKOUT_NAME =
+            "Seconds Until Password Reset Lockout";
+    static final String GRACE_LOGIN_USE_TIMES_NAME = "Grace Login Use Times";
+    static final String REMAINING_GRACE_LOGIN_COUNT_NAME = "Remaining Grace Login Count";
+    static final String PASSWORD_CHANGED_BY_REQUIRED_TIME_NAME =
+            "Password Changed By Required Time";
+    static final String SECONDS_UNTIL_REQUIRED_CHANGE_TIME_NAME =
+            "Seconds Until Required Change Time";
+    static final String PASSWORD_HISTORY_NAME = "Password History";
+
+    /** A decoder which can be used to decode password policy state extended operation requests. */
+    public static final RequestDecoder REQUEST_DECODER = new RequestDecoder();
+
+    /** No need to expose this. */
+    private static final ResultDecoder RESULT_DECODER = new ResultDecoder();
+
+    static ByteString encode(final String targetUser,
+            final List<PasswordPolicyStateOperation> operations) {
+        final ByteStringBuilder buffer = new ByteStringBuilder(6);
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+
+        try {
+            writer.writeStartSequence();
+            writer.writeOctetString(targetUser);
+            if (!operations.isEmpty()) {
+                writer.writeStartSequence();
+                for (final PasswordPolicyStateOperation operation : operations) {
+                    writer.writeStartSequence();
+                    writer.writeEnumerated(operation.getOperationType().ordinal());
+                    if (operation.getValues() != null) {
+                        writer.writeStartSequence();
+                        for (final ByteString value : operation.getValues()) {
+                            writer.writeOctetString(value);
+                        }
+                        writer.writeEndSequence();
+                    }
+                    writer.writeEndSequence();
+                }
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+
+        return buffer.toByteString();
+    }
+
+    private static void decodeOperations(final ASN1Reader reader,
+            final PasswordPolicyStateOperationContainer container) throws IOException,
+            DecodeException {
+        // See if we have operations
+        if (reader.hasNextElement()) {
+            reader.readStartSequence();
+            int opType;
+            PasswordPolicyStateOperationType type;
+            while (reader.hasNextElement()) {
+                reader.readStartSequence();
+                // Read the opType
+                opType = reader.readEnumerated();
+                try {
+                    type = PasswordPolicyStateOperationType.values()[opType];
+                } catch (final IndexOutOfBoundsException iobe) {
+                    throw DecodeException.error(ERR_PWPSTATE_EXTOP_UNKNOWN_OP_TYPE.get(String
+                            .valueOf(opType)), iobe);
+                }
+
+                // See if we have any values
+                if (reader.hasNextElement()) {
+                    reader.readStartSequence();
+                    final ArrayList<ByteString> values = new ArrayList<>();
+                    while (reader.hasNextElement()) {
+                        values.add(reader.readOctetString());
+                    }
+                    reader.readEndSequence();
+                    container.addOperation(new MultiValueOperation(type, values));
+                } else {
+                    container.addOperation(type);
+                }
+                reader.readEndSequence();
+            }
+            reader.readEndSequence();
+        }
+    }
+
+    /** Creates a new password policy state extended request. */
+    public PasswordPolicyStateExtendedRequest() {
+        // Nothing to do.
+    }
+
+    /**
+     * Adds the provided authentication failure time to this request.
+     *
+     * @param date
+     *            The authentication failure time.
+     */
+    public void addAuthenticationFailureTime(final Date date) {
+        setDateProperty(ADD_AUTHENTICATION_FAILURE_TIMES, date);
+    }
+
+    /**
+     * Adds the provided grace login use time to this request.
+     *
+     * @param date
+     *            The grace login use time.
+     */
+    public void addGraceLoginUseTime(final Date date) {
+        setDateProperty(ADD_GRACE_LOGIN_USE_TIME, date);
+    }
+
+    @Override
+    public void addOperation(final PasswordPolicyStateOperation operation) {
+        operations.add(operation);
+    }
+
+    /** Clears the account disabled state. */
+    public void clearAccountDisabledState() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_ACCOUNT_DISABLED_STATE);
+    }
+
+    /** Clears the account expiration time. */
+    public void clearAccountExpirationTime() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_ACCOUNT_EXPIRATION_TIME);
+    }
+
+    /** Clears the authentication failure times. */
+    public void clearAuthenticationFailureTimes() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_AUTHENTICATION_FAILURE_TIMES);
+    }
+
+    /** Clears the grace login use times. */
+    public void clearGraceLoginUseTimes() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_GRACE_LOGIN_USE_TIMES);
+    }
+
+    /** Clears the last login time. */
+    public void clearLastLoginTime() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_LAST_LOGIN_TIME);
+    }
+
+    /** Clears the password changed by required time. */
+    public void clearPasswordChangedByRequiredTime() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_PASSWORD_CHANGED_BY_REQUIRED_TIME);
+    }
+
+    /** Clears the password changed time. */
+    public void clearPasswordChangedTime() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_PASSWORD_CHANGED_TIME);
+    }
+
+    /** Clears the password expiration warned time. */
+    public void clearPasswordExpirationWarnedTime() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_PASSWORD_EXPIRATION_WARNED_TIME);
+    }
+
+    /** Clears the password history. */
+    public void clearPasswordHistory() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_PASSWORD_HISTORY);
+    }
+
+    /** Clears the password reset state. */
+    public void clearPasswordResetState() {
+        operations.add(PasswordPolicyStateOperationType.CLEAR_PASSWORD_RESET_STATE);
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public Iterable<PasswordPolicyStateOperation> getOperations() {
+        return operations;
+    }
+
+    @Override
+    public ExtendedResultDecoder<PasswordPolicyStateExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public String getTargetUser() {
+        return targetUser;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return encode(targetUser, operations);
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    /** Returns the account disabled state. */
+    public void requestAccountDisabledState() {
+        operations.add(PasswordPolicyStateOperationType.GET_ACCOUNT_DISABLED_STATE);
+    }
+
+    /** Returns the account expiration time. */
+    public void requestAccountExpirationTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_ACCOUNT_EXPIRATION_TIME);
+    }
+
+    /** Returns the authentication failure times. */
+    public void requestAuthenticationFailureTimes() {
+        operations.add(PasswordPolicyStateOperationType.GET_AUTHENTICATION_FAILURE_TIMES);
+    }
+
+    /** Returns the grace login use times. */
+    public void requestGraceLoginUseTimes() {
+        operations.add(PasswordPolicyStateOperationType.GET_GRACE_LOGIN_USE_TIMES);
+    }
+
+    /** Returns the last login time. */
+    public void requestLastLoginTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_LAST_LOGIN_TIME);
+    }
+
+    /** Returns the password changed by required time. */
+    public void requestPasswordChangedByRequiredTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_CHANGED_BY_REQUIRED_TIME);
+    }
+
+    /** Returns the password changed time. */
+    public void requestPasswordChangedTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_CHANGED_TIME);
+    }
+
+    /** Returns the password expiration warned time. */
+    public void requestPasswordExpirationWarnedTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_EXPIRATION_WARNED_TIME);
+    }
+
+    /** Returns the password history. */
+    public void requestPasswordHistory() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_HISTORY);
+    }
+
+    /** Returns the password policy DN. */
+    public void requestPasswordPolicyDN() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_POLICY_DN);
+    }
+
+    /** Returns the password reset state. */
+    public void requestPasswordResetState() {
+        operations.add(PasswordPolicyStateOperationType.GET_PASSWORD_RESET_STATE);
+    }
+
+    /** Returns the remaining authentication failure count. */
+    public void requestRemainingAuthenticationFailureCount() {
+        operations.add(PasswordPolicyStateOperationType.GET_REMAINING_AUTHENTICATION_FAILURE_COUNT);
+    }
+
+    /** Returns the remaining grace login count. */
+    public void requestRemainingGraceLoginCount() {
+        operations.add(PasswordPolicyStateOperationType.GET_REMAINING_GRACE_LOGIN_COUNT);
+    }
+
+    /** Returns the seconds until account expiration. */
+    public void requestSecondsUntilAccountExpiration() {
+        operations.add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_ACCOUNT_EXPIRATION);
+    }
+
+    /** Returns the seconds until authentication failure unlock. */
+    public void requestSecondsUntilAuthenticationFailureUnlock() {
+        operations
+                .add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_AUTHENTICATION_FAILURE_UNLOCK);
+    }
+
+    /** Returns the seconds until idle lockout. */
+    public void requestSecondsUntilIdleLockout() {
+        operations.add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_IDLE_LOCKOUT);
+    }
+
+    /** Returns the seconds until password expiration. */
+    public void requestSecondsUntilPasswordExpiration() {
+        operations.add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_PASSWORD_EXPIRATION);
+    }
+
+    /** Returns the seconds until password expiration warning. */
+    public void requestSecondsUntilPasswordExpirationWarning() {
+        operations
+                .add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_PASSWORD_EXPIRATION_WARNING);
+    }
+
+    /** Returns the seconds until password reset lockout. */
+    public void requestSecondsUntilPasswordResetLockout() {
+        operations.add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_PASSWORD_RESET_LOCKOUT);
+    }
+
+    /** Returns the seconds until required change time. */
+    public void requestSecondsUntilRequiredChangeTime() {
+        operations.add(PasswordPolicyStateOperationType.GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME);
+    }
+
+    /**
+     * Sets the account disabled state.
+     *
+     * @param state
+     *            The account disabled state.
+     */
+    public void setAccountDisabledState(final boolean state) {
+        setBooleanProperty(SET_ACCOUNT_DISABLED_STATE, state);
+    }
+
+    /**
+     * Sets the account expiration time.
+     *
+     * @param date
+     *            The account expiration time.
+     */
+    public void setAccountExpirationTime(final Date date) {
+        setDateProperty(SET_ACCOUNT_EXPIRATION_TIME, date);
+    }
+
+    /**
+     * Sets the authentication failure times.
+     *
+     * @param dates
+     *            The authentication failure times.
+     */
+    public void setAuthenticationFailureTimes(final Date... dates) {
+        setDateProperties(SET_AUTHENTICATION_FAILURE_TIMES, dates);
+    }
+
+    /**
+     * Sets the grace login use times.
+     *
+     * @param dates
+     *            The grace login use times.
+     */
+    public void setGraceLoginUseTimes(final Date... dates) {
+        setDateProperties(SET_GRACE_LOGIN_USE_TIMES, dates);
+    }
+
+    /**
+     * Sets the last login time.
+     *
+     * @param date
+     *            The last login time.
+     */
+    public void setLastLoginTime(final Date date) {
+        setDateProperty(SET_LAST_LOGIN_TIME, date);
+    }
+
+    /**
+     * Sets the password changed by required time.
+     *
+     * @param state
+     *            The password changed by required time.
+     */
+    public void setPasswordChangedByRequiredTime(final boolean state) {
+        setBooleanProperty(SET_PASSWORD_CHANGED_BY_REQUIRED_TIME, state);
+    }
+
+    /**
+     * Sets the password changed time.
+     *
+     * @param date
+     *            The password changed time.
+     */
+    public void setPasswordChangedTime(final Date date) {
+        setDateProperty(SET_PASSWORD_CHANGED_TIME, date);
+    }
+
+    /**
+     * Sets the password expiration warned time.
+     *
+     * @param date
+     *            The password expiration warned time.
+     */
+    public void setPasswordExpirationWarnedTime(final Date date) {
+        setDateProperty(SET_PASSWORD_EXPIRATION_WARNED_TIME, date);
+    }
+
+    /**
+     * Sets the password reset state.
+     *
+     * @param state
+     *            The password reset state.
+     */
+    public void setPasswordResetState(final boolean state) {
+        setBooleanProperty(SET_PASSWORD_RESET_STATE, state);
+    }
+
+    private void setBooleanProperty(PasswordPolicyStateOperationType property, final boolean state) {
+        operations.add(new MultiValueOperation(property, ByteString.valueOfUtf8(String.valueOf(state))));
+    }
+
+    private void setDateProperty(PasswordPolicyStateOperationType property, final Date date) {
+        if (date != null) {
+            operations.add(new MultiValueOperation(property, toByteString(date)));
+        } else {
+            operations.add(property);
+        }
+    }
+
+    private void setDateProperties(PasswordPolicyStateOperationType property, final Date... dates) {
+        if (dates == null) {
+            operations.add(property);
+        } else {
+            final ArrayList<ByteString> times = new ArrayList<>(dates.length);
+            for (final Date date : dates) {
+                times.add(toByteString(date));
+            }
+            operations.add(new MultiValueOperation(property, times));
+        }
+    }
+
+    private ByteString toByteString(final Date date) {
+        return ByteString.valueOfUtf8(formatAsGeneralizedTime(date));
+    }
+
+    @Override
+    public void setTargetUser(String targetUser) {
+        this.targetUser = targetUser != null ? targetUser : "";
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordPolicyStateExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", targetUser=");
+        builder.append(targetUser);
+        builder.append(", operations=");
+        builder.append(operations);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedResult.java
new file mode 100644
index 0000000..ad1779b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateExtendedResult.java
@@ -0,0 +1,103 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.extensions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResult;
+
+/** The password policy state extended result. */
+public final class PasswordPolicyStateExtendedResult extends
+        AbstractExtendedResult<PasswordPolicyStateExtendedResult> implements
+        PasswordPolicyStateOperationContainer {
+    private String targetUser = "";
+    private final List<PasswordPolicyStateOperation> operations = new ArrayList<>();
+
+    /**
+     * Creates a new password policy state extended result with the provided
+     * result code.
+     *
+     * @param resultCode
+     *            The result code.
+     */
+    public PasswordPolicyStateExtendedResult(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public void addOperation(final PasswordPolicyStateOperation operation) {
+        operations.add(operation);
+    }
+
+    @Override
+    public String getOID() {
+        // No response name defined.
+        return PasswordPolicyStateExtendedRequest.OID;
+    }
+
+    @Override
+    public Iterable<PasswordPolicyStateOperation> getOperations() {
+        return operations;
+    }
+
+    @Override
+    public String getTargetUser() {
+        return targetUser;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return PasswordPolicyStateExtendedRequest.encode(targetUser, operations);
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public void setTargetUser(String targetUser) {
+        this.targetUser = targetUser != null ? targetUser : "";
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordPolicyStateExtendedResponse(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", responseName=");
+        builder.append(getOID());
+        builder.append(", targetUser=");
+        builder.append(targetUser);
+        builder.append(", operations=");
+        builder.append(operations);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperation.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperation.java
new file mode 100644
index 0000000..67d9458
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperation.java
@@ -0,0 +1,38 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package com.forgerock.opendj.ldap.extensions;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * Password policy state operation.
+ */
+public interface PasswordPolicyStateOperation {
+    /**
+     * Returns the type of operation.
+     *
+     * @return The type of operation.
+     */
+    PasswordPolicyStateOperationType getOperationType();
+
+    /**
+     * Returns the operation values.
+     *
+     * @return The operation values.
+     */
+    Iterable<ByteString> getValues();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationContainer.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationContainer.java
new file mode 100644
index 0000000..6a48f2c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationContainer.java
@@ -0,0 +1,56 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package com.forgerock.opendj.ldap.extensions;
+
+/**
+ * Password policy state operation container.
+ */
+interface PasswordPolicyStateOperationContainer {
+    /**
+     * Returns the name of the user targeted by this password policy state
+     * operation.
+     *
+     * @return The name of the user targeted by this password policy state
+     *         operation.
+     */
+    String getTargetUser();
+
+    /**
+     * Sets the name of the user targeted by this password policy state
+     * operation.
+     *
+     * @param targetUser
+     *            The name of the user targeted by this password policy state
+     *            operation.
+     */
+    void setTargetUser(String targetUser);
+
+    /**
+     * Adds an operation to this container.
+     *
+     * @param operation
+     *            The operation to be added.
+     */
+    void addOperation(PasswordPolicyStateOperation operation);
+
+    /**
+     * Returns the operations in this container.
+     *
+     * @return The operations in this container.
+     */
+    Iterable<PasswordPolicyStateOperation> getOperations();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationType.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationType.java
new file mode 100644
index 0000000..1039e0b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/PasswordPolicyStateOperationType.java
@@ -0,0 +1,186 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.extensions;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/** Password policy state operation type. */
+public enum PasswordPolicyStateOperationType implements PasswordPolicyStateOperation {
+    /** Get password policy DN operation. */
+    GET_PASSWORD_POLICY_DN(PasswordPolicyStateExtendedRequest.PASSWORD_POLICY_DN_NAME),
+
+    /** Get account disabled state operation. */
+    GET_ACCOUNT_DISABLED_STATE(PasswordPolicyStateExtendedRequest.ACCOUNT_DISABLED_STATE_NAME),
+
+    /** Set account disabled state operation. */
+    SET_ACCOUNT_DISABLED_STATE(PasswordPolicyStateExtendedRequest.ACCOUNT_DISABLED_STATE_NAME),
+
+    /** Clear account disabled state operation. */
+    CLEAR_ACCOUNT_DISABLED_STATE(PasswordPolicyStateExtendedRequest.ACCOUNT_DISABLED_STATE_NAME),
+
+    /** Get account expiration time operation. */
+    GET_ACCOUNT_EXPIRATION_TIME(PasswordPolicyStateExtendedRequest.ACCOUNT_EXPIRATION_TIME_NAME),
+
+    /** Set account expiration time operation. */
+    SET_ACCOUNT_EXPIRATION_TIME(PasswordPolicyStateExtendedRequest.ACCOUNT_EXPIRATION_TIME_NAME),
+
+    /** Clear account expiration time operation. */
+    CLEAR_ACCOUNT_EXPIRATION_TIME(PasswordPolicyStateExtendedRequest.ACCOUNT_EXPIRATION_TIME_NAME),
+
+    /** Get seconds until account expiration operation. */
+    GET_SECONDS_UNTIL_ACCOUNT_EXPIRATION(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_ACCOUNT_EXPIRATION_NAME),
+
+    /** Get password changed time operation. */
+    GET_PASSWORD_CHANGED_TIME(PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_TIME_NAME),
+
+    /** Set password changed time operation. */
+    SET_PASSWORD_CHANGED_TIME(PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_TIME_NAME),
+
+    /** Clear password changed time operation. */
+    CLEAR_PASSWORD_CHANGED_TIME(PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_TIME_NAME),
+
+    /** Get password expiration warned time operation. */
+    GET_PASSWORD_EXPIRATION_WARNED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_EXPIRATION_WARNED_TIME_NAME),
+
+    /** Set password expiration warned time operation. */
+    SET_PASSWORD_EXPIRATION_WARNED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_EXPIRATION_WARNED_TIME_NAME),
+
+    /** Clear password expiration warned time operation. */
+    CLEAR_PASSWORD_EXPIRATION_WARNED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_EXPIRATION_WARNED_TIME_NAME),
+
+    /** Get seconds until password expiration operation. */
+    GET_SECONDS_UNTIL_PASSWORD_EXPIRATION(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_PASSWORD_EXPIRATION_NAME),
+
+    /** Get seconds until password expiration warning operation. */
+    GET_SECONDS_UNTIL_PASSWORD_EXPIRATION_WARNING(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_PASSWORD_EXPIRATION_WARNING_NAME),
+
+    /** Get authentication failure times operation. */
+    GET_AUTHENTICATION_FAILURE_TIMES(
+            PasswordPolicyStateExtendedRequest.AUTHENTICATION_FAILURE_TIMES_NAME),
+
+    /** Add authentication failure times operation. */
+    ADD_AUTHENTICATION_FAILURE_TIMES(
+            PasswordPolicyStateExtendedRequest.AUTHENTICATION_FAILURE_TIMES_NAME),
+
+    /** Set authentication failure times operation. */
+    SET_AUTHENTICATION_FAILURE_TIMES(
+            PasswordPolicyStateExtendedRequest.AUTHENTICATION_FAILURE_TIMES_NAME),
+
+    /** Clear authentication failure times operation. */
+    CLEAR_AUTHENTICATION_FAILURE_TIMES(
+            PasswordPolicyStateExtendedRequest.AUTHENTICATION_FAILURE_TIMES_NAME),
+
+    /** Get seconds until authentication failure unlock operation. */
+    GET_SECONDS_UNTIL_AUTHENTICATION_FAILURE_UNLOCK(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_AUTHENTICATION_FAILURE_UNLOCK_NAME),
+
+    /** Get remaining authentication failure count operation. */
+    GET_REMAINING_AUTHENTICATION_FAILURE_COUNT(
+            PasswordPolicyStateExtendedRequest.REMAINING_AUTHENTICATION_FAILURE_COUNT_NAME),
+
+    /** Get last login time operation. */
+    GET_LAST_LOGIN_TIME(PasswordPolicyStateExtendedRequest.LAST_LOGIN_TIME_NAME),
+
+    /** Set last login time operation. */
+    SET_LAST_LOGIN_TIME(PasswordPolicyStateExtendedRequest.LAST_LOGIN_TIME_NAME),
+
+    /** Clear last login time operation. */
+    CLEAR_LAST_LOGIN_TIME(PasswordPolicyStateExtendedRequest.LAST_LOGIN_TIME_NAME),
+
+    /** Get seconds until idle lockout operation. */
+    GET_SECONDS_UNTIL_IDLE_LOCKOUT(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_IDLE_LOCKOUT_NAME),
+
+    /** Get password reset state operation. */
+    GET_PASSWORD_RESET_STATE(PasswordPolicyStateExtendedRequest.PASSWORD_RESET_STATE_NAME),
+
+    /** Set password reset state operation. */
+    SET_PASSWORD_RESET_STATE(PasswordPolicyStateExtendedRequest.PASSWORD_RESET_STATE_NAME),
+
+    /** Clear password reset state operation. */
+    CLEAR_PASSWORD_RESET_STATE(PasswordPolicyStateExtendedRequest.PASSWORD_RESET_STATE_NAME),
+
+    /** Get seconds until password reset lockout operation. */
+    GET_SECONDS_UNTIL_PASSWORD_RESET_LOCKOUT(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_PASSWORD_RESET_LOCKOUT_NAME),
+
+    /** Get grace login use times operation. */
+    GET_GRACE_LOGIN_USE_TIMES(PasswordPolicyStateExtendedRequest.GRACE_LOGIN_USE_TIMES_NAME),
+
+    /** Add grace login use times operation. */
+    ADD_GRACE_LOGIN_USE_TIME(PasswordPolicyStateExtendedRequest.GRACE_LOGIN_USE_TIMES_NAME),
+
+    /** Set grace login use times operation. */
+    SET_GRACE_LOGIN_USE_TIMES(PasswordPolicyStateExtendedRequest.GRACE_LOGIN_USE_TIMES_NAME),
+
+    /** Clear grace login use times operation. */
+    CLEAR_GRACE_LOGIN_USE_TIMES(PasswordPolicyStateExtendedRequest.GRACE_LOGIN_USE_TIMES_NAME),
+
+    /** Get remaining grace login count operation. */
+    GET_REMAINING_GRACE_LOGIN_COUNT(
+            PasswordPolicyStateExtendedRequest.REMAINING_GRACE_LOGIN_COUNT_NAME),
+
+    /** Get password changed by required time operation. */
+    GET_PASSWORD_CHANGED_BY_REQUIRED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_BY_REQUIRED_TIME_NAME),
+
+    /** Set password changed by required time operation. */
+    SET_PASSWORD_CHANGED_BY_REQUIRED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_BY_REQUIRED_TIME_NAME),
+
+    /** Clear password changed by required time operation. */
+    CLEAR_PASSWORD_CHANGED_BY_REQUIRED_TIME(
+            PasswordPolicyStateExtendedRequest.PASSWORD_CHANGED_BY_REQUIRED_TIME_NAME),
+
+    /** Get seconds until required change time operation. */
+    GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME(
+            PasswordPolicyStateExtendedRequest.SECONDS_UNTIL_REQUIRED_CHANGE_TIME_NAME),
+
+    /** Get password history operation. */
+    GET_PASSWORD_HISTORY(PasswordPolicyStateExtendedRequest.PASSWORD_HISTORY_NAME),
+
+    /** Clear password history operation. */
+    CLEAR_PASSWORD_HISTORY(PasswordPolicyStateExtendedRequest.PASSWORD_HISTORY_NAME);
+
+    private String propertyName;
+
+    private PasswordPolicyStateOperationType(final String propertyName) {
+        this.propertyName = propertyName;
+    }
+
+    @Override
+    public PasswordPolicyStateOperationType getOperationType() {
+        return this;
+    }
+
+    @Override
+    public Iterable<ByteString> getValues() {
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        return propertyName;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/package-info.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/package-info.java
new file mode 100644
index 0000000..5de736e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes implementing Sun proprietary LDAP extended operations.
+ */
+package com.forgerock.opendj.ldap.extensions;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ASCIICharProp.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ASCIICharProp.java
new file mode 100644
index 0000000..3a6bf67
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ASCIICharProp.java
@@ -0,0 +1,316 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+/**
+ * A {@code ASCIICharProp} provides fast access to ASCII character properties.
+ * In particular, the ability to query whether or not a character is a letter, a
+ * digit, hexadecimal character, as well as various methods for performing
+ * character conversions.
+ * <p>
+ * The methods in this class do not perform memory allocations nor calculations
+ * and so can be used safely in high performance situations.
+ */
+public final class ASCIICharProp implements Comparable<ASCIICharProp> {
+    private final char c;
+
+    private final char upperCaseChar;
+
+    private final char lowerCaseChar;
+
+    private final boolean isUpperCaseChar;
+
+    private final boolean isLowerCaseChar;
+
+    private final boolean isDigit;
+
+    private final boolean isLetter;
+
+    private final boolean isKeyChar;
+
+    private final boolean isCompatKeyChar;
+
+    private final boolean isHexChar;
+
+    private final int hexValue;
+
+    private final int decimalValue;
+
+    private final String stringValue;
+
+    private static final ASCIICharProp[] CHAR_PROPS = new ASCIICharProp[128];
+
+    static {
+        for (int i = 0; i < 128; i++) {
+            CHAR_PROPS[i] = new ASCIICharProp((char) i);
+        }
+    }
+
+    /**
+     * Returns the character properties for the provided ASCII character. If a
+     * non-ASCII character is provided then this method returns {@code null}.
+     *
+     * @param c
+     *            The ASCII character.
+     * @return The character properties for the provided ASCII character, or
+     *         {@code null} if {@code c} is greater than {@code \u007F} .
+     */
+    public static ASCIICharProp valueOf(final char c) {
+        if (c < 128) {
+            return CHAR_PROPS[c];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the character properties for the provided ASCII character. If a
+     * non-ASCII character is provided then this method returns {@code null}.
+     *
+     * @param c
+     *            The ASCII character.
+     * @return The character properties for the provided ASCII character, or
+     *         {@code null} if {@code c} is less than zero or greater than
+     *         {@code \u007F} .
+     */
+    public static ASCIICharProp valueOf(final int c) {
+        if (c >= 0 && c < 128) {
+            return CHAR_PROPS[c];
+        } else {
+            return null;
+        }
+    }
+
+    private ASCIICharProp(final char c) {
+        this.c = c;
+        this.stringValue = new String(new char[] { c });
+
+        if (c >= 'a' && c <= 'z') {
+            this.upperCaseChar = (char) (c - 32);
+            this.lowerCaseChar = c;
+            this.isUpperCaseChar = false;
+            this.isLowerCaseChar = true;
+            this.isDigit = false;
+            this.isLetter = true;
+            this.isKeyChar = true;
+            this.isCompatKeyChar = true;
+            this.decimalValue = -1;
+            if (c >= 'a' && c <= 'f') {
+                this.isHexChar = true;
+                this.hexValue = c - 87;
+            } else {
+                this.isHexChar = false;
+                this.hexValue = -1;
+            }
+        } else if (c >= 'A' && c <= 'Z') {
+            this.upperCaseChar = c;
+            this.lowerCaseChar = (char) (c + 32);
+            this.isUpperCaseChar = true;
+            this.isLowerCaseChar = false;
+            this.isDigit = false;
+            this.isLetter = true;
+            this.isKeyChar = true;
+            this.isCompatKeyChar = true;
+            this.decimalValue = -1;
+            if (c >= 'A' && c <= 'F') {
+                this.isHexChar = true;
+                this.hexValue = c - 55;
+            } else {
+                this.isHexChar = false;
+                this.hexValue = -1;
+            }
+        } else if (c >= '0' && c <= '9') {
+            this.upperCaseChar = c;
+            this.lowerCaseChar = c;
+            this.isUpperCaseChar = false;
+            this.isLowerCaseChar = false;
+            this.isDigit = true;
+            this.isLetter = false;
+            this.isKeyChar = true;
+            this.isCompatKeyChar = true;
+            this.isHexChar = true;
+            this.hexValue = c - 48;
+            this.decimalValue = c - 48;
+        } else {
+            this.upperCaseChar = c;
+            this.lowerCaseChar = c;
+            this.isUpperCaseChar = false;
+            this.isLowerCaseChar = false;
+            this.isDigit = false;
+            this.isLetter = false;
+            this.isKeyChar = c == '-';
+            this.isCompatKeyChar = (c == '-') || (c == '.') || (c == '_') || (c == '=');
+            this.isHexChar = false;
+            this.hexValue = -1;
+            this.decimalValue = -1;
+        }
+    }
+
+    /**
+     * Returns the char value associated with this {@code ASCIICharProp}.
+     *
+     * @return The char value associated with this {@code ASCIICharProp}.
+     */
+    public char charValue() {
+        return c;
+    }
+
+    @Override
+    public int compareTo(final ASCIICharProp o) {
+        return c - o.c;
+    }
+
+    /**
+     * Returns the decimal value associated with this {@code ASCIICharProp}, or
+     * {@code -1} if the value is not a decimal digit.
+     *
+     * @return The decimal value associated with this {@code ASCIICharProp}, or
+     *         {@code -1} if the value is not a decimal digit.
+     */
+    public int decimalValue() {
+        return decimalValue;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+        return c;
+    }
+
+    /**
+     * Returns the hexadecimal value associated with this {@code ASCIICharProp}
+     * , or {@code -1} if the value is not a hexadecimal digit.
+     *
+     * @return The hexadecimal value associated with this {@code ASCIICharProp}
+     *         , or {@code -1} if the value is not a hexadecimal digit.
+     */
+    public int hexValue() {
+        return hexValue;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is a decimal digit.
+     *
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is a decimal digit.
+     */
+    public boolean isDigit() {
+        return isDigit;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is a hexadecimal digit.
+     *
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is a hexadecimal digit.
+     */
+    public boolean isHexDigit() {
+        return isHexChar;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is a {@code keychar} as defined in RFC 4512. A
+     * {@code keychar} is a letter, a digit, or a hyphen. When
+     * {@code allowCompatChars} is {@code true} the following illegal characters
+     * will be permitted:
+     *
+     * <pre>
+     * HYPHEN  = %x2D ; hyphen ("-")
+     * DOT     = %x2E ; period (".")
+     * EQUALS  = %x3D ; equals sign ("=")
+     * USCORE  = %x5F ; underscore ("_")
+     * </pre>
+     *
+     * @param allowCompatChars
+     *            {@code true} if certain illegal characters should be allowed
+     *            for compatibility reasons.
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is a {@code keychar}.
+     */
+    public boolean isKeyChar(final boolean allowCompatChars) {
+        return allowCompatChars ? isCompatKeyChar : isKeyChar;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is a letter.
+     *
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is a letter.
+     */
+    public boolean isLetter() {
+        return isLetter;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is a lower-case character.
+     *
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is a lower-case character.
+     */
+    public boolean isLowerCase() {
+        return isLowerCaseChar;
+    }
+
+    /**
+     * Indicates whether or not the char value associated with this
+     * {@code ASCIICharProp} is an upper-case character.
+     *
+     * @return {@code true} if the char value associated with this
+     *         {@code ASCIICharProp} is an upper-case character.
+     */
+    public boolean isUpperCase() {
+        return isUpperCaseChar;
+    }
+
+    /**
+     * Returns the lower-case char value associated with this
+     * {@code ASCIICharProp}.
+     *
+     * @return The lower-case char value associated with this
+     *         {@code ASCIICharProp}.
+     */
+    public char toLowerCase() {
+        return lowerCaseChar;
+    }
+
+    @Override
+    public String toString() {
+        return stringValue;
+    }
+
+    /**
+     * Returns the upper-case char value associated with this
+     * {@code ASCIICharProp}.
+     *
+     * @return The upper-case char value associated with this
+     *         {@code ASCIICharProp}.
+     */
+    public char toUpperCase() {
+        return upperCaseChar;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Collections2.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Collections2.java
new file mode 100644
index 0000000..c5940e7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Collections2.java
@@ -0,0 +1,260 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/** Additional {@code Collection} based utility methods. */
+public final class Collections2 {
+    private static class TransformedCollection<M, N, C extends Collection<M>> extends
+            AbstractCollection<N> implements Collection<N> {
+        protected final C collection;
+
+        protected final Function<? super M, ? extends N, NeverThrowsException> funcMtoN;
+
+        protected final Function<? super N, ? extends M, NeverThrowsException> funcNtoM;
+
+        protected TransformedCollection(final C collection,
+                final Function<? super M, ? extends N, NeverThrowsException> funcMtoN,
+                final Function<? super N, ? extends M, NeverThrowsException> funcNtoM) {
+            this.collection = collection;
+            this.funcMtoN = funcMtoN;
+            this.funcNtoM = funcNtoM;
+        }
+
+        @Override
+        public boolean add(final N e) {
+            return collection.add(funcNtoM.apply(e));
+        }
+
+        @Override
+        public void clear() {
+            collection.clear();
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public boolean contains(final Object o) {
+            return collection.contains(funcNtoM.apply((N) o));
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return collection.isEmpty();
+        }
+
+        @Override
+        public Iterator<N> iterator() {
+            return Iterators.transformedIterator(collection.iterator(), funcMtoN);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public boolean remove(final Object o) {
+            return collection.remove(funcNtoM.apply((N) o));
+        }
+
+        @Override
+        public int size() {
+            return collection.size();
+        }
+    }
+
+    private static final class TransformedList<M, N> extends
+            TransformedCollection<M, N, List<M>> implements List<N> {
+        private TransformedList(final List<M> list,
+                final Function<? super M, ? extends N, NeverThrowsException> funcMtoN,
+                final Function<? super N, ? extends M, NeverThrowsException> funcNtoM) {
+            super(list, funcMtoN, funcNtoM);
+        }
+
+        @Override
+        public void add(final int index, final N element) {
+            collection.add(index, funcNtoM.apply(element));
+        }
+
+        @Override
+        public boolean addAll(final int index, final Collection<? extends N> c) {
+            // We cannot transform c here due to type-safety.
+            boolean result = false;
+            for (final N e : c) {
+                result |= add(e);
+            }
+            return result;
+        }
+
+        @Override
+        public N get(final int index) {
+            return funcMtoN.apply(collection.get(index));
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public int indexOf(final Object o) {
+            return collection.indexOf(funcNtoM.apply((N) o));
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public int lastIndexOf(final Object o) {
+            return collection.lastIndexOf(funcNtoM.apply((N) o));
+        }
+
+        @Override
+        public ListIterator<N> listIterator() {
+            return listIterator(0);
+        }
+
+        @Override
+        public ListIterator<N> listIterator(final int index) {
+            final ListIterator<M> iterator = collection.listIterator(index);
+            return new ListIterator<N>() {
+
+                @Override
+                public void add(final N e) {
+                    iterator.add(funcNtoM.apply(e));
+                }
+
+                @Override
+                public boolean hasNext() {
+                    return iterator.hasNext();
+                }
+
+                @Override
+                public boolean hasPrevious() {
+                    return iterator.hasPrevious();
+                }
+
+                @Override
+                public N next() {
+                    return funcMtoN.apply(iterator.next());
+                }
+
+                @Override
+                public int nextIndex() {
+                    return iterator.nextIndex();
+                }
+
+                @Override
+                public N previous() {
+                    return funcMtoN.apply(iterator.previous());
+                }
+
+                @Override
+                public int previousIndex() {
+                    return iterator.previousIndex();
+                }
+
+                @Override
+                public void remove() {
+                    iterator.remove();
+                }
+
+                @Override
+                public void set(final N e) {
+                    iterator.set(funcNtoM.apply(e));
+                }
+
+            };
+        }
+
+        @Override
+        public N remove(final int index) {
+            return funcMtoN.apply(collection.remove(index));
+        }
+
+        @Override
+        public N set(final int index, final N element) {
+            final M result = collection.set(index, funcNtoM.apply(element));
+            return funcMtoN.apply(result);
+        }
+
+        @Override
+        public List<N> subList(final int fromIndex, final int toIndex) {
+            final List<M> subList = collection.subList(fromIndex, toIndex);
+            return new TransformedList<>(subList, funcMtoN, funcNtoM);
+        }
+    }
+
+    /**
+     * Returns a view of {@code collection} whose values have been mapped to
+     * elements of type {@code N} using {@code funcMtoN}. The returned
+     * collection supports all operations.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code collection}.
+     * @param <N>
+     *            The type of elements contained in the returned collection.
+     * @param collection
+     *            The collection to be transformed.
+     * @param funcMtoN
+     *            A function which maps values of type {@code M} to values of
+     *            type {@code N}. This function will be used when retrieving
+     *            values from {@code collection}.
+     * @param funcNtoM
+     *            A function which maps values of type {@code N} to values of
+     *            type {@code M}. This function will be used when performing
+     *            queries and adding values to {@code collection} .
+     * @return A view of {@code collection} whose values have been mapped to
+     *         elements of type {@code N} using {@code funcMtoN}.
+     */
+    public static <M, N> Collection<N> transformedCollection(final Collection<M> collection,
+            final Function<? super M, ? extends N, NeverThrowsException> funcMtoN,
+            final Function<? super N, ? extends M, NeverThrowsException> funcNtoM) {
+        return new TransformedCollection<>(collection, funcMtoN, funcNtoM);
+    }
+
+    /**
+     * Returns a view of {@code list} whose values have been mapped to elements
+     * of type {@code N} using {@code funcMtoN}. The returned list supports all
+     * operations.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code list}.
+     * @param <N>
+     *            The type of elements contained in the returned list.
+     * @param list
+     *            The list to be transformed.
+     * @param funcMtoN
+     *            A function which maps values of type {@code M} to values of
+     *            type {@code N}. This function will be used when retrieving
+     *            values from {@code list}.
+     * @param funcNtoM
+     *            A function which maps values of type {@code N} to values of
+     *            type {@code M}. This function will be used when performing
+     *            queries and adding values to {@code list} .
+     * @return A view of {@code list} whose values have been mapped to elements
+     *         of type {@code N} using {@code funcMtoN}.
+     */
+    public static <M, N> List<N> transformedList(final List<M> list,
+            final Function<? super M, ? extends N, NeverThrowsException> funcMtoN,
+            final Function<? super N, ? extends M, NeverThrowsException> funcNtoM) {
+        return new TransformedList<>(list, funcMtoN, funcNtoM);
+    }
+
+    /** Prevent instantiation. */
+    private Collections2() {
+        // Do nothing.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterables.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterables.java
new file mode 100644
index 0000000..5b29ddc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterables.java
@@ -0,0 +1,325 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/**
+ * Utility methods for manipulating {@link Iterable}s.
+ */
+public final class Iterables {
+    private static abstract class AbstractIterable<M> implements Iterable<M> {
+        @Override
+        public String toString() {
+            return Iterables.toString(this);
+        }
+    }
+
+    private static final class ArrayIterable<M> extends AbstractIterable<M> {
+        private final M[] a;
+
+        /** Constructed via factory methods. */
+        private ArrayIterable(final M[] a) {
+            this.a = a;
+        }
+
+        @Override
+        public Iterator<M> iterator() {
+            return Iterators.arrayIterator(a);
+        }
+    }
+
+    private static final class EmptyIterable<M> extends AbstractIterable<M> {
+        @Override
+        public Iterator<M> iterator() {
+            return Iterators.emptyIterator();
+        }
+    }
+
+    private static final class FilteredIterable<M, P> extends AbstractIterable<M> {
+        private final Iterable<M> iterable;
+        private final P parameter;
+        private final Predicate<? super M, P> predicate;
+
+        /** Constructed via factory methods. */
+        private FilteredIterable(final Iterable<M> iterable,
+                final Predicate<? super M, P> predicate, final P p) {
+            this.iterable = iterable;
+            this.predicate = predicate;
+            this.parameter = p;
+        }
+
+        @Override
+        public Iterator<M> iterator() {
+            return Iterators.filteredIterator(iterable.iterator(), predicate, parameter);
+        }
+    }
+
+    private static final class SingletonIterable<M> extends AbstractIterable<M> {
+        private final M value;
+
+        /** Constructed via factory methods. */
+        private SingletonIterable(final M value) {
+            this.value = value;
+        }
+
+        @Override
+        public Iterator<M> iterator() {
+            return Iterators.singletonIterator(value);
+        }
+    }
+
+    private static final class TransformedIterable<M, N> extends AbstractIterable<N> {
+        private final Function<? super M, ? extends N, NeverThrowsException> function;
+        private final Iterable<M> iterable;
+
+        /** Constructed via factory methods. */
+        private TransformedIterable(final Iterable<M> iterable,
+                final Function<? super M, ? extends N, NeverThrowsException> function) {
+            this.iterable = iterable;
+            this.function = function;
+        }
+
+        @Override
+        public Iterator<N> iterator() {
+            return Iterators.transformedIterator(iterable.iterator(), function);
+        }
+    }
+
+    private static final class UnmodifiableIterable<M> extends AbstractIterable<M> {
+        private final Iterable<M> iterable;
+
+        /** Constructed via factory methods. */
+        private UnmodifiableIterable(final Iterable<M> iterable) {
+            this.iterable = iterable;
+        }
+
+        @Override
+        public Iterator<M> iterator() {
+            return Iterators.unmodifiableIterator(iterable.iterator());
+        }
+    }
+
+    private static final Iterable<Object> EMPTY_ITERABLE = new EmptyIterable<>();
+
+    /**
+     * Returns an iterable containing the elements of {@code a}. The returned
+     * iterable's iterator does not support element removal via the
+     * {@code remove()} method.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code a}.
+     * @param a
+     *            The array of elements.
+     * @return An iterable containing the elements of {@code a}.
+     */
+    public static <M> Iterable<M> arrayIterable(final M[] a) {
+        return new ArrayIterable<>(a);
+    }
+
+    /**
+     * Returns an immutable empty iterable.
+     *
+     * @param <M>
+     *            The required type of the empty iterable.
+     * @return An immutable empty iterable.
+     */
+    @SuppressWarnings("unchecked")
+    public static <M> Iterable<M> emptyIterable() {
+        return (Iterable<M>) EMPTY_ITERABLE;
+    }
+
+    /**
+     * Returns a filtered view of {@code iterable} containing only those
+     * elements which match {@code predicate}. The returned iterable's iterator
+     * supports element removal via the {@code remove()} method subject to any
+     * constraints imposed by {@code iterable}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterable}.
+     * @param <P>
+     *            The type of the additional parameter to the predicate's
+     *            {@code matches} method. Use {@link java.lang.Void} for
+     *            predicates that do not need an additional parameter.
+     * @param iterable
+     *            The iterable to be filtered.
+     * @param predicate
+     *            The predicate.
+     * @param p
+     *            A predicate specified parameter.
+     * @return A filtered view of {@code iterable} containing only those
+     *         elements which match {@code predicate}.
+     */
+    public static <M, P> Iterable<M> filteredIterable(final Iterable<M> iterable,
+            final Predicate<? super M, P> predicate, final P p) {
+        return new FilteredIterable<>(iterable, predicate, p);
+    }
+
+    /**
+     * Returns a filtered view of {@code iterable} containing only those
+     * elements which match {@code predicate}. The returned iterable's iterator
+     * supports element removal via the {@code remove()} method subject to any
+     * constraints imposed by {@code iterable}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterable}.
+     * @param iterable
+     *            The iterable to be filtered.
+     * @param predicate
+     *            The predicate.
+     * @return A filtered view of {@code iterable} containing only those
+     *         elements which match {@code predicate}.
+     */
+    public static <M> Iterable<M> filteredIterable(final Iterable<M> iterable,
+            final Predicate<? super M, Void> predicate) {
+        return new FilteredIterable<>(iterable, predicate, null);
+    }
+
+    /**
+     * Returns {@code true} if the provided iterable does not contain any
+     * elements.
+     *
+     * @param iterable
+     *            The iterable.
+     * @return {@code true} if the provided iterable does not contain any
+     *         elements.
+     */
+    public static boolean isEmpty(final Iterable<?> iterable) {
+        if (iterable instanceof Collection) {
+            // Fall-through if possible and potentially avoid allocation.
+            return ((Collection<?>) iterable).isEmpty();
+        } else {
+            return !iterable.iterator().hasNext();
+        }
+    }
+
+    /**
+     * Returns an iterable containing the single element {@code value}. The
+     * returned iterable's iterator does not support element removal via the
+     * {@code remove()} method.
+     *
+     * @param <M>
+     *            The type of the single element {@code value}.
+     * @param value
+     *            The single element.
+     * @return An iterable containing the single element {@code value}.
+     */
+    public static <M> Iterable<M> singletonIterable(final M value) {
+        return new SingletonIterable<>(value);
+    }
+
+    /**
+     * Returns the number of elements contained in the provided iterable.
+     *
+     * @param iterable
+     *            The iterable.
+     * @return The number of elements contained in the provided iterable.
+     */
+    public static int size(final Iterable<?> iterable) {
+        if (iterable instanceof Collection) {
+            // Fall-through if possible and potentially benefit from constant time calculation.
+            return ((Collection<?>) iterable).size();
+        } else {
+            final Iterator<?> i = iterable.iterator();
+            int sz = 0;
+            while (i.hasNext()) {
+                i.next();
+                sz++;
+            }
+            return sz;
+        }
+    }
+
+    /**
+     * Returns a string representation of the provided iterable composed of an
+     * opening square bracket, followed by each element separated by commas, and
+     * then a closing square bracket.
+     *
+     * @param iterable
+     *            The iterable whose string representation is to be returned.
+     * @return A string representation of the provided iterable.
+     * @see java.util.AbstractCollection#toString()
+     */
+    public static String toString(final Iterable<?> iterable) {
+        if (iterable instanceof Collection) {
+            // Fall-through if possible.
+            return ((Collection<?>) iterable).toString();
+        } else {
+            final StringBuilder builder = new StringBuilder();
+            boolean firstValue = true;
+            builder.append('[');
+            for (final Object value : iterable) {
+                if (!firstValue) {
+                    builder.append(", ");
+                }
+                builder.append(value);
+                firstValue = false;
+            }
+            builder.append(']');
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Returns a view of {@code iterable} whose values have been mapped to
+     * elements of type {@code N} using {@code function}. The returned
+     * iterable's iterator supports element removal via the {@code remove()}
+     * method subject to any constraints imposed by {@code iterable}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterable}.
+     * @param <N>
+     *            The type of elements contained in the returned iterable.
+     * @param iterable
+     *            The iterable to be transformed.
+     * @param function
+     *            The function.
+     * @return A view of {@code iterable} whose values have been mapped to
+     *         elements of type {@code N} using {@code function}.
+     */
+    public static <M, N> Iterable<N> transformedIterable(final Iterable<M> iterable,
+            final Function<? super M, ? extends N, NeverThrowsException> function) {
+        return new TransformedIterable<>(iterable, function);
+    }
+
+    /**
+     * Returns a read-only view of {@code iterable} whose iterator does not
+     * support element removal via the {@code remove()}. Attempts to use the
+     * {@code remove()} method will result in a
+     * {@code UnsupportedOperationException}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterable}.
+     * @param iterable
+     *            The iterable to be made read-only.
+     * @return A read-only view of {@code iterable} whose iterator does not
+     *         support element removal via the {@code remove()}.
+     */
+    public static <M> Iterable<M> unmodifiableIterable(final Iterable<M> iterable) {
+        return new UnmodifiableIterable<>(iterable);
+    }
+
+    /** Prevent instantiation. */
+    private Iterables() {
+        // Do nothing.
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterators.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterators.java
new file mode 100644
index 0000000..bc5c51b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Iterators.java
@@ -0,0 +1,341 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/** Utility methods for manipulating {@link Iterator}s. */
+public final class Iterators {
+    private static final class ArrayIterator<M> implements Iterator<M> {
+        private int i;
+        private final M[] a;
+
+        /** Constructed via factory methods. */
+        private ArrayIterator(final M[] a) {
+            this.a = a;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return i < a.length;
+        }
+
+        @Override
+        public M next() {
+            if (hasNext()) {
+                return a[i++];
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+
+    private static final class EmptyIterator<M> implements Iterator<M> {
+        @Override
+        public boolean hasNext() {
+            return false;
+        }
+
+        @Override
+        public M next() {
+            throw new NoSuchElementException();
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static final class FilteredIterator<M, P> implements Iterator<M> {
+
+        private boolean hasNextMustIterate = true;
+        private final Iterator<M> iterator;
+        private M next;
+
+        private final P parameter;
+        private final Predicate<? super M, P> predicate;
+
+        /** Constructed via factory methods. */
+        private FilteredIterator(final Iterator<M> iterator,
+                final Predicate<? super M, P> predicate, final P p) {
+            this.iterator = iterator;
+            this.predicate = predicate;
+            this.parameter = p;
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (hasNextMustIterate) {
+                hasNextMustIterate = false;
+                while (iterator.hasNext()) {
+                    next = iterator.next();
+                    if (predicate.matches(next, parameter)) {
+                        return true;
+                    }
+                }
+                next = null;
+                return false;
+            } else {
+                return next != null;
+            }
+        }
+
+        @Override
+        public M next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            hasNextMustIterate = true;
+            return next;
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+
+    }
+
+    private static final class SingletonIterator<M> implements Iterator<M> {
+        private M value;
+
+        /** Constructed via factory methods. */
+        private SingletonIterator(final M value) {
+            this.value = value;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return value != null;
+        }
+
+        @Override
+        public M next() {
+            if (value != null) {
+                final M tmp = value;
+                value = null;
+                return tmp;
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+
+    private static final class TransformedIterator<M, N> implements Iterator<N> {
+
+        private final Function<? super M, ? extends N, NeverThrowsException> function;
+        private final Iterator<M> iterator;
+
+        /** Constructed via factory methods. */
+        private TransformedIterator(final Iterator<M> iterator,
+                final Function<? super M, ? extends N, NeverThrowsException> function) {
+            this.iterator = iterator;
+            this.function = function;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public N next() {
+            return function.apply(iterator.next());
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+
+    }
+
+    private static final class UnmodifiableIterator<M> implements Iterator<M> {
+        private final Iterator<M> iterator;
+
+        private UnmodifiableIterator(final Iterator<M> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public M next() {
+            return iterator.next();
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static final Iterator<Object> EMPTY_ITERATOR = new EmptyIterator<>();
+
+    /**
+     * Returns an iterator over the elements contained in {@code a}. The
+     * returned iterator does not support element removal via the
+     * {@code remove()} method.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code a}.
+     * @param a
+     *            The array of elements to be returned by the iterator.
+     * @return An iterator over the elements contained in {@code a}.
+     */
+    public static <M> Iterator<M> arrayIterator(final M[] a) {
+        return new ArrayIterator<>(a);
+    }
+
+    /**
+     * Returns an immutable empty iterator.
+     *
+     * @param <M>
+     *            The required type of the empty iterator.
+     * @return An immutable empty iterator.
+     */
+    @SuppressWarnings("unchecked")
+    public static <M> Iterator<M> emptyIterator() {
+        return (Iterator<M>) EMPTY_ITERATOR;
+    }
+
+    /**
+     * Returns a filtered view of {@code iterator} containing only those
+     * elements which match {@code predicate}. The returned iterator supports
+     * element removal via the {@code remove()} method subject to any
+     * constraints imposed by {@code iterator}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterator}.
+     * @param <P>
+     *            The type of the additional parameter to the predicate's
+     *            {@code matches} method. Use {@link java.lang.Void} for
+     *            predicates that do not need an additional parameter.
+     * @param iterator
+     *            The iterator to be filtered.
+     * @param predicate
+     *            The predicate.
+     * @param p
+     *            A predicate specified parameter.
+     * @return A filtered view of {@code iterator} containing only those
+     *         elements which match {@code predicate}.
+     */
+    public static <M, P> Iterator<M> filteredIterator(final Iterator<M> iterator,
+            final Predicate<? super M, P> predicate, final P p) {
+        return new FilteredIterator<>(iterator, predicate, p);
+    }
+
+    /**
+     * Returns a filtered view of {@code iterator} containing only those
+     * elements which match {@code predicate}. The returned iterator supports
+     * element removal via the {@code remove()} method subject to any
+     * constraints imposed by {@code iterator}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterator}.
+     * @param iterator
+     *            The iterator to be filtered.
+     * @param predicate
+     *            The predicate.
+     * @return A filtered view of {@code iterator} containing only those
+     *         elements which match {@code predicate}.
+     */
+    public static <M> Iterator<M> filteredIterator(final Iterator<M> iterator,
+            final Predicate<? super M, Void> predicate) {
+        return new FilteredIterator<>(iterator, predicate, null);
+    }
+
+    /**
+     * Returns an iterator containing the single element {@code value}. The
+     * returned iterator does not support element removal via the
+     * {@code remove()} method.
+     *
+     * @param <M>
+     *            The type of the single element {@code value}.
+     * @param value
+     *            The single element to be returned by the iterator.
+     * @return An iterator containing the single element {@code value}.
+     */
+    public static <M> Iterator<M> singletonIterator(final M value) {
+        return new SingletonIterator<>(value);
+    }
+
+    /**
+     * Returns a view of {@code iterator} whose values have been mapped to
+     * elements of type {@code N} using {@code function}. The returned iterator
+     * supports element removal via the {@code remove()} method subject to any
+     * constraints imposed by {@code iterator}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterator}.
+     * @param <N>
+     *            The type of elements contained in the returned iterator.
+     * @param iterator
+     *            The iterator to be transformed.
+     * @param function
+     *            The function.
+     * @return A view of {@code iterator} whose values have been mapped to
+     *         elements of type {@code N} using {@code function}.
+     */
+    public static <M, N> Iterator<N> transformedIterator(final Iterator<M> iterator,
+            final Function<? super M, ? extends N, NeverThrowsException> function) {
+        return new TransformedIterator<>(iterator, function);
+    }
+
+    /**
+     * Returns a read-only view of {@code iterator} which does not support
+     * element removal via the {@code remove()}. Attempts to use the
+     * {@code remove()} method will result in a
+     * {@code UnsupportedOperationException}.
+     *
+     * @param <M>
+     *            The type of elements contained in {@code iterator}.
+     * @param iterator
+     *            The iterator to be made read-only.
+     * @return A read-only view of {@code iterator} which does not support
+     *         element removal via the {@code remove()}.
+     */
+    public static <M> Iterator<M> unmodifiableIterator(final Iterator<M> iterator) {
+        return new UnmodifiableIterator<>(iterator);
+    }
+
+    /** Prevent instantiation. */
+    private Iterators() {
+        // Do nothing.
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/OperatingSystem.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/OperatingSystem.java
new file mode 100644
index 0000000..9a256a9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/OperatingSystem.java
@@ -0,0 +1,247 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+/**
+ * This class defines an enumeration that may be used to identify the operating system
+ * on which the JVM is running.
+ */
+public enum OperatingSystem {
+    /**
+     * The value indicating the AIX operating system.
+     */
+    AIX("AIX", false, false, true),
+
+    /**
+     * The value indicating the FreeBSD operating system.
+     */
+    FREEBSD("FreeBSD", false, false, true),
+
+    /**
+     * The value indicating the HP-UX operating system.
+     */
+    HPUX("HP UX", false, false, true),
+
+    /**
+     * The value indicating the Linux operating system.
+     */
+    LINUX("Linux", false, false, true),
+
+    /**
+     * The value indicating the Mac OS X operating system.
+     */
+    MACOSX("Mac OS X", false, true, true),
+
+    /**
+     * The value indicating the Solaris operating system.
+     */
+    SOLARIS("Solaris", false, false, true),
+
+    /**
+     * The value indicating the Windows operating system.
+     */
+    WINDOWS("Windows", true, false, false),
+
+    /**
+     * The value indicating the Windows 7 operating system.
+     */
+    WINDOWS7("Windows 7", true, false, false),
+
+    /**
+     * The value indicating the Windows Vista operating system.
+     */
+    WINDOWS_VISTA("Windows Vista", true, false, false),
+
+    /**
+     * The value indicating the Windows Server 2008 operating system.
+     */
+    WINDOWS_SERVER_2008("Server 2008", true, false, false),
+
+    /**
+     * The value indicating the z/OS operating system.
+     */
+    ZOS("z/OS", false, false, false),
+
+    /**
+     * The value indicating an unknown operating system.
+     */
+    UNKNOWN("Unknown", false, false, false);
+
+    /** The human-readable name for this operating system. */
+    private String osName;
+
+    private boolean isWindows;
+    private boolean isMacOS;
+    private boolean isUnixBased;
+
+    private static final OperatingSystem INSTANCE = forName(System.getProperty("os.name"));
+
+    /**
+     * Creates a new operating system value with the provided name.
+     *
+     * @param osName
+     *            The human-readable name for the operating system.
+     */
+    private OperatingSystem(String osName, boolean isWindows, boolean isMacOS, boolean isUnixBased) {
+        this.osName = osName;
+        this.isWindows = isWindows;
+        this.isMacOS = isMacOS;
+        this.isUnixBased = isUnixBased;
+    }
+
+    /**
+     * Retrieves the human-readable name of this operating system.
+     *
+     * @return The human-readable name for this operating system.
+     */
+    @Override
+    public String toString() {
+        return osName;
+    }
+
+    /**
+     * Retrieves the operating system for the provided name.
+     *
+     * @param osName
+     *            The name for which to retrieve the corresponding operating system.
+     * @return The operating system for the provided name.
+     */
+    public static OperatingSystem forName(final String osName) {
+        if (osName == null) {
+            return UNKNOWN;
+        }
+
+        final String lowerName = osName.toLowerCase();
+        if (lowerName.contains("solaris") || lowerName.contains("sunos")) {
+            return SOLARIS;
+        } else if (lowerName.contains("linux")) {
+            return LINUX;
+        } else if (lowerName.contains("hp-ux")
+                || lowerName.contains("hp ux")
+                || lowerName.contains("hpux")) {
+            return HPUX;
+        } else if (lowerName.contains("aix")) {
+            return AIX;
+        } else if (lowerName.contains("windows")) {
+            if (lowerName.indexOf("windows 7") != -1) {
+                return WINDOWS7;
+            } else if (lowerName.indexOf("vista") != -1) {
+                return WINDOWS_VISTA;
+            } else if (lowerName.indexOf("server 2008") != -1) {
+                return WINDOWS_SERVER_2008;
+            }
+            return WINDOWS;
+        } else if (lowerName.contains("freebsd") || lowerName.contains("free bsd")) {
+            return FREEBSD;
+        } else if (lowerName.contains("macos x") || lowerName.contains("mac os x")) {
+            return MACOSX;
+        } else if (lowerName.contains("z/os")) {
+            return ZOS;
+        }
+        return UNKNOWN;
+    }
+
+    /**
+     * Returns the operating system on which the JVM is running.
+     *
+     * @return The operating system on which the JVM is running
+     */
+    public static OperatingSystem getOperatingSystem() {
+        return INSTANCE;
+    }
+
+    /**
+     * Indicates whether the underlying operating system is a Windows variant.
+     *
+     * @return {@code true} if the underlying operating system is a Windows variant, or {@code false} if not.
+     */
+    public static boolean isWindows() {
+        return INSTANCE.isWindows;
+    }
+
+    /**
+     * Indicates whether the underlying operating system is Windows Vista.
+     *
+     * @return {@code true} if the underlying operating system is Windows Vista, or {@code false} if not.
+     */
+    public static boolean isVista() {
+        return INSTANCE == WINDOWS_VISTA;
+    }
+
+    /**
+     * Indicates whether the underlying operating system is Windows 2008.
+     *
+     * @return {@code true} if the underlying operating system is Windows 2008, or {@code false} if not.
+     */
+    public static boolean isWindows2008() {
+        return INSTANCE == WINDOWS_SERVER_2008;
+    }
+
+    /**
+     * Indicates whether the underlying operating system is Windows 7.
+     *
+     * @return {@code true} if the underlying operating system is Windows 7, or {@code false} if not.
+     */
+    public static boolean isWindows7() {
+        return INSTANCE == WINDOWS7;
+    }
+
+    /**
+     * Returns {@code true} if we are running under Mac OS and {@code false} otherwise.
+     *
+     * @return {@code true} if we are running under Mac OS and {@code false} otherwise.
+     */
+    public static boolean isMacOS() {
+        return INSTANCE.isMacOS;
+    }
+
+    /**
+     * Returns {@code true} if we are running under Unix and {@code false} otherwise.
+     *
+     * @return {@code true} if we are running under Unix and {@code false} otherwise.
+     */
+    public static boolean isUnix() {
+        return INSTANCE.isUnixBased;
+    }
+
+    /**
+     * Returns {@code true} if the OS is Unix based.
+     *
+     * @return {@code true} if the OS is Unix based.
+     */
+    public static boolean isUnixBased() {
+        return INSTANCE.isUnixBased;
+    }
+
+    /**
+     * Returns {@code true} if the OS is Unknown.
+     *
+     * @return {@code true} if the OS is Unknown.
+     */
+    public static boolean isUnknown() {
+        return INSTANCE == UNKNOWN;
+    }
+
+    /**
+     * Indicates whether the underlying operating system has UAC (User Access Control).
+     *
+     * @return {@code true} if the underlying operating system has UAC (User Access Control), or {@code false} if not.
+     */
+    public static boolean hasUAC() {
+        return isVista() || isWindows2008() || isWindows7();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/PackedLong.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/PackedLong.java
new file mode 100644
index 0000000..2d70272
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/PackedLong.java
@@ -0,0 +1,235 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+/**
+ * Provides static methods to manipulate compact long representation. Compact long allow to stores unsigned long values
+ * up to 56 bits using a variable number of bytes from 1 to 8. The binary representations of this compact encoding has
+ * the interesting properties of maintaining correct order of values when compared.
+ */
+public final class PackedLong {
+
+    /** Maximum size in bytes of a compact encoded value. */
+    public static final int MAX_COMPACT_SIZE = 8;
+
+    private PackedLong() {
+    }
+
+    private static final int[] DECODE_SIZE = new int[256];
+    static {
+        Arrays.fill(DECODE_SIZE, 0, 0x80, 1);
+        Arrays.fill(DECODE_SIZE, 0x80, 0xc0, 2);
+        Arrays.fill(DECODE_SIZE, 0xc0, 0xe0, 3);
+        Arrays.fill(DECODE_SIZE, 0xe0, 0xf0, 4);
+        Arrays.fill(DECODE_SIZE, 0xf0, 0xf8, 5);
+        Arrays.fill(DECODE_SIZE, 0xf8, 0xfc, 6);
+        Arrays.fill(DECODE_SIZE, 0xfc, 0xfe, 7);
+        Arrays.fill(DECODE_SIZE, 0xfe, 0x100, 8);
+    }
+
+    /** Maximum value that can be stored with a compacted representation. */
+    public static final long COMPACTED_MAX_VALUE = 0x00FFFFFFFFFFFFFFL;
+
+    /**
+     * Append the compact representation of the value into the {@link OutputStream}.
+     *
+     * @param os
+     *            {@link OutputStream} where the compact representation will be written.
+     * @param value
+     *            Value to be encoded and written in the compact long format.
+     * @throws IOException
+     *             if problem appear in the underlying {@link OutputStream}
+     * @return Number of bytes which has been written in the buffer.
+     */
+    public static int writeCompactUnsigned(OutputStream os, long value) throws IOException {
+        final int size = getEncodedSize(value);
+        switch (size) {
+        case 1:
+            os.write((int) value);
+            break;
+        case 2:
+            os.write((int) ((value >>> 8) | 0x80L));
+            os.write((int) value);
+            break;
+        case 3:
+            os.write((int) ((value >>> 16) | 0xc0L));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        case 4:
+            os.write((int) ((value >>> 24) | 0xe0L));
+            os.write((int) (value >>> 16));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        case 5:
+            os.write((int) ((value >>> 32) | 0xf0L));
+            os.write((int) (value >>> 24));
+            os.write((int) (value >>> 16));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        case 6:
+            os.write((int) ((value >>> 40) | 0xf8L));
+            os.write((int) (value >>> 32));
+            os.write((int) (value >>> 24));
+            os.write((int) (value >>> 16));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        case 7:
+            os.write((int) ((value >>> 48) | 0xfcL));
+            os.write((int) (value >>> 40));
+            os.write((int) (value >>> 32));
+            os.write((int) (value >>> 24));
+            os.write((int) (value >>> 16));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        case 8:
+            os.write(0xfe);
+            os.write((int) (value >>> 48));
+            os.write((int) (value >>> 40));
+            os.write((int) (value >>> 32));
+            os.write((int) (value >>> 24));
+            os.write((int) (value >>> 16));
+            os.write((int) (value >>> 8));
+            os.write((int) (value));
+            break;
+        default:
+            throw new IllegalArgumentException();
+        }
+        return size;
+    }
+
+    /**
+     * Get the number of bytes required to store the given value using the compact long representation.
+     *
+     * @param value
+     *            Value to get the compact representation's size.
+     * @return Number of bytes required to store the compact long representation of the value.
+     */
+    public static int getEncodedSize(long value) {
+        if (value < 0x80L) {
+            return 1;
+        } else if (value < 0x4000L) {
+            return 2;
+        } else if (value < 0x200000L) {
+            return 3;
+        } else if (value < 0x10000000L) {
+            return 4;
+        } else if (value < 0x800000000L) {
+            return 5;
+        } else if (value < 0x40000000000L) {
+            return 6;
+        } else if (value < 0x2000000000000L) {
+            return 7;
+        } else if (value < 0x100000000000000L) {
+            return 8;
+        } else {
+            throw new IllegalArgumentException("value out of range: " + value);
+        }
+    }
+
+    /**
+     * Decode and get the value of the compact long contained in the specified {@link InputStream}.
+     *
+     * @param is
+     *            Stream where to read the compact unsigned long
+     * @return The long value.
+     * @throws IOException
+     *             If the first byte cannot be read for any reason other than the end of the file, if the input stream
+     *             has been closed, or if some other I/O error occurs.
+     */
+    public static long readCompactUnsignedLong(InputStream is) throws IOException {
+        final int b0 = checkNotEndOfStream(is.read());
+        final int size = decodeSize(b0);
+        long value;
+        switch (size) {
+        case 1:
+            value = b2l((byte) b0);
+            break;
+        case 2:
+            value = (b0 & 0x3fL) << 8;
+            value |= checkNotEndOfStream(is.read());
+            break;
+        case 3:
+            value = (b0 & 0x1fL) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= checkNotEndOfStream(is.read());
+            break;
+        case 4:
+            value = (b0 & 0x0fL) << 24;
+            value |= ((long) checkNotEndOfStream(is.read())) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= is.read();
+            break;
+        case 5:
+            value = (b0 & 0x07L) << 32;
+            value |= ((long) checkNotEndOfStream(is.read())) << 24;
+            value |= ((long) checkNotEndOfStream(is.read())) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= (is.read());
+            break;
+        case 6:
+            value = (b0 & 0x03L) << 40;
+            value |= ((long) checkNotEndOfStream(is.read())) << 32;
+            value |= ((long) checkNotEndOfStream(is.read())) << 24;
+            value |= ((long) checkNotEndOfStream(is.read())) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= is.read();
+            break;
+        case 7:
+            value = (b0 & 0x01L) << 48;
+            value |= ((long) checkNotEndOfStream(is.read())) << 40;
+            value |= ((long) checkNotEndOfStream(is.read())) << 32;
+            value |= ((long) checkNotEndOfStream(is.read())) << 24;
+            value |= ((long) checkNotEndOfStream(is.read())) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= is.read();
+            break;
+        default:
+            value = ((long) checkNotEndOfStream(is.read())) << 48;
+            value |= ((long) checkNotEndOfStream(is.read())) << 40;
+            value |= ((long) checkNotEndOfStream(is.read())) << 32;
+            value |= ((long) checkNotEndOfStream(is.read())) << 24;
+            value |= ((long) checkNotEndOfStream(is.read())) << 16;
+            value |= ((long) checkNotEndOfStream(is.read())) << 8;
+            value |= is.read();
+        }
+        return value;
+    }
+
+    private static int checkNotEndOfStream(final int byteValue) {
+        if (byteValue == -1) {
+            throw new IllegalArgumentException("End of stream reached.");
+        }
+        return byteValue;
+    }
+
+    private static int decodeSize(int b) {
+        return DECODE_SIZE[b & 0xff];
+    }
+
+    private static long b2l(final byte b) {
+        return b & 0xffL;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Predicate.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Predicate.java
new file mode 100644
index 0000000..9cdda2f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/Predicate.java
@@ -0,0 +1,43 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package com.forgerock.opendj.util;
+
+/**
+ * Predicates transform input values of type {@code M} to a boolean output value
+ * and are typically used for performing filtering.
+ *
+ * @param <M>
+ *            The type of input values matched by this predicate.
+ * @param <P>
+ *            The type of the additional parameter to this predicate's
+ *            {@code matches} method. Use {@link java.lang.Void} for predicates
+ *            that do not need an additional parameter.
+ */
+public interface Predicate<M, P> {
+    /**
+     * Indicates whether or not this predicate matches the provided input value
+     * of type {@code M}.
+     *
+     * @param value
+     *            The input value for which to make the determination.
+     * @param p
+     *            A predicate specified parameter.
+     * @return {@code true} if this predicate matches {@code value}, otherwise
+     *         {@code false}.
+     */
+    boolean matches(M value, P p);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ReferenceCountedObject.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ReferenceCountedObject.java
new file mode 100644
index 0000000..d7ff6ea
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/ReferenceCountedObject.java
@@ -0,0 +1,156 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+
+/**
+ * An object which is lazily created when first referenced, and destroyed when
+ * the last reference is released.
+ *
+ * @param <T>
+ *            The type of referenced object.
+ */
+public abstract class ReferenceCountedObject<T> {
+
+    /**
+     * A reference to the reference counted object which will automatically be
+     * released during garbage collection.
+     */
+    public final class Reference {
+        /**
+         * The value will be accessed by the finalizer thread so it needs to be
+         * volatile in order to ensure that updates are published.
+         */
+        private volatile T value;
+
+        private Reference(final T value) {
+            this.value = value;
+        }
+
+        /**
+         * Returns the referenced object.
+         *
+         * @return The referenced object.
+         * @throws NullPointerException
+         *             If the referenced object has already been released.
+         */
+        public T get() {
+            if (value == null) {
+                throw new NullPointerException(); // Fail-fast.
+            }
+            return value;
+        }
+
+        /**
+         * Decrements the reference count for the reference counted object if
+         * this reference refers to the reference counted instance. If the
+         * reference count drops to zero then the referenced object will be
+         * destroyed.
+         */
+        public void release() {
+            T instanceToRelease = null;
+            synchronized (lock) {
+                if (value != null) {
+                    if (instance == value && --refCount == 0) {
+                        // This was the last reference.
+                        instanceToRelease = value;
+                        instance = null;
+                    }
+
+                    /*
+                     * Force NPE for subsequent get() attempts and prevent
+                     * multiple releases.
+                     */
+                    value = null;
+                }
+            }
+            if (instanceToRelease != null) {
+                destroyInstance(instanceToRelease);
+            }
+        }
+
+        /**
+         * Provide a finalizer because reference counting is intended for
+         * expensive rarely created resources which should not be accidentally
+         * left around.
+         */
+        @Override
+        protected void finalize() {
+            release();
+        }
+    }
+
+    private T instance;
+    private final Object lock = new Object();
+    private int refCount;
+
+    /**
+     * Creates a new referenced object whose reference count is initially zero.
+     */
+    protected ReferenceCountedObject() {
+        // Nothing to do.
+    }
+
+    /**
+     * Returns a reference to the reference counted object.
+     *
+     * @return A reference to the reference counted object.
+     */
+    public final Reference acquire() {
+        synchronized (lock) {
+            if (refCount++ == 0) {
+                assert instance == null;
+                instance = newInstance();
+            }
+            assert instance != null;
+            return new Reference(instance);
+        }
+    }
+
+    /**
+     * Returns a reference to the provided object or, if it is {@code null}, a
+     * reference to the reference counted object.
+     *
+     * @param value
+     *            The object to be referenced, or {@code null} if the reference
+     *            counted object should be used.
+     * @return A reference to the provided object or, if it is {@code null}, a
+     *         reference to the reference counted object.
+     */
+    public final Reference acquireIfNull(final T value) {
+        return value != null ? new Reference(value) : acquire();
+    }
+
+    /**
+     * Invoked when a reference is released and the reference count will become
+     * zero. Implementations should release any resources associated with the
+     * resource and should not return until the resources have been released.
+     *
+     * @param instance
+     *            The instance to be destroyed.
+     */
+    protected abstract void destroyInstance(T instance);
+
+    /**
+     * Invoked when a reference is acquired and the current reference count is
+     * zero. Implementations should create a new instance as fast as possible.
+     *
+     * @return The new instance.
+     */
+    protected abstract T newInstance();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
new file mode 100644
index 0000000..a7afa17
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
@@ -0,0 +1,136 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** An implementation of input stream that enforces an read size limit. */
+public class SizeLimitInputStream extends InputStream {
+    private int bytesRead;
+    private int markBytesRead;
+    private final int readLimit;
+    private final InputStream parentStream;
+
+    /**
+     * Creates a new a new size limit input stream.
+     *
+     * @param parentStream
+     *            The parent stream.
+     * @param readLimit
+     *            The size limit.
+     */
+    public SizeLimitInputStream(final InputStream parentStream, final int readLimit) {
+        this.parentStream = parentStream;
+        this.readLimit = readLimit;
+    }
+
+    @Override
+    public int available() throws IOException {
+        final int streamAvail = parentStream.available();
+        final int limitedAvail = readLimit - bytesRead;
+        return limitedAvail < streamAvail ? limitedAvail : streamAvail;
+    }
+
+    @Override
+    public void close() throws IOException {
+        parentStream.close();
+    }
+
+    /**
+     * Retrieves the number of bytes read from this stream.
+     *
+     * @return The number of bytes read from this stream.
+     */
+    public int getBytesRead() {
+        return bytesRead;
+    }
+
+    /**
+     * Retrieves the size limit of this stream.
+     *
+     * @return The size limit of this stream.
+     */
+    public int getSizeLimit() {
+        return readLimit;
+    }
+
+    @Override
+    public synchronized void mark(final int readlimit) {
+        parentStream.mark(readlimit);
+        markBytesRead = bytesRead;
+    }
+
+    @Override
+    public boolean markSupported() {
+        return parentStream.markSupported();
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (bytesRead >= readLimit) {
+            return -1;
+        }
+
+        final int b = parentStream.read();
+        if (b != -1) {
+            ++bytesRead;
+        }
+        return b;
+    }
+
+    @Override
+    public int read(final byte[] b, final int off, int len) throws IOException {
+        if (off < 0 || len < 0 || off + len > b.length) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        if (len == 0) {
+            return 0;
+        }
+
+        if (bytesRead >= readLimit) {
+            return -1;
+        }
+
+        if (bytesRead + len > readLimit) {
+            len = readLimit - bytesRead;
+        }
+
+        final int readLen = parentStream.read(b, off, len);
+        if (readLen > 0) {
+            bytesRead += readLen;
+        }
+        return readLen;
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        parentStream.reset();
+        bytesRead = markBytesRead;
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        if (bytesRead + n > readLimit) {
+            n = readLimit - bytesRead;
+        }
+
+        bytesRead += n;
+        return parentStream.skip(n);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StaticUtils.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StaticUtils.java
new file mode 100644
index 0000000..7a32e63
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StaticUtils.java
@@ -0,0 +1,789 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.ServiceLoader;
+import java.util.TimeZone;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ProviderNotFoundException;
+import org.forgerock.opendj.ldap.spi.Provider;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Utils;
+
+/**
+ * Common utility methods.
+ */
+public final class StaticUtils {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * Indicates whether the SDK is being used in debug mode. In debug mode
+     * components may enable certain instrumentation in order to help debug
+     * applications.
+     */
+    public static final boolean DEBUG_ENABLED =
+            System.getProperty("org.forgerock.opendj.debug") != null;
+
+    private static final boolean DEBUG_TO_STDERR = System
+            .getProperty("org.forgerock.opendj.debug.stderr") != null;
+
+    static {
+        logIfDebugEnabled("debugging enabled", null);
+    }
+
+    /**
+     * The end-of-line character for this platform.
+     */
+    public static final String EOL = System.getProperty("line.separator");
+
+    /**
+     * A zero-length byte array.
+     */
+    public static final byte[] EMPTY_BYTES = new byte[0];
+
+    /** The name of the time zone for universal coordinated time (UTC). */
+    private static final String TIME_ZONE_UTC = "UTC";
+
+    /** UTC TimeZone is assumed to never change over JVM lifetime. */
+    private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone(TIME_ZONE_UTC);
+
+    private static final String[][] BYTE_HEX_STRINGS = new String[2][256];
+    private static final int UPPER_CASE = 0;
+    private static final int LOWER_CASE = 1;
+
+    static {
+        String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
+        for (int i = 0; i < 256; i++) {
+            BYTE_HEX_STRINGS[UPPER_CASE][i] = hexDigits[i >>> 4] + hexDigits[i & 0xF];
+            BYTE_HEX_STRINGS[LOWER_CASE][i] = BYTE_HEX_STRINGS[UPPER_CASE][i].toLowerCase();
+        }
+    }
+
+    /**
+     * The default scheduler which should be used when the application does not
+     * provide one.
+     */
+    public static final ReferenceCountedObject<ScheduledExecutorService> DEFAULT_SCHEDULER =
+            new ReferenceCountedObject<ScheduledExecutorService>() {
+
+                @Override
+                protected ScheduledExecutorService newInstance() {
+                    final ThreadFactory factory =
+                            Utils.newThreadFactory(null, "OpenDJ LDAP SDK Default Scheduler", true);
+                    return Executors.newSingleThreadScheduledExecutor(factory);
+                }
+
+                @Override
+                protected void destroyInstance(ScheduledExecutorService instance) {
+                    instance.shutdownNow();
+                }
+            };
+
+    /**
+     * Retrieves a string representation of the provided byte in hexadecimal.
+     *
+     * @param b
+     *            The byte for which to retrieve the hexadecimal string
+     *            representation.
+     * @return The string representation of the provided byte in hexadecimal.
+     */
+    public static String byteToHex(final byte b) {
+        return BYTE_HEX_STRINGS[UPPER_CASE][b & 0xFF];
+    }
+
+    /**
+     * Retrieves a string representation of the provided byte in hexadecimal.
+     *
+     * @param b
+     *            The byte for which to retrieve the hexadecimal string
+     *            representation.
+     * @return The string representation of the provided byte in hexadecimal
+     *         using lowercase characters.
+     */
+    public static String byteToLowerHex(final byte b) {
+        return BYTE_HEX_STRINGS[LOWER_CASE][b & 0xFF];
+    }
+
+    /**
+     * Returns a string containing provided date formatted using the generalized
+     * time syntax.
+     *
+     * @param date
+     *            The date to be formated.
+     * @return The string containing provided date formatted using the
+     *         generalized time syntax.
+     * @throws NullPointerException
+     *             If {@code date} was {@code null}.
+     */
+    public static String formatAsGeneralizedTime(final Date date) {
+        return formatAsGeneralizedTime(date.getTime());
+    }
+
+    /**
+     * Returns a string containing provided date formatted using the generalized
+     * time syntax.
+     *
+     * @param date
+     *            The date to be formated.
+     * @return The string containing provided date formatted using the
+     *         generalized time syntax.
+     * @throws IllegalArgumentException
+     *             If {@code date} was invalid.
+     */
+    public static String formatAsGeneralizedTime(final long date) {
+        // Generalized time has the format yyyyMMddHHmmss.SSS'Z'
+
+        // Do this in a thread-safe non-synchronized fashion.
+        // (Simple)DateFormat is neither fast nor thread-safe.
+
+        final StringBuilder sb = new StringBuilder(19);
+
+        final GregorianCalendar calendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
+        calendar.setLenient(false);
+        calendar.setTimeInMillis(date);
+
+        // Format the year yyyy.
+        int n = calendar.get(Calendar.YEAR);
+        if (n < 0) {
+            final IllegalArgumentException e = new IllegalArgumentException("Year cannot be < 0:" + n);
+            throw e;
+        } else if (n < 10) {
+            sb.append("000");
+        } else if (n < 100) {
+            sb.append("00");
+        } else if (n < 1000) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the month MM.
+        n = calendar.get(Calendar.MONTH) + 1;
+        if (n < 10) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the day dd.
+        n = calendar.get(Calendar.DAY_OF_MONTH);
+        if (n < 10) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the hour HH.
+        n = calendar.get(Calendar.HOUR_OF_DAY);
+        if (n < 10) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the minute mm.
+        n = calendar.get(Calendar.MINUTE);
+        if (n < 10) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the seconds ss.
+        n = calendar.get(Calendar.SECOND);
+        if (n < 10) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the milli-seconds.
+        sb.append('.');
+        n = calendar.get(Calendar.MILLISECOND);
+        if (n < 10) {
+            sb.append("00");
+        } else if (n < 100) {
+            sb.append("0");
+        }
+        sb.append(n);
+
+        // Format the timezone (always Z).
+        sb.append('Z');
+
+        return sb.toString();
+    }
+
+    /**
+     * Construct a byte array containing the UTF-8 encoding of the provided
+     * character array.
+     *
+     * @param chars
+     *            The character array to convert to a UTF-8 byte array.
+     * @return A byte array containing the UTF-8 encoding of the provided
+     *         character array.
+     */
+    public static byte[] getBytes(final char[] chars) {
+        final Charset utf8 = Charset.forName("UTF-8");
+        final ByteBuffer buffer = utf8.encode(CharBuffer.wrap(chars));
+        final byte[] bytes = new byte[buffer.remaining()];
+        buffer.get(bytes);
+        return bytes;
+    }
+
+    /**
+     * Construct a byte array containing the UTF-8 encoding of the provided
+     * char sequence. This is significantly faster than calling
+     * {@link String#getBytes(String)} for ASCII strings.
+     *
+     * @param s
+     *            The char sequence to convert to a UTF-8 byte array.
+     * @return Returns a byte array containing the UTF-8 encoding of the
+     *         provided string.
+     */
+    public static byte[] getBytes(final CharSequence s) {
+        if (s == null) {
+            return null;
+        }
+
+        char c;
+        final int length = s.length();
+        final byte[] returnArray = new byte[length];
+        for (int i = 0; i < length; i++) {
+            c = s.charAt(i);
+            returnArray[i] = (byte) (c & 0x0000007F);
+            if (c != returnArray[i]) {
+                try {
+                    return s.toString().getBytes("UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    // TODO: I18N
+                    throw new RuntimeException("Unable to encode UTF-8 string " + s, e);
+                }
+            }
+        }
+
+        return returnArray;
+    }
+
+    /**
+     * Retrieves the best human-readable message for the provided exception. For
+     * exceptions defined in the OpenDJ project, it will attempt to use the
+     * message (combining it with the message ID if available). For some
+     * exceptions that use encapsulation (e.g., InvocationTargetException), it
+     * will be unwrapped and the cause will be treated. For all others, the
+     *
+     * @param t
+     *            The {@code Throwable} object for which to retrieve the
+     *            message.
+     * @return The human-readable message generated for the provided exception.
+     */
+    public static LocalizableMessage getExceptionMessage(final Throwable t) {
+        if (t instanceof LocalizableException) {
+            final LocalizableException ie = (LocalizableException) t;
+            return ie.getMessageObject();
+        } else if (t instanceof NullPointerException) {
+            final StackTraceElement[] stackElements = t.getStackTrace();
+
+            final LocalizableMessageBuilder message = new LocalizableMessageBuilder();
+            message.append("NullPointerException(");
+            message.append(stackElements[0].getFileName());
+            message.append(":");
+            message.append(stackElements[0].getLineNumber());
+            message.append(")");
+            return message.toMessage();
+        } else if (t instanceof InvocationTargetException && t.getCause() != null) {
+            return getExceptionMessage(t.getCause());
+        } else {
+            final StringBuilder message = new StringBuilder();
+
+            final String className = t.getClass().getName();
+            final int periodPos = className.lastIndexOf('.');
+            if (periodPos > 0) {
+                message.append(className.substring(periodPos + 1));
+            } else {
+                message.append(className);
+            }
+
+            message.append("(");
+            if (t.getMessage() == null) {
+                final StackTraceElement[] stackElements = t.getStackTrace();
+                message.append(stackElements[0].getFileName());
+                message.append(":");
+                message.append(stackElements[0].getLineNumber());
+
+                // FIXME Temporary to debug issue 2256.
+                if (t instanceof IllegalStateException) {
+                    for (int i = 1; i < stackElements.length; i++) {
+                        message.append(' ');
+                        message.append(stackElements[i].getFileName());
+                        message.append(":");
+                        message.append(stackElements[i].getLineNumber());
+                    }
+                }
+            } else {
+                message.append(t.getMessage());
+            }
+
+            message.append(")");
+
+            return LocalizableMessage.raw(message.toString());
+        }
+    }
+
+    /**
+     * Indicates whether the provided character is an ASCII alphabetic
+     * character.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided value is an uppercase or
+     *         lowercase ASCII alphabetic character, or <CODE>false</CODE> if it
+     *         is not.
+     */
+    public static boolean isAlpha(final char c) {
+        final ASCIICharProp cp = ASCIICharProp.valueOf(c);
+        return cp != null ? cp.isLetter() : false;
+    }
+
+    /**
+     * Indicates whether the provided character is a numeric digit.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided character represents a numeric
+     *         digit, or <CODE>false</CODE> if not.
+     */
+    public static boolean isDigit(final char c) {
+        final ASCIICharProp cp = ASCIICharProp.valueOf(c);
+        return cp != null ? cp.isDigit() : false;
+    }
+
+    /**
+     * Indicates whether the provided character is a hexadecimal digit.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided character represents a
+     *         hexadecimal digit, or <CODE>false</CODE> if not.
+     */
+    public static boolean isHexDigit(final char c) {
+        final ASCIICharProp cp = ASCIICharProp.valueOf(c);
+        return cp != null ? cp.isHexDigit() : false;
+    }
+
+    /**
+     * Indicates whether the provided character is a keychar.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @param allowCompatChars
+     *            {@code true} if certain illegal characters should be allowed
+     *            for compatibility reasons.
+     * @return <CODE>true</CODE> if the provided character represents a keychar,
+     *         or <CODE>false</CODE> if not.
+     */
+    public static boolean isKeyChar(final char c, final boolean allowCompatChars) {
+        final ASCIICharProp cp = ASCIICharProp.valueOf(c);
+        return cp != null ? cp.isKeyChar(allowCompatChars) : false;
+    }
+
+    /**
+     * Retrieves a stack trace from the provided exception as a single-line
+     * string.
+     *
+     * @param throwable
+     *            The exception for which to retrieve the stack trace.
+     * @param isFullStack
+     *            If {@code true}, provides the full stack trace, otherwise
+     *            provides a limited extract of stack trace
+     * @return A stack trace from the provided exception as a single-line
+     *         string.
+     */
+    public static String stackTraceToSingleLineString(Throwable throwable, boolean isFullStack) {
+        StringBuilder buffer = new StringBuilder();
+        stackTraceToSingleLineString(buffer, throwable, isFullStack);
+        return buffer.toString();
+    }
+
+
+    /**
+     * Appends a single-line string representation of the provided exception to
+     * the given buffer.
+     *
+     * @param buffer
+     *            The buffer to which the information is to be appended.
+     * @param throwable
+     *            The exception for which to retrieve the stack trace.
+     * @param isFullStack
+     *            If {@code true}, provides the full stack trace, otherwise
+     *            provides a limited extract of stack trace
+     */
+    public static void stackTraceToSingleLineString(StringBuilder buffer, Throwable throwable, boolean isFullStack) {
+        if (throwable == null) {
+            return;
+        }
+        if (isFullStack) {
+            // add class name and message of the exception
+            buffer.append(throwable.getClass().getName());
+            final String message = throwable.getLocalizedMessage();
+            if (message != null && message.length() != 0) {
+                buffer.append(": ").append(message);
+            }
+            // add first-level stack trace
+            for (StackTraceElement e : throwable.getStackTrace()) {
+                buffer.append(" / ");
+                buffer.append(e.getFileName());
+                buffer.append(":");
+                buffer.append(e.getLineNumber());
+            }
+            // add stack trace of all underlying causes
+            while (throwable.getCause() != null) {
+                throwable = throwable.getCause();
+                buffer.append("; caused by ");
+                buffer.append(throwable);
+
+                for (StackTraceElement e : throwable.getStackTrace()) {
+                    buffer.append(" / ");
+                    buffer.append(e.getFileName());
+                    buffer.append(":");
+                    buffer.append(e.getLineNumber());
+                }
+            }
+        } else {
+            if (throwable instanceof InvocationTargetException && throwable.getCause() != null) {
+                throwable = throwable.getCause();
+            }
+            // add class name and message of the exception
+            buffer.append(throwable.getClass().getSimpleName());
+            final String message = throwable.getLocalizedMessage();
+            if (message != null && message.length() != 0) {
+                buffer.append(": ").append(message);
+            }
+            // add first 20 items of the first-level stack trace
+            int i = 0;
+            buffer.append(" (");
+            for (StackTraceElement e : throwable.getStackTrace()) {
+                if (i > 20) {
+                    buffer.append(" ...");
+                    break;
+                } else if (i > 0) {
+                    buffer.append(" ");
+                }
+
+                buffer.append(e.getFileName());
+                buffer.append(":");
+                buffer.append(e.getLineNumber());
+                i++;
+            }
+
+            buffer.append(")");
+        }
+    }
+
+    /**
+     * Appends a lowercase string representation of the contents of the given
+     * byte array to the provided buffer. This implementation presumes that the
+     * provided string will contain only ASCII characters and is optimized for
+     * that case. However, if a non-ASCII character is encountered it will fall
+     * back on a more expensive algorithm that will work properly for non-ASCII
+     * characters.
+     *
+     * @param b
+     *            The byte array for which to obtain the lowercase string
+     *            representation.
+     * @param builder
+     *            The buffer to which the lowercase form of the string should be
+     *            appended.
+     * @return The updated {@code StringBuilder}.
+     */
+    public static StringBuilder toLowerCase(final ByteSequence b, final StringBuilder builder) {
+        Reject.ifNull(b, builder);
+
+        // FIXME: What locale should we use for non-ASCII characters? I
+        // think we should use default to the Unicode StringPrep.
+
+        final int origBufferLen = builder.length();
+        final int length = b.length();
+
+        for (int i = 0; i < length; i++) {
+            final int c = b.byteAt(i);
+
+            if (c < 0) {
+                builder.replace(origBufferLen, builder.length(), b.toString().toLowerCase(
+                        Locale.ENGLISH));
+                return builder;
+            }
+
+            // At this point 0 <= 'c' <= 128.
+            final ASCIICharProp cp = ASCIICharProp.valueOf(c);
+            builder.append(cp.toLowerCase());
+        }
+
+        return builder;
+    }
+
+    /**
+     * Retrieves a lower-case representation of the given string. This
+     * implementation presumes that the provided string will contain only ASCII
+     * characters and is optimized for that case. However, if a non-ASCII
+     * character is encountered it will fall back on a more expensive algorithm
+     * that will work properly for non-ASCII characters.
+     *
+     * @param s
+     *            The string for which to obtain the lower-case representation.
+     * @return The lower-case representation of the given string.
+     */
+    public static String toLowerCase(final String s) {
+        Reject.ifNull(s);
+
+        // FIXME: What locale should we use for non-ASCII characters? I
+        // think we should use default to the Unicode StringPrep.
+
+        // This code is optimized for the case where the input string 's'
+        // has already been converted to lowercase.
+        final int length = s.length();
+        int i = 0;
+        ASCIICharProp cp = null;
+
+        // Scan for non lowercase ASCII.
+        while (i < length) {
+            cp = ASCIICharProp.valueOf(s.charAt(i));
+            if (cp == null || cp.isUpperCase()) {
+                break;
+            }
+            i++;
+        }
+
+        if (i == length) {
+            // String was already lowercase ASCII.
+            return s;
+        }
+
+        // Found non lowercase ASCII.
+        final StringBuilder builder = new StringBuilder(length);
+        builder.append(s, 0, i);
+
+        if (cp != null) {
+            // Upper-case ASCII.
+            builder.append(cp.toLowerCase());
+            i++;
+            while (i < length) {
+                cp = ASCIICharProp.valueOf(s.charAt(i));
+                if (cp == null) {
+                    break;
+                }
+                builder.append(cp.toLowerCase());
+                i++;
+            }
+        }
+
+        if (i < length) {
+            builder.append(s.substring(i).toLowerCase(Locale.ENGLISH));
+        }
+
+        return builder.toString();
+    }
+
+    /**
+     * Appends a lower-case representation of the given string to the provided
+     * buffer. This implementation presumes that the provided string will
+     * contain only ASCII characters and is optimized for that case. However, if
+     * a non-ASCII character is encountered it will fall back on a more
+     * expensive algorithm that will work properly for non-ASCII characters.
+     *
+     * @param s
+     *            The string for which to obtain the lower-case representation.
+     * @param builder
+     *            The {@code StringBuilder} to which the lower-case form of the
+     *            string should be appended.
+     * @return The updated {@code StringBuilder}.
+     */
+    public static StringBuilder toLowerCase(final String s, final StringBuilder builder) {
+        Reject.ifNull(s);
+        Reject.ifNull(builder);
+
+        // FIXME: What locale should we use for non-ASCII characters? I
+        // think we should use default to the Unicode StringPrep.
+
+        final int length = s.length();
+        builder.ensureCapacity(builder.length() + length);
+
+        for (int i = 0; i < length; i++) {
+            final ASCIICharProp cp = ASCIICharProp.valueOf(s.charAt(i));
+            if (cp != null) {
+                builder.append(cp.toLowerCase());
+            } else {
+                // Non-ASCII.
+                builder.append(s.substring(i).toLowerCase(Locale.ENGLISH));
+                return builder;
+            }
+        }
+
+        return builder;
+    }
+
+    /**
+     * Returns a copy of the provided byte array.
+     *
+     * @param bytes
+     *            The byte array to be copied.
+     * @return A copy of the provided byte array.
+     */
+    public static byte[] copyOfBytes(final byte[] bytes) {
+        return Arrays.copyOf(bytes, bytes.length);
+    }
+
+    /**
+     * Returns the stack trace for the calling method, but only if SDK debugging
+     * is enabled.
+     *
+     * @return The stack trace for the calling method, but only if SDK debugging
+     *         is enabled, otherwise {@code null}..
+     */
+    public static StackTraceElement[] getStackTraceIfDebugEnabled() {
+        if (!DEBUG_ENABLED) {
+            return null;
+        }
+        final StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+        return Arrays.copyOfRange(stack, 2, stack.length);
+    }
+
+    /**
+     * Logs the provided message and stack trace if SDK debugging is enabled to
+     * either stderr or the debug logger.
+     *
+     * @param msg
+     *            The message to be logged.
+     * @param stackTrace
+     *            The stack trace, which may be {@code null}.
+     */
+    public static void logIfDebugEnabled(final String msg, final StackTraceElement[] stackTrace) {
+        if (DEBUG_ENABLED) {
+            final StringBuilder builder = new StringBuilder("OPENDJ SDK: ");
+            builder.append(msg);
+            if (stackTrace != null) {
+                builder.append(EOL);
+                for (StackTraceElement e : stackTrace) {
+                    builder.append("\tat ");
+                    builder.append(e);
+                    builder.append(EOL);
+                }
+            }
+            if (DEBUG_TO_STDERR) {
+                System.err.println(builder);
+            } else {
+                // TODO: I18N
+                logger.error(LocalizableMessage.raw("%s", builder));
+            }
+        }
+    }
+
+    /**
+     * Retrieves the printable ASCII representation of the provided byte.
+     *
+     * @param b
+     *            The byte for which to retrieve the printable ASCII
+     *            representation.
+     * @return The printable ASCII representation of the provided byte, or a
+     *         space if the provided byte does not have printable ASCII
+     *         representation.
+     */
+    public static char byteToASCII(final byte b) {
+        if (isPrintable(b)) {
+            return (char) b;
+        }
+        return ' ';
+    }
+
+    /**
+     * Returns whether the byte is a printable ASCII character.
+     *
+     * @param b
+     *          The byte for which to determine whether it is printable ASCII
+     * @return true if the byte is a printable ASCII character
+     */
+    public static boolean isPrintable(final byte b) {
+        return 32 <= b && b <= 126;
+    }
+
+    /** Prevent instantiation. */
+    private StaticUtils() {
+        // No implementation required.
+    }
+
+    /**
+     * Find and returns a provider of one or more implementations.
+     * <p>
+     * The provider is loaded using the {@code ServiceLoader} facility.
+     *
+     * @param <P>
+     *            type of provider
+     * @param providerClass
+     *            class of provider
+     * @param requestedProvider
+     *            name of provider to use, or {@code null} if no specific
+     *            provider is requested.
+     * @param classLoader
+     *            class loader to use to load the provider, or {@code null} to
+     *            use the default class loader (the context class loader of the
+     *            current thread).
+     * @return a provider
+     * @throws ProviderNotFoundException
+     *             if no provider is available or if the provider requested
+     *             using options is not found.
+     */
+    public static <P extends Provider> P getProvider(final Class<P> providerClass, final String requestedProvider,
+            final ClassLoader classLoader) {
+        ServiceLoader<P> loader = null;
+        if (classLoader != null) {
+            loader = ServiceLoader.load(providerClass, classLoader);
+        } else {
+            loader = ServiceLoader.load(providerClass);
+        }
+        StringBuilder providersFound = new StringBuilder();
+        for (P provider : loader) {
+            if (providersFound.length() > 0) {
+                providersFound.append(" ");
+            }
+            providersFound.append(provider.getName());
+            if (requestedProvider == null || provider.getName().equals(requestedProvider)) {
+                return provider;
+            }
+        }
+        if (providersFound.length() > 0) {
+            throw new ProviderNotFoundException(providerClass, requestedProvider, String.format(
+                    "The requested provider '%s' of type '%s' was not found. Available providers: %s",
+                    requestedProvider, providerClass.getName(), providersFound));
+        } else {
+            throw new ProviderNotFoundException(providerClass, requestedProvider, String.format(
+                    "There was no provider of type '%s' available.", providerClass.getName()));
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StringPrepProfile.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StringPrepProfile.java
new file mode 100644
index 0000000..d7443bf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/StringPrepProfile.java
@@ -0,0 +1,508 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines the "stringprep" profile as defined in RFC 4518. It must
+ * be used by all the matching rules that support unicode characters. For a
+ * complete list of such rules, refer to Section 4.2, RFC 4517.
+ */
+public final class StringPrepProfile {
+    /** A Table defining the mapped code-points as per RFC 3454. */
+    private static final class MappingTable {
+        /** Set of chars which are deleted from the incoming value. */
+        private static final HashSet<Character> MAP_2_NULL = new HashSet<>();
+        /** Set of chars which are replaced by a SPACE when found. */
+        private static final HashSet<Character> MAP_2_SPACE = new HashSet<>();
+
+        /**
+         * Table for case-folding. Map of Character and String containing
+         * uppercase and lowercase value as the key-value pair.
+         */
+        private static final HashMap<Character, String> CASE_MAP_TABLE = new HashMap<>();
+
+        static {
+            // Appendix B.1 RFC 3454.
+            final char[][] mapped2null =
+                    new char[][] { { '\u0000', '\u0008' }, { '\u000E', '\u001F' },
+                        { '\u007F', '\u0084' }, { '\u0086', '\u009F' }, { '\u00AD' }, { '\u034F' },
+                        { '\u06DD' }, { '\u070F' }, { '\u1806' }, { '\u180B', '\u180E' },
+                        { '\u200C', '\u200F' }, { '\u202A', '\u202E' }, { '\u2060', '\u2063' },
+                        { '\u206A', '\u206F' }, { '\uFE00', '\uFE0F' }, { '\uFEFF' },
+                        { '\uFFF9', '\uFFFC' } };
+
+            for (final char[] element : mapped2null) {
+                if (element.length == 1) {
+                    MAP_2_NULL.add(element[0]);
+                } else {
+                    // Contains a range of values.
+                    for (char c = element[0]; c <= element[1]; c++) {
+                        MAP_2_NULL.add(c);
+                    }
+                }
+            }
+
+            final char[] mapped2Space =
+                    new char[] { '\u0009', 0xA, '\u000B', '\u000C', 0xD, '\u0085', '\u00A0',
+                        '\u1680', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005',
+                        '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u2028', '\u2029',
+                        '\u202F', '\u205F', '\u3000' };
+
+            for (final char c : mapped2Space) {
+                MAP_2_SPACE.add(c);
+            }
+
+            // Appendix B.2 RFC 3454.
+            // Build an uppercase array and a lowercase array and create a map
+            // of both values.
+            final char[] upperCaseArr =
+                    new char[] { '\u0041', '\u0042', '\u0043', '\u0044', '\u0045', '\u0046',
+                        '\u0047', '\u0048', '\u0049', '\u004A', '\u004B', '\u004C', '\u004D',
+                        '\u004E', '\u004F', '\u0050', '\u0051', '\u0052', '\u0053', '\u0054',
+                        '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005A', '\u00B5',
+                        '\u00C0', '\u00C1', '\u00C2', '\u00C3', '\u00C4', '\u00C5', '\u00C6',
+                        '\u00C7', '\u00C8', '\u00C9', '\u00CA', '\u00CB', '\u00CC', '\u00CD',
+                        '\u00CE', '\u00CF', '\u00D0', '\u00D1', '\u00D2', '\u00D3', '\u00D4',
+                        '\u00D5', '\u00D6', '\u00D8', '\u00D9', '\u00DA', '\u00DB', '\u00DC',
+                        '\u00DD', '\u00DE', '\u00DF', '\u0100', '\u0102', '\u0104', '\u0106',
+                        '\u0108', '\u010A', '\u010C', '\u010E', '\u0110', '\u0112', '\u0114',
+                        '\u0116', '\u0118', '\u011A', '\u011C', '\u011E', '\u0120', '\u0122',
+                        '\u0124', '\u0126', '\u0128', '\u012A', '\u012C', '\u012E', '\u0130',
+                        '\u0132', '\u0134', '\u0136', '\u0139', '\u013B', '\u013D', '\u013F',
+                        '\u0141', '\u0143', '\u0145', '\u0147', '\u0149', '\u014A', '\u014C',
+                        '\u014E', '\u0150', '\u0152', '\u0154', '\u0156', '\u0158', '\u015A',
+                        '\u015C', '\u015E', '\u0160', '\u0162', '\u0164', '\u0166', '\u0168',
+                        '\u016A', '\u016C', '\u016E', '\u0170', '\u0172', '\u0174', '\u0176',
+                        '\u0178', '\u0179', '\u017B', '\u017D', '\u017F', '\u0181', '\u0182',
+                        '\u0184', '\u0186', '\u0187', '\u0189', '\u018A', '\u018B', '\u018E',
+                        '\u018F', '\u0190', '\u0191', '\u0193', '\u0194', '\u0196', '\u0197',
+                        '\u0198', '\u019C', '\u019D', '\u019F', '\u01A0', '\u01A2', '\u01A4',
+                        '\u01A6', '\u01A7', '\u01A9', '\u01AC', '\u01AE', '\u01AF', '\u01B1',
+                        '\u01B2', '\u01B3', '\u01B5', '\u01B7', '\u01B8', '\u01BC', '\u01C4',
+                        '\u01C5', '\u01C7', '\u01C8', '\u01CA', '\u01CB', '\u01CD', '\u01CF',
+                        '\u01D1', '\u01D3', '\u01D5', '\u01D7', '\u01D9', '\u01DB', '\u01DE',
+                        '\u01E0', '\u01E2', '\u01E4', '\u01E6', '\u01E8', '\u01EA', '\u01EC',
+                        '\u01EE', '\u01F0', '\u01F1', '\u01F2', '\u01F4', '\u01F6', '\u01F7',
+                        '\u01F8', '\u01FA', '\u01FC', '\u01FE', '\u0200', '\u0202', '\u0204',
+                        '\u0206', '\u0208', '\u020A', '\u020C', '\u020E', '\u0210', '\u0212',
+                        '\u0214', '\u0216', '\u0218', '\u021A', '\u021C', '\u021E', '\u0220',
+                        '\u0222', '\u0224', '\u0226', '\u0228', '\u022A', '\u022C', '\u022E',
+                        '\u0230', '\u0232', '\u0345', '\u037A', '\u0386', '\u0388', '\u0389',
+                        '\u038A', '\u038C', '\u038E', '\u038F', '\u0390', '\u0391', '\u0392',
+                        '\u0393', '\u0394', '\u0395', '\u0396', '\u0397', '\u0398', '\u0399',
+                        '\u039A', '\u039B', '\u039C', '\u039D', '\u039E', '\u039F', '\u03A0',
+                        '\u03A1', '\u03A3', '\u03A4', '\u03A5', '\u03A6', '\u03A7', '\u03A8',
+                        '\u03A9', '\u03AA', '\u03AB', '\u03B0', '\u03C2', '\u03D0', '\u03D1',
+                        '\u03D2', '\u03D3', '\u03D4', '\u03D5', '\u03D6', '\u03D8', '\u03DA',
+                        '\u03DC', '\u03DE', '\u03E0', '\u03E2', '\u03E4', '\u03E6', '\u03E8',
+                        '\u03EA', '\u03EC', '\u03EE', '\u03F0', '\u03F1', '\u03F2', '\u03F4',
+                        '\u03F5', '\u0400', '\u0401', '\u0402', '\u0403', '\u0404', '\u0405',
+                        '\u0406', '\u0407', '\u0408', '\u0409', '\u040A', '\u040B', '\u040C',
+                        '\u040D', '\u040E', '\u040F', '\u0410', '\u0411', '\u0412', '\u0413',
+                        '\u0414', '\u0415', '\u0416', '\u0417', '\u0418', '\u0419', '\u041A',
+                        '\u041B', '\u041C', '\u041D', '\u041E', '\u041F', '\u0420', '\u0421',
+                        '\u0422', '\u0423', '\u0424', '\u0425', '\u0426', '\u0427', '\u0428',
+                        '\u0429', '\u042A', '\u042B', '\u042C', '\u042D', '\u042E', '\u042F',
+                        '\u0460', '\u0462', '\u0464', '\u0466', '\u0468', '\u046A', '\u046C',
+                        '\u046E', '\u0470', '\u0472', '\u0474', '\u0476', '\u0478', '\u047A',
+                        '\u047C', '\u047E', '\u0480', '\u048A', '\u048C', '\u048E', '\u0490',
+                        '\u0492', '\u0494', '\u0496', '\u0498', '\u049A', '\u049C', '\u049E',
+                        '\u04A0', '\u04A2', '\u04A4', '\u04A6', '\u04A8', '\u04AA', '\u04AC',
+                        '\u04AE', '\u04B0', '\u04B2', '\u04B4', '\u04B6', '\u04B8', '\u04BA',
+                        '\u04BC', '\u04BE', '\u04C1', '\u04C3', '\u04C5', '\u04C7', '\u04C9',
+                        '\u04CB', '\u04CD', '\u04D0', '\u04D2', '\u04D4', '\u04D6', '\u04D8',
+                        '\u04DA', '\u04DC', '\u04DE', '\u04E0', '\u04E2', '\u04E4', '\u04E6',
+                        '\u04E8', '\u04EA', '\u04EC', '\u04EE', '\u04F0', '\u04F2', '\u04F4',
+                        '\u04F8', '\u0500', '\u0502', '\u0504', '\u0506', '\u0508', '\u050A',
+                        '\u050C', '\u050E', '\u0531', '\u0532', '\u0533', '\u0534', '\u0535',
+                        '\u0536', '\u0537', '\u0538', '\u0539', '\u053A', '\u053B', '\u053C',
+                        '\u053D', '\u053E', '\u053F', '\u0540', '\u0541', '\u0542', '\u0543',
+                        '\u0544', '\u0545', '\u0546', '\u0547', '\u0548', '\u0549', '\u054A',
+                        '\u054B', '\u054C', '\u054D', '\u054E', '\u054F', '\u0550', '\u0551',
+                        '\u0552', '\u0553', '\u0554', '\u0555', '\u0556', '\u0587', '\u1E00',
+                        '\u1E02', '\u1E04', '\u1E06', '\u1E08', '\u1E0A', '\u1E0C', '\u1E0E',
+                        '\u1E10', '\u1E12', '\u1E14', '\u1E16', '\u1E18', '\u1E1A', '\u1E1C',
+                        '\u1E1E', '\u1E20', '\u1E22', '\u1E24', '\u1E26', '\u1E28', '\u1E2A',
+                        '\u1E2C', '\u1E2E', '\u1E30', '\u1E32', '\u1E34', '\u1E36', '\u1E38',
+                        '\u1E3A', '\u1E3C', '\u1E3E', '\u1E40', '\u1E42', '\u1E44', '\u1E46',
+                        '\u1E48', '\u1E4A', '\u1E4C', '\u1E4E', '\u1E50', '\u1E52', '\u1E54',
+                        '\u1E56', '\u1E58', '\u1E5A', '\u1E5C', '\u1E5E', '\u1E60', '\u1E62',
+                        '\u1E64', '\u1E66', '\u1E68', '\u1E6A', '\u1E6C', '\u1E6E', '\u1E70',
+                        '\u1E72', '\u1E74', '\u1E76', '\u1E78', '\u1E7A', '\u1E7C', '\u1E7E',
+                        '\u1E80', '\u1E82', '\u1E84', '\u1E86', '\u1E88', '\u1E8A', '\u1E8C',
+                        '\u1E8E', '\u1E90', '\u1E92', '\u1E94', '\u1E96', '\u1E97', '\u1E98',
+                        '\u1E99', '\u1E9A', '\u1E9B', '\u1EA0', '\u1EA2', '\u1EA4', '\u1EA6',
+                        '\u1EA8', '\u1EAA', '\u1EAC', '\u1EAE', '\u1EB0', '\u1EB2', '\u1EB4',
+                        '\u1EB6', '\u1EB8', '\u1EBA', '\u1EBC', '\u1EBE', '\u1EC0', '\u1EC2',
+                        '\u1EC4', '\u1EC6', '\u1EC8', '\u1ECA', '\u1ECC', '\u1ECE', '\u1ED0',
+                        '\u1ED2', '\u1ED4', '\u1ED6', '\u1ED8', '\u1EDA', '\u1EDC', '\u1EDE',
+                        '\u1EE0', '\u1EE2', '\u1EE4', '\u1EE6', '\u1EE8', '\u1EEA', '\u1EEC',
+                        '\u1EEE', '\u1EF0', '\u1EF2', '\u1EF4', '\u1EF6', '\u1EF8', '\u1F08',
+                        '\u1F09', '\u1F0A', '\u1F0B', '\u1F0C', '\u1F0D', '\u1F0E', '\u1F0F',
+                        '\u1F18', '\u1F19', '\u1F1A', '\u1F1B', '\u1F1C', '\u1F1D', '\u1F28',
+                        '\u1F29', '\u1F2A', '\u1F2B', '\u1F2C', '\u1F2D', '\u1F2E', '\u1F2F',
+                        '\u1F38', '\u1F39', '\u1F3A', '\u1F3B', '\u1F3C', '\u1F3D', '\u1F3E',
+                        '\u1F3F', '\u1F48', '\u1F49', '\u1F4A', '\u1F4B', '\u1F4C', '\u1F4D',
+                        '\u1F50', '\u1F52', '\u1F54', '\u1F56', '\u1F59', '\u1F5B', '\u1F5D',
+                        '\u1F5F', '\u1F68', '\u1F69', '\u1F6A', '\u1F6B', '\u1F6C', '\u1F6D',
+                        '\u1F6E', '\u1F6F', '\u1F80', '\u1F81', '\u1F82', '\u1F83', '\u1F84',
+                        '\u1F85', '\u1F86', '\u1F87', '\u1F88', '\u1F89', '\u1F8A', '\u1F8B',
+                        '\u1F8C', '\u1F8D', '\u1F8E', '\u1F8F', '\u1F90', '\u1F91', '\u1F92',
+                        '\u1F93', '\u1F94', '\u1F95', '\u1F96', '\u1F97', '\u1F98', '\u1F99',
+                        '\u1F9A', '\u1F9B', '\u1F9C', '\u1F9D', '\u1F9E', '\u1F9F', '\u1FA0',
+                        '\u1FA1', '\u1FA2', '\u1FA3', '\u1FA4', '\u1FA5', '\u1FA6', '\u1FA7',
+                        '\u1FA8', '\u1FA9', '\u1FAA', '\u1FAB', '\u1FAC', '\u1FAD', '\u1FAE',
+                        '\u1FAF', '\u1FB2', '\u1FB3', '\u1FB4', '\u1FB6', '\u1FB7', '\u1FB8',
+                        '\u1FB9', '\u1FBA', '\u1FBB', '\u1FBC', '\u1FBE', '\u1FC2', '\u1FC3',
+                        '\u1FC4', '\u1FC6', '\u1FC7', '\u1FC8', '\u1FC9', '\u1FCA', '\u1FCB',
+                        '\u1FCC', '\u1FD2', '\u1FD3', '\u1FD6', '\u1FD7', '\u1FD8', '\u1FD9',
+                        '\u1FDA', '\u1FDB', '\u1FE2', '\u1FE3', '\u1FE4', '\u1FE6', '\u1FE7',
+                        '\u1FE8', '\u1FE9', '\u1FEA', '\u1FEB', '\u1FEC', '\u1FF2', '\u1FF3',
+                        '\u1FF4', '\u1FF6', '\u1FF7', '\u1FF8', '\u1FF9', '\u1FFA', '\u1FFB',
+                        '\u1FFC', '\u20A8', '\u2102', '\u2103', '\u2107', '\u2109', '\u210B',
+                        '\u210C', '\u210D', '\u2110', '\u2111', '\u2112', '\u2115', '\u2116',
+                        '\u2119', '\u211A', '\u211B', '\u211C', '\u211D', '\u2120', '\u2121',
+                        '\u2122', '\u2124', '\u2126', '\u2128', '\u212A', '\u212B', '\u212C',
+                        '\u212D', '\u2130', '\u2131', '\u2133', '\u213E', '\u213F', '\u2145',
+                        '\u2160', '\u2161', '\u2162', '\u2163', '\u2164', '\u2165', '\u2166',
+                        '\u2167', '\u2168', '\u2169', '\u216A', '\u216B', '\u216C', '\u216D',
+                        '\u216E', '\u216F', '\u24B6', '\u24B7', '\u24B8', '\u24B9', '\u24BA',
+                        '\u24BB', '\u24BC', '\u24BD', '\u24BE', '\u24BF', '\u24C0', '\u24C1',
+                        '\u24C2', '\u24C3', '\u24C4', '\u24C5', '\u24C6', '\u24C7', '\u24C8',
+                        '\u24C9', '\u24CA', '\u24CB', '\u24CC', '\u24CD', '\u24CE', '\u24CF',
+                        '\u3371', '\u3373', '\u3375', '\u3380', '\u3381', '\u3382', '\u3383',
+                        '\u3384', '\u3385', '\u3386', '\u3387', '\u338A', '\u338B', '\u338C',
+                        '\u3390', '\u3391', '\u3392', '\u3393', '\u3394', '\u33A9', '\u33AA',
+                        '\u33AB', '\u33AC', '\u33B4', '\u33B5', '\u33B6', '\u33B7', '\u33B8',
+                        '\u33B9', '\u33BA', '\u33BB', '\u33BC', '\u33BD', '\u33BE', '\u33BF',
+                        '\u33C0', '\u33C1', '\u33C3', '\u33C6', '\u33C7', '\u33C8', '\u33C9',
+                        '\u33CB', '\u33CD', '\u33CE', '\u33D7', '\u33D9', '\u33DA', '\u33DC',
+                        '\u33DD', '\uFB00', '\uFB01', '\uFB02', '\uFB03', '\uFB04', '\uFB05',
+                        '\uFB06', '\uFB13', '\uFB14', '\uFB15', '\uFB16', '\uFB17', '\uFF21',
+                        '\uFF22', '\uFF23', '\uFF24', '\uFF25', '\uFF26', '\uFF27', '\uFF28',
+                        '\uFF29', '\uFF2A', '\uFF2B', '\uFF2C', '\uFF2D', '\uFF2E', '\uFF2F',
+                        '\uFF30', '\uFF31', '\uFF32', '\uFF33', '\uFF34', '\uFF35', '\uFF36',
+                        '\uFF37', '\uFF38', '\uFF39', '\uFF3A' };
+            final String[] lowerCaseFoldedArr =
+                    new String[] { "\u0061", "\u0062", "\u0063", "\u0064", "\u0065", "\u0066",
+                        "\u0067", "\u0068", "\u0069", "\u006A", "\u006B", "\u006C", "\u006D",
+                        "\u006E", "\u006F", "\u0070", "\u0071", "\u0072", "\u0073", "\u0074",
+                        "\u0075", "\u0076", "\u0077", "\u0078", "\u0079", "\u007A", "\u03BC",
+                        "\u00E0", "\u00E1", "\u00E2", "\u00E3", "\u00E4", "\u00E5", "\u00E6",
+                        "\u00E7", "\u00E8", "\u00E9", "\u00EA", "\u00EB", "\u00EC", "\u00ED",
+                        "\u00EE", "\u00EF", "\u00F0", "\u00F1", "\u00F2", "\u00F3", "\u00F4",
+                        "\u00F5", "\u00F6", "\u00F8", "\u00F9", "\u00FA", "\u00FB", "\u00FC",
+                        "\u00FD", "\u00FE", "\u0073\u0073", "\u0101", "\u0103", "\u0105", "\u0107",
+                        "\u0109", "\u010B", "\u010D", "\u010F", "\u0111", "\u0113", "\u0115",
+                        "\u0117", "\u0119", "\u011B", "\u011D", "\u011F", "\u0121", "\u0123",
+                        "\u0125", "\u0127", "\u0129", "\u012B", "\u012D", "\u012F", "\u0069\u0307",
+                        "\u0133", "\u0135", "\u0137", "\u013A", "\u013C", "\u013E", "\u0140",
+                        "\u0142", "\u0144", "\u0146", "\u0148", "\u02BC\u006E", "\u014B", "\u014D",
+                        "\u014F", "\u0151", "\u0153", "\u0155", "\u0157", "\u0159", "\u015B",
+                        "\u015D", "\u015F", "\u0161", "\u0163", "\u0165", "\u0167", "\u0169",
+                        "\u016B", "\u016D", "\u016F", "\u0171", "\u0173", "\u0175", "\u0177",
+                        "\u00FF", "\u017A", "\u017C", "\u017E", "\u0073", "\u0253", "\u0183",
+                        "\u0185", "\u0254", "\u0188", "\u0256", "\u0257", "\u018C", "\u01DD",
+                        "\u0259", "\u025B", "\u0192", "\u0260", "\u0263", "\u0269", "\u0268",
+                        "\u0199", "\u026F", "\u0272", "\u0275", "\u01A1", "\u01A3", "\u01A5",
+                        "\u0280", "\u01A8", "\u0283", "\u01AD", "\u0288", "\u01B0", "\u028A",
+                        "\u028B", "\u01B4", "\u01B6", "\u0292", "\u01B9", "\u01BD", "\u01C6",
+                        "\u01C6", "\u01C9", "\u01C9", "\u01CC", "\u01CC", "\u01CE", "\u01D0",
+                        "\u01D2", "\u01D4", "\u01D6", "\u01D8", "\u01DA", "\u01DC", "\u01DF",
+                        "\u01E1", "\u01E3", "\u01E5", "\u01E7", "\u01E9", "\u01EB", "\u01ED",
+                        "\u01EF", "\u006A\u030C", "\u01F3", "\u01F3", "\u01F5", "\u0195", "\u01BF",
+                        "\u01F9", "\u01FB", "\u01FD", "\u01FF", "\u0201", "\u0203", "\u0205",
+                        "\u0207", "\u0209", "\u020B", "\u020D", "\u020F", "\u0211", "\u0213",
+                        "\u0215", "\u0217", "\u0219", "\u021B", "\u021D", "\u021F", "\u019E",
+                        "\u0223", "\u0225", "\u0227", "\u0229", "\u022B", "\u022D", "\u022F",
+                        "\u0231", "\u0233", "\u03B9", "\u0020\u03B9", "\u03AC", "\u03AD", "\u03AE",
+                        "\u03AF", "\u03CC", "\u03CD", "\u03CE", "\u03B9\u0308\u0301", "\u03B1",
+                        "\u03B2", "\u03B3", "\u03B4", "\u03B5", "\u03B6", "\u03B7", "\u03B8",
+                        "\u03B9", "\u03BA", "\u03BB", "\u03BC", "\u03BD", "\u03BE", "\u03BF",
+                        "\u03C0", "\u03C1", "\u03C3", "\u03C4", "\u03C5", "\u03C6", "\u03C7",
+                        "\u03C8", "\u03C9", "\u03CA", "\u03CB", "\u03C5\u0308\u0301", "\u03C3",
+                        "\u03B2", "\u03B8", "\u03C5", "\u03CD", "\u03CB", "\u03C6", "\u03C0",
+                        "\u03D9", "\u03DB", "\u03DD", "\u03DF", "\u03E1", "\u03E3", "\u03E5",
+                        "\u03E7", "\u03E9", "\u03EB", "\u03ED", "\u03EF", "\u03BA", "\u03C1",
+                        "\u03C3", "\u03B8", "\u03B5", "\u0450", "\u0451", "\u0452", "\u0453",
+                        "\u0454", "\u0455", "\u0456", "\u0457", "\u0458", "\u0459", "\u045A",
+                        "\u045B", "\u045C", "\u045D", "\u045E", "\u045F", "\u0430", "\u0431",
+                        "\u0432", "\u0433", "\u0434", "\u0435", "\u0436", "\u0437", "\u0438",
+                        "\u0439", "\u043A", "\u043B", "\u043C", "\u043D", "\u043E", "\u043F",
+                        "\u0440", "\u0441", "\u0442", "\u0443", "\u0444", "\u0445", "\u0446",
+                        "\u0447", "\u0448", "\u0449", "\u044A", "\u044B", "\u044C", "\u044D",
+                        "\u044E", "\u044F", "\u0461", "\u0463", "\u0465", "\u0467", "\u0469",
+                        "\u046B", "\u046D", "\u046F", "\u0471", "\u0473", "\u0475", "\u0477",
+                        "\u0479", "\u047B", "\u047D", "\u047F", "\u0481", "\u048B", "\u048D",
+                        "\u048F", "\u0491", "\u0493", "\u0495", "\u0497", "\u0499", "\u049B",
+                        "\u049D", "\u049F", "\u04A1", "\u04A3", "\u04A5", "\u04A7", "\u04A9",
+                        "\u04AB", "\u04AD", "\u04AF", "\u04B1", "\u04B3", "\u04B5", "\u04B7",
+                        "\u04B9", "\u04BB", "\u04BD", "\u04BF", "\u04C2", "\u04C4", "\u04C6",
+                        "\u04C8", "\u04CA", "\u04CC", "\u04CE", "\u04D1", "\u04D3", "\u04D5",
+                        "\u04D7", "\u04D9", "\u04DB", "\u04DD", "\u04DF", "\u04E1", "\u04E3",
+                        "\u04E5", "\u04E7", "\u04E9", "\u04EB", "\u04ED", "\u04EF", "\u04F1",
+                        "\u04F3", "\u04F5", "\u04F9", "\u0501", "\u0503", "\u0505", "\u0507",
+                        "\u0509", "\u050B", "\u050D", "\u050F", "\u0561", "\u0562", "\u0563",
+                        "\u0564", "\u0565", "\u0566", "\u0567", "\u0568", "\u0569", "\u056A",
+                        "\u056B", "\u056C", "\u056D", "\u056E", "\u056F", "\u0570", "\u0571",
+                        "\u0572", "\u0573", "\u0574", "\u0575", "\u0576", "\u0577", "\u0578",
+                        "\u0579", "\u057A", "\u057B", "\u057C", "\u057D", "\u057E", "\u057F",
+                        "\u0580", "\u0581", "\u0582", "\u0583", "\u0584", "\u0585", "\u0586",
+                        "\u0565\u0582", "\u1E01", "\u1E03", "\u1E05", "\u1E07", "\u1E09", "\u1E0B",
+                        "\u1E0D", "\u1E0F", "\u1E11", "\u1E13", "\u1E15", "\u1E17", "\u1E19",
+                        "\u1E1B", "\u1E1D", "\u1E1F", "\u1E21", "\u1E23", "\u1E25", "\u1E27",
+                        "\u1E29", "\u1E2B", "\u1E2D", "\u1E2F", "\u1E31", "\u1E33", "\u1E35",
+                        "\u1E37", "\u1E39", "\u1E3B", "\u1E3D", "\u1E3F", "\u1E41", "\u1E43",
+                        "\u1E45", "\u1E47", "\u1E49", "\u1E4B", "\u1E4D", "\u1E4F", "\u1E51",
+                        "\u1E53", "\u1E55", "\u1E57", "\u1E59", "\u1E5B", "\u1E5D", "\u1E5F",
+                        "\u1E61", "\u1E63", "\u1E65", "\u1E67", "\u1E69", "\u1E6B", "\u1E6D",
+                        "\u1E6F", "\u1E71", "\u1E73", "\u1E75", "\u1E77", "\u1E79", "\u1E7B",
+                        "\u1E7D", "\u1E7F", "\u1E81", "\u1E83", "\u1E85", "\u1E87", "\u1E89",
+                        "\u1E8B", "\u1E8D", "\u1E8F", "\u1E91", "\u1E93", "\u1E95", "\u0068\u0331",
+                        "\u0074\u0308", "\u0077\u030A", "\u0079\u030A", "\u0061\u02BE", "\u1E61",
+                        "\u1EA1", "\u1EA3", "\u1EA5", "\u1EA7", "\u1EA9", "\u1EAB", "\u1EAD",
+                        "\u1EAF", "\u1EB1", "\u1EB3", "\u1EB5", "\u1EB7", "\u1EB9", "\u1EBB",
+                        "\u1EBD", "\u1EBF", "\u1EC1", "\u1EC3", "\u1EC5", "\u1EC7", "\u1EC9",
+                        "\u1ECB", "\u1ECD", "\u1ECF", "\u1ED1", "\u1ED3", "\u1ED5", "\u1ED7",
+                        "\u1ED9", "\u1EDB", "\u1EDD", "\u1EDF", "\u1EE1", "\u1EE3", "\u1EE5",
+                        "\u1EE7", "\u1EE9", "\u1EEB", "\u1EED", "\u1EEF", "\u1EF1", "\u1EF3",
+                        "\u1EF5", "\u1EF7", "\u1EF9", "\u1F00", "\u1F01", "\u1F02", "\u1F03",
+                        "\u1F04", "\u1F05", "\u1F06", "\u1F07", "\u1F10", "\u1F11", "\u1F12",
+                        "\u1F13", "\u1F14", "\u1F15", "\u1F20", "\u1F21", "\u1F22", "\u1F23",
+                        "\u1F24", "\u1F25", "\u1F26", "\u1F27", "\u1F30", "\u1F31", "\u1F32",
+                        "\u1F33", "\u1F34", "\u1F35", "\u1F36", "\u1F37", "\u1F40", "\u1F41",
+                        "\u1F42", "\u1F43", "\u1F44", "\u1F45", "\u03C5\u0313",
+                        "\u03C5\u0313\u0300", "\u03C5\u0313\u0301", "\u03C5\u0313\u0342", "\u1F51",
+                        "\u1F53", "\u1F55", "\u1F57", "\u1F60", "\u1F61", "\u1F62", "\u1F63",
+                        "\u1F64", "\u1F65", "\u1F66", "\u1F67", "\u1F00\u03B9", "\u1F01\u03B9",
+                        "\u1F02\u03B9", "\u1F03\u03B9", "\u1F04\u03B9", "\u1F05\u03B9",
+                        "\u1F06\u03B9", "\u1F07\u03B9", "\u1F00\u03B9", "\u1F01\u03B9",
+                        "\u1F02\u03B9", "\u1F03\u03B9", "\u1F04\u03B9", "\u1F05\u03B9",
+                        "\u1F06\u03B9", "\u1F07\u03B9", "\u1F20\u03B9", "\u1F21\u03B9",
+                        "\u1F22\u03B9", "\u1F23\u03B9", "\u1F24\u03B9", "\u1F25\u03B9",
+                        "\u1F26\u03B9", "\u1F27\u03B9", "\u1F20\u03B9", "\u1F21\u03B9",
+                        "\u1F22\u03B9", "\u1F23\u03B9", "\u1F24\u03B9", "\u1F25\u03B9",
+                        "\u1F26\u03B9", "\u1F27\u03B9", "\u1F60\u03B9", "\u1F61\u03B9",
+                        "\u1F62\u03B9", "\u1F63\u03B9", "\u1F64\u03B9", "\u1F65\u03B9",
+                        "\u1F66\u03B9", "\u1F67\u03B9", "\u1F60\u03B9", "\u1F61\u03B9",
+                        "\u1F62\u03B9", "\u1F63\u03B9", "\u1F64\u03B9", "\u1F65\u03B9",
+                        "\u1F66\u03B9", "\u1F67\u03B9", "\u1F70\u03B9", "\u03B1\u03B9",
+                        "\u03AC\u03B9", "\u03B1\u0342", "\u03B1\u0342\u03B9", "\u1FB0", "\u1FB1",
+                        "\u1F70", "\u1F71", "\u03B1\u03B9", "\u03B9", "\u1F74\u03B9",
+                        "\u03B7\u03B9", "\u03AE\u03B9", "\u03B7\u0342", "\u03B7\u0342\u03B9",
+                        "\u1F72", "\u1F73", "\u1F74", "\u1F75", "\u03B7\u03B9",
+                        "\u03B9\u0308\u0300", "\u03B9\u0308\u0301", "\u03B9\u0342",
+                        "\u03B9\u0308\u0342", "\u1FD0", "\u1FD1", "\u1F76", "\u1F77",
+                        "\u03C5\u0308\u0300", "\u03C5\u0308\u0301", "\u03C1\u0313", "\u03C5\u0342",
+                        "\u03C5\u0308\u0342", "\u1FE0", "\u1FE1", "\u1F7A", "\u1F7B", "\u1FE5",
+                        "\u1F7C\u03B9", "\u03C9\u03B9", "\u03CE\u03B9", "\u03C9\u0342",
+                        "\u03C9\u0342\u03B9", "\u1F78", "\u1F79", "\u1F7C", "\u1F7D",
+                        "\u03C9\u03B9", "\u0072\u0073", "\u0063", "\u00B0\u0063", "\u025B",
+                        "\u00B0\u0066", "\u0068", "\u0068", "\u0068", "\u0069", "\u0069", "\u006C",
+                        "\u006E", "\u006E\u006F", "\u0070", "\u0071", "\u0072", "\u0072", "\u0072",
+                        "\u0073\u006D", "\u0074\u0065\u006C", "\u0074\u006D", "\u007A", "\u03C9",
+                        "\u007A", "\u006B", "\u00E5", "\u0062", "\u0063", "\u0065", "\u0066",
+                        "\u006D", "\u03B3", "\u03C0", "\u0064", "\u2170", "\u2171", "\u2172",
+                        "\u2173", "\u2174", "\u2175", "\u2176", "\u2177", "\u2178", "\u2179",
+                        "\u217A", "\u217B", "\u217C", "\u217D", "\u217E", "\u217F", "\u24D0",
+                        "\u24D1", "\u24D2", "\u24D3", "\u24D4", "\u24D5", "\u24D6", "\u24D7",
+                        "\u24D8", "\u24D9", "\u24DA", "\u24DB", "\u24DC", "\u24DD", "\u24DE",
+                        "\u24DF", "\u24E0", "\u24E1", "\u24E2", "\u24E3", "\u24E4", "\u24E5",
+                        "\u24E6", "\u24E7", "\u24E8", "\u24E9", "\u0068\u0070\u0061",
+                        "\u0061\u0075", "\u006F\u0076", "\u0070\u0061", "\u006E\u0061",
+                        "\u03BC\u0061", "\u006D\u0061", "\u006B\u0061", "\u006B\u0062",
+                        "\u006D\u0062", "\u0067\u0062", "\u0070\u0066", "\u006E\u0066",
+                        "\u03BC\u0066", "\u0068\u007A", "\u006B\u0068\u007A", "\u006D\u0068\u007A",
+                        "\u0067\u0068\u007A", "\u0074\u0068\u007A", "\u0070\u0061",
+                        "\u006B\u0070\u0061", "\u006D\u0070\u0061", "\u0067\u0070\u0061",
+                        "\u0070\u0076", "\u006E\u0076", "\u03BC\u0076", "\u006D\u0076",
+                        "\u006B\u0076", "\u006D\u0076", "\u0070\u0077", "\u006E\u0077",
+                        "\u03BC\u0077", "\u006D\u0077", "\u006B\u0077", "\u006D\u0077",
+                        "\u006B\u03C9", "\u006D\u03C9", "\u0062\u0071", "\u0063\u2215\u006B\u0067",
+                        "\u0063\u006F\u002E", "\u0064\u0062", "\u0067\u0079", "\u0068\u0070",
+                        "\u006B\u006B", "\u006B\u006D", "\u0070\u0068", "\u0070\u0070\u006D",
+                        "\u0070\u0072", "\u0073\u0076", "\u0077\u0062", "\u0066\u0066",
+                        "\u0066\u0069", "\u0066\u006C", "\u0066\u0066\u0069", "\u0066\u0066\u006C",
+                        "\u0073\u0074", "\u0073\u0074", "\u0574\u0576", "\u0574\u0565",
+                        "\u0574\u056B", "\u057E\u0576", "\u0574\u056D", "\uFF41", "\uFF42",
+                        "\uFF43", "\uFF44", "\uFF45", "\uFF46", "\uFF47", "\uFF48", "\uFF49",
+                        "\uFF4A", "\uFF4B", "\uFF4C", "\uFF4D", "\uFF4E", "\uFF4F", "\uFF50",
+                        "\uFF51", "\uFF52", "\uFF53", "\uFF54", "\uFF55", "\uFF56", "\uFF57",
+                        "\uFF58", "\uFF59", "\uFF5A" };
+            for (int count = 0; count < upperCaseArr.length; count++) {
+                CASE_MAP_TABLE.put(upperCaseArr[count], lowerCaseFoldedArr[count]);
+            }
+        }
+
+        /** Checks each character and replaces it with its mapping. */
+        private static void map(final StringBuilder buffer, final ByteSequence sequence,
+                final boolean trim, final boolean foldCase) {
+            final String value = sequence.toString();
+            for (int i = 0; i < value.length(); i++) {
+                final char c = value.charAt(i);
+                if (MAP_2_NULL.contains(c)) {
+                    continue;
+                }
+
+                if (MAP_2_SPACE.contains(c)) {
+                    if (canMapToSpace(buffer, trim)) {
+                        buffer.append(SPACE_CHAR);
+                    }
+                    continue;
+                }
+
+                if (foldCase) {
+                    final String mapping = CASE_MAP_TABLE.get(c);
+                    if (mapping != null) {
+                        buffer.append(mapping);
+                        continue;
+                    }
+                }
+                // It came here so no match was found.
+                buffer.append(c);
+            }
+        }
+
+        private MappingTable() {
+            // Prevent instantiation.
+        }
+    }
+
+    /**
+     * Defines SPACE character.
+     */
+    private static final char SPACE_CHAR = '\u0020';
+
+    /**
+     * Indicates whether case should be folded during string preparation.
+     */
+    public static final boolean CASE_FOLD = true;
+
+    /**
+     * Indicates whether case should not be folded during string preparation.
+     */
+    public static final boolean NO_CASE_FOLD = false;
+
+    /**
+     * Indicates whether leading and trailing spaces should be trimmed during
+     * string preparation.
+     */
+    public static final boolean TRIM = true;
+
+    /**
+     * Prepares an attribute or assertion value as per stringprep algorithm
+     * defined in RFC 4518.
+     *
+     * @param buffer
+     *            The buffer to which the prepared form of the string should be
+     *            appended.
+     * @param sequence
+     *            The {@link org.forgerock.opendj.ldap.ByteSequence} that needs
+     *            preparation.
+     * @param trim
+     *            Indicates whether leading and trailing spaces should be
+     *            omitted from the string representation.
+     * @param foldCase
+     *            Indicates whether the case will be folded during mapping.
+     * @see <a href="http://www.rfc-editor.org/rfc/rfc4518.txt">
+     *      Internationalized String Preparation</a>
+     */
+    public static void prepareUnicode(final StringBuilder buffer, final ByteSequence sequence,
+            final boolean trim, final boolean foldCase) {
+        Reject.ifNull(buffer, sequence);
+
+        // Optimize in the case of purely ascii characters which is the most
+        // common case.
+        final int length = sequence.length();
+        for (int i = 0; i < length; i++) {
+            final byte b = sequence.byteAt(i);
+            if ((b & 0x7F) != b) {
+                // Map the attribute value.
+                MappingTable.map(buffer, sequence.subSequence(i, length), trim, foldCase);
+                // Normalize the attribute value.
+                String normalizedForm = Normalizer.normalize(buffer, Form.NFKD);
+                buffer.setLength(0);
+                buffer.append(normalizedForm);
+                break;
+            }
+
+            switch (b) {
+            case ' ':
+                if (canMapToSpace(buffer, trim)) {
+                    buffer.append(' ');
+                }
+                break;
+            default:
+                // Perform mapping.
+                if (b >= '\u0009' && b < '\u000E') {
+                    // These characters are mapped to a SPACE.
+                    if (canMapToSpace(buffer, trim)) {
+                        buffer.append(SPACE_CHAR);
+                    }
+                } else if ((b >= '\u0000' && b <= '\u0008') || (b >= '\u000E' && b <= '\u001F')
+                        || b == '\u007F') {
+                    // These characters are mapped to nothing and hence not
+                    // copied over..
+                    break;
+                } else if (foldCase && b >= 'A' && b <= 'Z') {
+                    // If case-folding is allowed then map to the lower case.
+                    buffer.append((char) (b + 32));
+                } else {
+                    buffer.append((char) b);
+                }
+                break;
+            }
+        }
+        if (trim) {
+            // Strip off any trailing spaces.
+            for (int i = buffer.length() - 1; i > 0; i--) {
+                if (buffer.charAt(i) == SPACE_CHAR) {
+                    buffer.delete(i, i + 1);
+                } else {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Do not map this character into a space if:
+     * <ol>
+     * <li>trimming is desired and this was the leading char.</li>
+     * <li>The last character was a space.</li>
+     * </ol>
+     */
+    private static boolean canMapToSpace(final StringBuilder buffer, final boolean trim) {
+        final int buffLen = buffer.length();
+        final boolean doNotMap = (trim && buffLen == 0)
+                || (buffLen > 0 && buffer.charAt(buffLen - 1) == SPACE_CHAR);
+        return !doNotMap;
+    }
+
+    /** Prevent instantiation. */
+    private StringPrepProfile() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SubstringReader.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SubstringReader.java
new file mode 100644
index 0000000..f2abf40
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/SubstringReader.java
@@ -0,0 +1,151 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A sub-string reader.
+ */
+public class SubstringReader {
+    /** The source string. */
+    private final String source;
+    /** The current position. */
+    private int pos;
+    /** The marked position. */
+    private int mark;
+    /** The length of the source. */
+    private final int length;
+
+    /**
+     * Creates an instance of SubstringReader.
+     *
+     * @param s
+     *            the source of the reader.
+     */
+    public SubstringReader(final String s) {
+        Reject.ifNull(s);
+        source = s;
+        length = s.length();
+        pos = 0;
+        mark = 0;
+    }
+
+    /**
+     * Returns the source string.
+     *
+     * @return source string.
+     */
+    public String getString() {
+        return source;
+    }
+
+    /**
+     * Marks the present position in the stream. Subsequent calls to reset()
+     * will reposition the stream to this point.
+     */
+    public void mark() {
+        mark = pos;
+    }
+
+    /**
+     * Returns the current position of the reader.
+     *
+     * @return current position of the reader.
+     */
+    public int pos() {
+        return pos;
+    }
+
+    /**
+     * Attempts to read a character from the current position. The caller must
+     * ensure that the source string has the data available from the current
+     * position.
+     *
+     * @return The character at the current position.
+     * @throws StringIndexOutOfBoundsException
+     *             If there is no more data available to read.
+     */
+    public char read() {
+        if (pos >= length) {
+            throw new StringIndexOutOfBoundsException();
+        }
+        return source.charAt(pos++);
+    }
+
+    /**
+     * Attempts to read a substring of the specified length from the current
+     * position. The caller must ensure that the requested length is within the
+     * bounds i.e. the requested length from the current position should not
+     * exceed the length of the source string.
+     *
+     * @param length
+     *            The number of characters to read.
+     * @return The substring.
+     * @throws StringIndexOutOfBoundsException
+     *             If the length exceeds the allowed length.
+     */
+    public String read(final int length) {
+        if (length > this.length || pos + length > this.length) {
+            throw new StringIndexOutOfBoundsException();
+        }
+        final String substring = source.substring(pos, pos + length);
+        pos += length;
+        return substring;
+    }
+
+    /**
+     * Returns the remaining length of the available data.
+     *
+     * @return remaining length.
+     */
+    public int remaining() {
+        return length - pos;
+    }
+
+    /**
+     * Resets the stream to the most recent mark, or to the beginning of the
+     * string if it has never been marked.
+     */
+    public void reset() {
+        pos = mark;
+    }
+
+    /**
+     * Skips the whitespace characters and advances the reader to the next non
+     * whitespace character.
+     *
+     * @return number of whitespace characters skipped.
+     */
+    public int skipWhitespaces() {
+        int skipped = 0;
+        while (pos < length && source.charAt(pos) == ' ') {
+            skipped++;
+            pos++;
+        }
+        return skipped;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "("
+                + "source=" + source
+                + ", remaining=" + source.substring(pos, length)
+                + ")";
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/package-info.java b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/package-info.java
new file mode 100644
index 0000000..1b5c369
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/com/forgerock/opendj/util/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes providing utility functionality.
+ */
+package com.forgerock.opendj.util;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1.java
new file mode 100644
index 0000000..bc8690f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1.java
@@ -0,0 +1,327 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteSequenceReader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+
+/**
+ * This class contains various static factory methods for creating ASN.1 readers
+ * and writers.
+ *
+ * @see ASN1Reader
+ * @see ASN1Writer
+ */
+public final class ASN1 {
+
+    /**
+     * Maximum buffer size when reading ASN1. Buffers above this threshold will
+     * be discarded for garbage collection to avoid OutOfMemoryErrors.
+     */
+    private static final int DEFAULT_MAX_BUFFER_SIZE = 32 * 1024;
+
+    /**
+     * The byte array containing the pre-encoded ASN.1 encoding for a boolean
+     * value of "false".
+     */
+    public static final byte BOOLEAN_VALUE_FALSE = 0x00;
+
+    /**
+     * The byte array containing the pre-encoded ASN.1 encoding for a boolean
+     * value of "false".
+     */
+    public static final byte BOOLEAN_VALUE_TRUE = (byte) 0xFF;
+
+    /**
+     * The BER type that is assigned to the universal Boolean element.
+     */
+    public static final byte UNIVERSAL_BOOLEAN_TYPE = 0x01;
+
+    /**
+     * The BER type that is assigned to the universal integer type.
+     */
+    public static final byte UNIVERSAL_INTEGER_TYPE = 0x02;
+    /**
+     * The BER type that is assigned to the universal bit string type.
+     */
+    public static final byte UNIVERSAL_BIT_STRING_TYPE = 0x03;
+
+    /**
+     * The BER type that is assigned to the universal octet string type.
+     */
+    public static final byte UNIVERSAL_OCTET_STRING_TYPE = 0x04;
+
+    /**
+     * The BER type that is assigned to the universal null type.
+     */
+    public static final byte UNIVERSAL_NULL_TYPE = 0x05;
+
+    /**
+     * The BER type that is assigned to the universal enumerated type.
+     */
+    public static final byte UNIVERSAL_ENUMERATED_TYPE = 0x0A;
+
+    /**
+     * The BER type that is assigned to the universal sequence type.
+     */
+    public static final byte UNIVERSAL_SEQUENCE_TYPE = 0x30;
+
+    /**
+     * The BER type that is assigned to the universal set type.
+     */
+    public static final byte UNIVERSAL_SET_TYPE = 0x31;
+
+    /**
+     * The ASN.1 element decoding state that indicates that the next byte read
+     * should be additional bytes of a multi-byte length.
+     */
+    public static final int ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES = 2;
+
+    /**
+     * The ASN.1 element decoding state that indicates that the next byte read
+     * should be the first byte for the element length.
+     */
+    public static final int ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE = 1;
+
+    /**
+     * The ASN.1 element decoding state that indicates that the next byte read
+     * should be the BER type for a new element.
+     */
+    public static final int ELEMENT_READ_STATE_NEED_TYPE = 0;
+
+    /**
+     * The ASN.1 element decoding state that indicates that the next byte read
+     * should be applied to the value of the element.
+     */
+
+    public static final int ELEMENT_READ_STATE_NEED_VALUE_BYTES = 3;
+    /**
+     * The byte array that will be used for ASN.1 elements with no value.
+     */
+    static final byte[] NO_VALUE = new byte[0];
+
+    /**
+     * The bitmask that can be ANDed with the BER type to zero out all bits
+     * except those used in the class.
+     */
+    static final byte TYPE_MASK_ALL_BUT_CLASS = (byte) 0xC0;
+    /**
+     * The bitmask that can be ANDed with the BER type to zero out all bits
+     * except the primitive/constructed bit.
+     */
+    static final byte TYPE_MASK_ALL_BUT_PC = 0x20;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is in the application-specific class.
+     */
+    static final byte TYPE_MASK_APPLICATION = 0x40;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is constructed.
+     */
+    public static final byte TYPE_MASK_CONSTRUCTED = 0x20;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is in the context-specific class.
+     */
+    public static final byte TYPE_MASK_CONTEXT = (byte) 0x80;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is a primitive.
+     */
+    static final byte TYPE_MASK_PRIMITIVE = 0x00;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is in the private class.
+     */
+    static final byte TYPE_MASK_PRIVATE = (byte) 0xC0;
+    /**
+     * The bitmask that can be ANDed with the BER type to determine if the
+     * element is in the universal class.
+     */
+    static final byte TYPE_MASK_UNIVERSAL = 0x00;
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte array and
+     * having an unlimited maximum BER element size.
+     *
+     * @param array
+     *            The byte array to use.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final byte[] array) {
+        return getReader(array, 0);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte array and
+     * having a user defined maximum BER element size.
+     *
+     * @param array
+     *            The byte array to use.
+     * @param maxElementSize
+     *            The maximum BER element size, or {@code 0} to indicate that
+     *            there is no limit.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final byte[] array, final int maxElementSize) {
+        return getReader(ByteString.wrap(array), maxElementSize);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte sequence and
+     * having an unlimited maximum BER element size.
+     *
+     * @param sequence
+     *            The byte sequence to use.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final ByteSequence sequence) {
+        return getReader(sequence, 0);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte sequence and
+     * having a user defined maximum BER element size.
+     *
+     * @param sequence
+     *            The byte sequence to use.
+     * @param maxElementSize
+     *            The maximum BER element size, or {@code 0} to indicate that
+     *            there is no limit.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final ByteSequence sequence, final int maxElementSize) {
+        return new ASN1ByteSequenceReader(sequence.asReader(), maxElementSize);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte sequence reader
+     * and having an unlimited maximum BER element size.
+     *
+     * @param reader
+     *            The byte sequence reader to use.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final ByteSequenceReader reader) {
+        return getReader(reader, 0);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided byte sequence reader
+     * and having a user defined maximum BER element size.
+     *
+     * @param reader
+     *            The byte sequence reader to use.
+     * @param maxElementSize
+     *            The maximum BER element size, or {@code 0} to indicate that
+     *            there is no limit.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final ByteSequenceReader reader, final int maxElementSize) {
+        return new ASN1ByteSequenceReader(reader, maxElementSize);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided input stream and
+     * having an unlimited maximum BER element size.
+     *
+     * @param stream
+     *            The input stream to use.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final InputStream stream) {
+        return getReader(stream, 0);
+    }
+
+    /**
+     * Returns an ASN.1 reader whose source is the provided input stream and
+     * having a user defined maximum BER element size.
+     *
+     * @param stream
+     *            The input stream to use.
+     * @param maxElementSize
+     *            The maximum BER element size, or {@code 0} to indicate that
+     *            there is no limit.
+     * @return The new ASN.1 reader.
+     */
+    public static ASN1Reader getReader(final InputStream stream, final int maxElementSize) {
+        return new ASN1InputStreamReader(stream, maxElementSize);
+    }
+
+    /**
+     * Returns an ASN.1 writer whose destination is the provided byte string
+     * builder.
+     *
+     * @param builder
+     *            The byte string builder to use.
+     * @return The new ASN.1 writer.
+     */
+    public static ASN1Writer getWriter(final ByteStringBuilder builder) {
+        return getWriter(builder.asOutputStream(), DEFAULT_MAX_BUFFER_SIZE);
+    }
+
+    /**
+     * Returns an ASN.1 writer whose destination is the provided byte string
+     * builder.
+     *
+     * @param builder
+     *            The output stream to use.
+     * @param maxBufferSize
+     *          The threshold capacity beyond which internal cached buffers used
+     *          for encoding and decoding ASN1 will be trimmed after use.
+     * @return The new ASN.1 writer.
+     */
+    public static ASN1Writer getWriter(final ByteStringBuilder builder, final int maxBufferSize) {
+        return getWriter(builder.asOutputStream(), maxBufferSize);
+    }
+
+    /**
+     * Returns an ASN.1 writer whose destination is the provided output stream.
+     *
+     * @param stream
+     *            The output stream to use.
+     * @return The new ASN.1 writer.
+     */
+    public static ASN1Writer getWriter(final OutputStream stream) {
+        return getWriter(stream, DEFAULT_MAX_BUFFER_SIZE);
+    }
+
+    /**
+     * Returns an ASN.1 writer whose destination is the provided output stream.
+     *
+     * @param stream
+     *            The output stream to use.
+     * @param maxBufferSize
+     *          The threshold capacity beyond which internal cached buffers used
+     *          for encoding and decoding ASN1 will be trimmed after use.
+     * @return The new ASN.1 writer.
+     */
+    public static ASN1Writer getWriter(final OutputStream stream, final int maxBufferSize) {
+        return new ASN1OutputStreamWriter(stream, maxBufferSize);
+    }
+
+    /** Prevent instantiation. */
+    private ASN1() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1ByteSequenceReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1ByteSequenceReader.java
new file mode 100644
index 0000000..0a9ef1b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1ByteSequenceReader.java
@@ -0,0 +1,400 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.LinkedList;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequenceReader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/** An ASN.1 reader that reads from a {@link ByteSequenceReader}. */
+final class ASN1ByteSequenceReader extends AbstractASN1Reader {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private int state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    private byte peekType;
+    private int peekLength = -1;
+    private final int maxElementSize;
+    private ByteSequenceReader reader;
+    private final LinkedList<ByteSequenceReader> readerStack = new LinkedList<>();
+
+    /**
+     * Creates a new ASN1 reader whose source is the provided byte sequence
+     * reader and having a user defined maximum BER element size.
+     *
+     * @param reader
+     *            The byte sequence reader to be read.
+     * @param maxElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     */
+    ASN1ByteSequenceReader(final ByteSequenceReader reader, final int maxElementSize) {
+        this.reader = reader;
+        this.maxElementSize = maxElementSize;
+    }
+
+    @Override
+    public void close() throws IOException {
+        readerStack.clear();
+    }
+
+    @Override
+    public boolean elementAvailable() throws IOException {
+        return (state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(false))
+            && (state != ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE || needFirstLengthByteState(false))
+            && peekLength <= reader.remaining();
+    }
+
+    @Override
+    public boolean hasNextElement() throws IOException {
+        return state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(false);
+    }
+
+    @Override
+    public int peekLength() throws IOException {
+        peekType();
+
+        if (state == ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE) {
+            needFirstLengthByteState(true);
+        }
+
+        return peekLength;
+    }
+
+    @Override
+    public byte peekType() throws IOException {
+        if (state == ASN1.ELEMENT_READ_STATE_NEED_TYPE) {
+            // Read just the type.
+            if (reader.remaining() <= 0) {
+                final LocalizableMessage message = ERR_ASN1_TRUCATED_TYPE_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            final int type = reader.readByte();
+
+            peekType = (byte) type;
+            state = ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE;
+        }
+
+        return peekType;
+    }
+
+    @Override
+    public boolean readBoolean() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength != 1) {
+            final LocalizableMessage message = ERR_ASN1_BOOLEAN_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message = ERR_ASN1_BOOLEAN_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+        final int readByte = reader.readByte();
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return readByte != 0x00;
+    }
+
+    @Override
+    public void readEndSequence() throws IOException {
+        if (readerStack.isEmpty()) {
+            final LocalizableMessage message = ERR_ASN1_SEQUENCE_READ_NOT_STARTED.get();
+            throw new IllegalStateException(message.toString());
+        }
+
+        if (reader.remaining() > 0) {
+            logger.debug(LocalizableMessage.raw(
+                    "Ignoring %d unused trailing bytes in ASN.1 SEQUENCE", reader.remaining()));
+        }
+
+        reader = readerStack.removeFirst();
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readEndExplicitTag() throws DecodeException, IOException {
+        readEndSequence();
+    }
+
+    @Override
+    public void readEndSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readEndSequence();
+    }
+
+    @Override
+    public int readEnumerated() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 4) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        // From an implementation point of view, an enumerated value is
+        // equivalent to an integer.
+        return (int) readInteger();
+    }
+
+    @Override
+    public long readInteger() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 8) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+        if (peekLength > 4) {
+            long longValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = reader.readByte();
+                if (i == 0 && readByte < 0) {
+                    longValue = 0xFFFFFFFFFFFFFFFFL;
+                }
+                longValue = (longValue << 8) | (readByte & 0xFF);
+            }
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return longValue;
+        } else {
+            int intValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = reader.readByte();
+                if (i == 0 && readByte < 0) {
+                    intValue = 0xFFFFFFFF;
+                }
+                intValue = (intValue << 8) | (readByte & 0xFF);
+            }
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return intValue;
+        }
+    }
+
+    @Override
+    public void readNull() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        // Make sure that the decoded length is exactly zero byte.
+        if (peekLength != 0) {
+            final LocalizableMessage message = ERR_ASN1_NULL_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public ByteString readOctetString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message =
+                    ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return reader.readByteString(peekLength);
+    }
+
+    @Override
+    public ByteStringBuilder readOctetString(final ByteStringBuilder builder) throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        // Copy the value.
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message =
+                    ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+        builder.appendBytes(reader, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return builder;
+    }
+
+    @Override
+    public String readOctetStringAsString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message =
+                    ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return reader.readStringUtf8(peekLength);
+    }
+
+    @Override
+    public void readStartSequence() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message =
+                    ERR_ASN1_SEQUENCE_SET_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        final ByteSequenceReader subByteString = reader.readByteSequence(peekLength).asReader();
+        readerStack.addFirst(reader);
+        reader = subByteString;
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readStartExplicitTag() throws DecodeException, IOException {
+        readStartSequence();
+    }
+
+    @Override
+    public void readStartSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readStartSequence();
+    }
+
+    @Override
+    public ASN1Reader skipElement() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (reader.remaining() < peekLength) {
+            final LocalizableMessage message = ERR_ASN1_SKIP_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        reader.skip(peekLength);
+        return this;
+    }
+
+    /**
+     * Internal helper method reading the first length bytes and transition to
+     * the next state if successful.
+     *
+     * @param throwEofException
+     *            <code>true</code> to throw an exception when the end of the
+     *            sequence is encountered.
+     * @return <code>true</code> if the length bytes was successfully read
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    private boolean needFirstLengthByteState(final boolean throwEofException) throws IOException {
+        if (reader.remaining() <= 0) {
+            if (throwEofException) {
+                final LocalizableMessage message = ERR_ASN1_TRUNCATED_LENGTH_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            return false;
+        }
+        int readByte = reader.readByte();
+        peekLength = (readByte & 0x7F);
+        if (peekLength != readByte) {
+            int lengthBytesNeeded = peekLength;
+            if (lengthBytesNeeded > 4) {
+                final LocalizableMessage message =
+                        ERR_ASN1_INVALID_NUM_LENGTH_BYTES.get(lengthBytesNeeded);
+                throw DecodeException.fatalError(message);
+            }
+
+            peekLength = 0x00;
+            if (reader.remaining() < lengthBytesNeeded) {
+                if (throwEofException) {
+                    final LocalizableMessage message =
+                            ERR_ASN1_TRUNCATED_LENGTH_BYTES.get(lengthBytesNeeded);
+                    throw DecodeException.fatalError(message);
+                }
+                return false;
+            }
+
+            while (lengthBytesNeeded > 0) {
+                readByte = reader.readByte();
+                peekLength = (peekLength << 8) | (readByte & 0xFF);
+                lengthBytesNeeded--;
+            }
+        }
+
+        // Make sure that the element is not larger than the maximum allowed
+        // message size.
+        if (maxElementSize > 0 && peekLength > maxElementSize) {
+            final LocalizableMessage message =
+                    ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED
+                            .get(peekLength, maxElementSize);
+            throw DecodeException.fatalError(message);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES;
+        return true;
+    }
+
+    /**
+     * Internal helper method reading the ASN.1 type byte and transition to the
+     * next state if successful.
+     *
+     * @param throwEofException
+     *            <code>true</code> to throw an exception when the end of the
+     *            sequence is encountered.
+     * @return <code>true</code> if the type byte was successfully read
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    private boolean needTypeState(final boolean throwEofException) throws IOException {
+        // Read just the type.
+        if (reader.remaining() <= 0) {
+            if (throwEofException) {
+                final LocalizableMessage message = ERR_ASN1_TRUCATED_TYPE_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            return false;
+        }
+        final int type = reader.readByte();
+
+        peekType = (byte) type;
+        state = ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE;
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
new file mode 100644
index 0000000..c6a1522
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
@@ -0,0 +1,550 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SizeLimitInputStream;
+
+/** An ASN1Reader that reads from an input stream. */
+final class ASN1InputStreamReader extends AbstractASN1Reader {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private int state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    private byte peekType;
+    private int peekLength = -1;
+    private int lengthBytesNeeded;
+    private final int maxElementSize;
+    private InputStream in;
+    private final LinkedList<InputStream> streamStack = new LinkedList<>();
+    private byte[] buffer = new byte[512];
+
+    /**
+     * Creates a new ASN1 reader whose source is the provided input stream and
+     * having a user defined maximum BER element size.
+     *
+     * @param stream
+     *            The input stream to be read.
+     * @param maxElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     */
+    ASN1InputStreamReader(final InputStream stream, final int maxElementSize) {
+        this.in = stream;
+        this.maxElementSize = maxElementSize;
+    }
+
+    @Override
+    public void close() throws IOException {
+        // Calling close of SizeLimitInputStream should close the parent stream.
+        in.close();
+        streamStack.clear();
+    }
+
+    @Override
+    public boolean elementAvailable() throws IOException {
+        return (state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(false, false))
+            && (state != ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE || needFirstLengthByteState(false, false))
+            && (state != ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES
+                 || needAdditionalLengthBytesState(false, false))
+            && peekLength <= in.available();
+    }
+
+    @Override
+    public boolean hasNextElement() throws IOException {
+        if (!streamStack.isEmpty()) {
+            // We are reading a sub sequence. Return true as long as we
+            // haven't exhausted the size limit for the sub sequence sub input
+            // stream.
+            final SizeLimitInputStream subSq = (SizeLimitInputStream) in;
+            return subSq.getSizeLimit() - subSq.getBytesRead() > 0;
+        }
+
+        return state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(true, false);
+    }
+
+    @Override
+    public int peekLength() throws IOException {
+        peekType();
+        switch (state) {
+        case ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE:
+            needFirstLengthByteState(true, true);
+            break;
+        case ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES:
+            needAdditionalLengthBytesState(true, true);
+            break;
+        default: // ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES
+            break;
+        }
+        return peekLength;
+    }
+
+    @Override
+    public byte peekType() throws IOException {
+        if (state == ASN1.ELEMENT_READ_STATE_NEED_TYPE) {
+            needTypeState(true, true);
+        }
+
+        return peekType;
+    }
+
+    @Override
+    public boolean readBoolean() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength != 1) {
+            final LocalizableMessage message = ERR_ASN1_BOOLEAN_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        final int readByte = in.read();
+        if (readByte == -1) {
+            final LocalizableMessage message = ERR_ASN1_BOOLEAN_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        logger.trace("READ ASN.1 BOOLEAN(type=0x%x, length=%d, value=%s)", peekType, peekLength, readByte != 0x00);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return readByte != 0x00;
+    }
+
+    @Override
+    public void readEndSequence() throws IOException {
+        if (streamStack.isEmpty()) {
+            final LocalizableMessage message = ERR_ASN1_SEQUENCE_READ_NOT_STARTED.get();
+            throw new IllegalStateException(message.toString());
+        }
+
+        // Ignore all unused trailing components.
+        final SizeLimitInputStream subSq = (SizeLimitInputStream) in;
+        if (subSq.getSizeLimit() - subSq.getBytesRead() > 0) {
+            logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE",
+                    subSq.getSizeLimit() - subSq.getBytesRead());
+            subSq.skip(subSq.getSizeLimit() - subSq.getBytesRead());
+        }
+
+        logger.trace("READ ASN.1 END SEQUENCE");
+
+        in = streamStack.removeFirst();
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readEndExplicitTag() throws DecodeException, IOException {
+        readEndSequence();
+    }
+
+    @Override
+    public void readEndSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readEndSequence();
+    }
+
+    @Override
+    public int readEnumerated() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 4) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        // From an implementation point of view, an enumerated value is
+        // equivalent to an integer.
+        return (int) readInteger();
+    }
+
+    @Override
+    public long readInteger() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 8) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        if (peekLength > 4) {
+            long longValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = in.read();
+                if (readByte == -1) {
+                    final LocalizableMessage message =
+                            ERR_ASN1_INTEGER_TRUNCATED_VALUE.get(peekLength);
+                    throw DecodeException.fatalError(message);
+                }
+                if (i == 0 && ((byte) readByte) < 0) {
+                    longValue = 0xFFFFFFFFFFFFFFFFL;
+                }
+                longValue = (longValue << 8) | (readByte & 0xFF);
+            }
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return longValue;
+        } else {
+            int intValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = in.read();
+                if (readByte == -1) {
+                    final LocalizableMessage message =
+                            ERR_ASN1_INTEGER_TRUNCATED_VALUE.get(peekLength);
+                    throw DecodeException.fatalError(message);
+                }
+                if (i == 0 && ((byte) readByte) < 0) {
+                    intValue = 0xFFFFFFFF;
+                }
+                intValue = (intValue << 8) | (readByte & 0xFF);
+            }
+
+            logger.trace("READ ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", peekType, peekLength, intValue);
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return intValue;
+        }
+    }
+
+    @Override
+    public void readNull() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        // Make sure that the decoded length is exactly zero byte.
+        if (peekLength != 0) {
+            final LocalizableMessage message = ERR_ASN1_NULL_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        logger.trace("READ ASN.1 NULL(type=0x%x, length=%d)", peekType, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public ByteString readOctetString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return ByteString.empty();
+        }
+
+        // Copy the value and construct the element to return.
+        final byte[] value = new byte[peekLength];
+        int bytesNeeded = peekLength;
+        int bytesRead;
+        while (bytesNeeded > 0) {
+            bytesRead = in.read(value, peekLength - bytesNeeded, bytesNeeded);
+            if (bytesRead < 0) {
+                final LocalizableMessage message =
+                        ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+                throw DecodeException.fatalError(message);
+            }
+
+            bytesNeeded -= bytesRead;
+        }
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d)", peekType, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return ByteString.wrap(value);
+    }
+
+    @Override
+    public ByteStringBuilder readOctetString(final ByteStringBuilder builder) throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return builder;
+        }
+
+        // Copy the value and construct the element to return.
+        int bytesNeeded = peekLength;
+        int bytesRead;
+        while (bytesNeeded > 0) {
+            bytesRead = builder.appendBytes(in, bytesNeeded);
+            if (bytesRead < 0) {
+                final LocalizableMessage message =
+                        ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+                throw DecodeException.fatalError(message);
+            }
+            bytesNeeded -= bytesRead;
+        }
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d)", peekType, peekLength);
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return builder;
+    }
+
+    @Override
+    public String readOctetStringAsString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return "";
+        }
+
+        // Resize the temp buffer if needed
+        if (peekLength > buffer.length) {
+            buffer = new byte[peekLength];
+        }
+
+        int bytesNeeded = peekLength;
+        int bytesRead;
+        while (bytesNeeded > 0) {
+            bytesRead = in.read(buffer, peekLength - bytesNeeded, bytesNeeded);
+            if (bytesRead < 0) {
+                final LocalizableMessage message =
+                        ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE.get(peekLength);
+                throw DecodeException.fatalError(message);
+            }
+            bytesNeeded -= bytesRead;
+        }
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+
+        String str;
+        try {
+            str = new String(buffer, 0, peekLength, "UTF-8");
+        } catch (final UnsupportedEncodingException e) {
+            // TODO: I18N
+            throw new RuntimeException("Unable to decode ASN.1 OCTETSTRING bytes as UTF-8 string ", e);
+        }
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d, value=%s)", peekType, peekLength, str);
+
+        return str;
+    }
+
+    @Override
+    public void readStartSequence() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        final SizeLimitInputStream subStream = new SizeLimitInputStream(in, peekLength);
+
+        logger.trace("READ ASN.1 START SEQUENCE(type=0x%x, length=%d)", peekType, peekLength);
+
+        streamStack.addFirst(in);
+        in = subStream;
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readStartExplicitTag() throws DecodeException, IOException {
+        readStartSequence();
+    }
+
+    @Override
+    public void readStartSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readStartSequence();
+    }
+
+    @Override
+    public ASN1Reader skipElement() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        final long bytesSkipped = in.skip(peekLength);
+        if (bytesSkipped != peekLength) {
+            final LocalizableMessage message = ERR_ASN1_SKIP_TRUNCATED_VALUE.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return this;
+    }
+
+    /**
+     * Internal helper method reading the additional ASN.1 length bytes and
+     * transition to the next state if successful.
+     *
+     * @param isBlocking
+     *            <code>true</code> to block if the type byte is not available
+     *            or <code>false</code> to check for availability first.
+     * @param throwEofException
+     *            <code>true</code> to throw an exception when an EOF is
+     *            encountered or <code>false</code> to return false.
+     * @return <code>true</code> if the length bytes was successfully read.
+     * @throws IOException
+     *             If an error occurs while reading from the stream.
+     */
+    private boolean needAdditionalLengthBytesState(final boolean isBlocking,
+            final boolean throwEofException) throws IOException {
+        if (!isBlocking && (in.available() < lengthBytesNeeded)) {
+            return false;
+        }
+
+        int readByte;
+        while (lengthBytesNeeded > 0) {
+            readByte = in.read();
+            if (readByte == -1) {
+                state = ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES;
+                if (throwEofException) {
+                    final LocalizableMessage message =
+                            ERR_ASN1_TRUNCATED_LENGTH_BYTES.get(lengthBytesNeeded);
+                    throw DecodeException.fatalError(message);
+                }
+                return false;
+            }
+            peekLength = (peekLength << 8) | (readByte & 0xFF);
+            lengthBytesNeeded--;
+        }
+
+        // Make sure that the element is not larger than the maximum allowed
+        // message size.
+        if (maxElementSize > 0 && peekLength > maxElementSize) {
+            final LocalizableMessage message =
+                    ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED
+                            .get(peekLength, maxElementSize);
+            throw DecodeException.fatalError(message);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES;
+        return true;
+    }
+
+    /**
+     * Internal helper method reading the first length bytes and transition to
+     * the next state if successful.
+     *
+     * @param isBlocking
+     *            <code>true</code> to block if the type byte is not available
+     *            or <code>false</code> to check for availability first.
+     * @param throwEofException
+     *            <code>true</code> to throw an exception when an EOF is
+     *            encountered or <code>false</code> to return false.
+     * @return <code>true</code> if the length bytes was successfully read
+     * @throws IOException
+     *             If an error occurs while reading from the stream.
+     */
+    private boolean needFirstLengthByteState(final boolean isBlocking,
+            final boolean throwEofException) throws IOException {
+        if (!isBlocking && in.available() <= 0) {
+            return false;
+        }
+
+        int readByte = in.read();
+        if (readByte == -1) {
+            if (throwEofException) {
+                final LocalizableMessage message = ERR_ASN1_TRUNCATED_LENGTH_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            return false;
+        }
+        peekLength = readByte & 0x7F;
+        if (peekLength != readByte) {
+            lengthBytesNeeded = peekLength;
+            if (lengthBytesNeeded > 4) {
+                final LocalizableMessage message =
+                        ERR_ASN1_INVALID_NUM_LENGTH_BYTES.get(lengthBytesNeeded);
+                throw DecodeException.fatalError(message);
+            }
+            peekLength = 0x00;
+
+            if (!isBlocking && (in.available() < lengthBytesNeeded)) {
+                state = ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES;
+                return false;
+            }
+
+            while (lengthBytesNeeded > 0) {
+                readByte = in.read();
+                if (readByte == -1) {
+                    state = ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES;
+                    if (throwEofException) {
+                        final LocalizableMessage message =
+                                ERR_ASN1_TRUNCATED_LENGTH_BYTES.get(lengthBytesNeeded);
+                        throw DecodeException.fatalError(message);
+                    }
+                    return false;
+                }
+                peekLength = (peekLength << 8) | (readByte & 0xFF);
+                lengthBytesNeeded--;
+            }
+        }
+
+        // Make sure that the element is not larger than the maximum allowed
+        // message size.
+        if (maxElementSize > 0 && peekLength > maxElementSize) {
+            final LocalizableMessage message =
+                    ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED
+                            .get(peekLength, maxElementSize);
+            throw DecodeException.fatalError(message);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES;
+        return true;
+    }
+
+    /**
+     * Internal helper method reading the ASN.1 type byte and transition to the
+     * next state if successful.
+     *
+     * @param isBlocking
+     *            <code>true</code> to block if the type byte is not available
+     *            or <code>false</code> to check for availability first.
+     * @param throwEofException
+     *            <code>true</code> to throw an exception when an EOF is
+     *            encountered or <code>false</code> to return false.
+     * @return <code>true</code> if the type byte was successfully read
+     * @throws IOException
+     *             If an error occurs while reading from the stream.
+     */
+    private boolean needTypeState(final boolean isBlocking, final boolean throwEofException)
+            throws IOException {
+        // Read just the type.
+        if (!isBlocking && in.available() <= 0) {
+            return false;
+        }
+
+        final int type = in.read();
+        if (type == -1) {
+            if (throwEofException) {
+                final LocalizableMessage message = ERR_ASN1_TRUCATED_TYPE_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            return false;
+        }
+
+        peekType = (byte) type;
+        state = ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE;
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1OutputStreamWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1OutputStreamWriter.java
new file mode 100644
index 0000000..397cb70
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1OutputStreamWriter.java
@@ -0,0 +1,336 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/** An ASN1Writer implementation that outputs to an outputstream. */
+final class ASN1OutputStreamWriter extends AbstractASN1Writer {
+    /** Initial size of internal buffers. */
+    private static final int BUFFER_INIT_SIZE = 32;
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private final OutputStream rootStream;
+    private OutputStream out;
+    private final ArrayList<ByteStringBuilder> streamStack = new ArrayList<>();
+    private int stackDepth;
+    private final int maxBufferSize;
+
+    /**
+     * Creates a new ASN.1 output stream reader.
+     *
+     * @param stream
+     *            The underlying output stream.
+     * @param maxBufferSize
+     *          The threshold capacity beyond which internal cached buffers used
+     *          for encoding and decoding ASN1 will be trimmed after use.
+     */
+    ASN1OutputStreamWriter(final OutputStream stream, final int maxBufferSize) {
+        this.out = stream;
+        this.rootStream = stream;
+        this.maxBufferSize = Math.max(maxBufferSize, BUFFER_INIT_SIZE);
+        this.stackDepth = -1;
+    }
+
+    @Override
+    public void close() throws IOException {
+        while (stackDepth >= 0) {
+            writeEndSequence();
+        }
+        rootStream.flush();
+        streamStack.clear();
+        rootStream.close();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        rootStream.flush();
+    }
+
+    @Override
+    public ASN1Writer writeBoolean(final byte type, final boolean booleanValue) throws IOException {
+        out.write(type);
+        writeLength(1);
+        out.write(booleanValue ? ASN1.BOOLEAN_VALUE_TRUE : ASN1.BOOLEAN_VALUE_FALSE);
+
+        logger.trace("WRITE ASN.1 BOOLEAN(type=0x%x, length=%d, value=%s)", type, 1, booleanValue);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeEndSequence() throws IOException {
+        if (stackDepth < 0) {
+            final LocalizableMessage message = ERR_ASN1_SEQUENCE_WRITE_NOT_STARTED.get();
+            throw new IllegalStateException(message.toString());
+        }
+
+        final ByteStringBuilder childStream = streamStack.get(stackDepth);
+
+        // Decrement the stack depth and get the parent stream
+        --stackDepth;
+        final OutputStream parentStream =
+                stackDepth < 0 ? rootStream : streamStack.get(stackDepth).asOutputStream();
+
+        // Switch to parent stream and reset the sub-stream
+        out = parentStream;
+
+        // Write the length and contents of the sub-stream
+        writeLength(childStream.length());
+        childStream.copyTo(parentStream);
+
+        logger.trace("WRITE ASN.1 END SEQUENCE(length=%d)", childStream.length());
+
+        childStream.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeEndSet() throws IOException {
+        return writeEndSequence();
+    }
+
+    @Override
+    public ASN1Writer writeEnumerated(final byte type, final int intValue) throws IOException {
+        return writeInteger(type, intValue);
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final byte type, final int intValue) throws IOException {
+        out.write(type);
+        if (((intValue < 0) && ((intValue & 0xFFFFFF80) == 0xFFFFFF80))
+                || ((intValue & 0x0000007F) == intValue)) {
+            writeLength(1);
+            out.write((byte) (intValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 1, intValue);
+        } else if (((intValue < 0) && ((intValue & 0xFFFF8000) == 0xFFFF8000))
+                || ((intValue & 0x00007FFF) == intValue)) {
+            writeLength(2);
+            out.write((byte) ((intValue >> 8) & 0xFF));
+            out.write((byte) (intValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 2, intValue);
+        } else if (((intValue < 0) && ((intValue & 0xFF800000) == 0xFF800000))
+                || ((intValue & 0x007FFFFF) == intValue)) {
+            writeLength(3);
+            out.write((byte) ((intValue >> 16) & 0xFF));
+            out.write((byte) ((intValue >> 8) & 0xFF));
+            out.write((byte) (intValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 3, intValue);
+        } else {
+            writeLength(4);
+            out.write((byte) ((intValue >> 24) & 0xFF));
+            out.write((byte) ((intValue >> 16) & 0xFF));
+            out.write((byte) ((intValue >> 8) & 0xFF));
+            out.write((byte) (intValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 4, intValue);
+        }
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final byte type, final long longValue) throws IOException {
+        out.write(type);
+        if (((longValue < 0) && ((longValue & 0xFFFFFFFFFFFFFF80L) == 0xFFFFFFFFFFFFFF80L))
+                || ((longValue & 0x000000000000007FL) == longValue)) {
+            writeLength(1);
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 1, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFFFFFF8000L) == 0xFFFFFFFFFFFF8000L))
+                || ((longValue & 0x0000000000007FFFL) == longValue)) {
+            writeLength(2);
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 2, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFFFF800000L) == 0xFFFFFFFFFF800000L))
+                || ((longValue & 0x00000000007FFFFFL) == longValue)) {
+            writeLength(3);
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 3, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFF80000000L) == 0xFFFFFFFF80000000L))
+                || ((longValue & 0x000000007FFFFFFFL) == longValue)) {
+            writeLength(4);
+            out.write((byte) ((longValue >> 24) & 0xFF));
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 4, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFF8000000000L) == 0xFFFFFF8000000000L))
+                || ((longValue & 0x0000007FFFFFFFFFL) == longValue)) {
+            writeLength(5);
+            out.write((byte) ((longValue >> 32) & 0xFF));
+            out.write((byte) ((longValue >> 24) & 0xFF));
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 5, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFF800000000000L) == 0xFFFF800000000000L))
+                || ((longValue & 0x00007FFFFFFFFFFFL) == longValue)) {
+            writeLength(6);
+            out.write((byte) ((longValue >> 40) & 0xFF));
+            out.write((byte) ((longValue >> 32) & 0xFF));
+            out.write((byte) ((longValue >> 24) & 0xFF));
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 6, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFF80000000000000L) == 0xFF80000000000000L))
+                || ((longValue & 0x007FFFFFFFFFFFFFL) == longValue)) {
+            writeLength(7);
+            out.write((byte) ((longValue >> 48) & 0xFF));
+            out.write((byte) ((longValue >> 40) & 0xFF));
+            out.write((byte) ((longValue >> 32) & 0xFF));
+            out.write((byte) ((longValue >> 24) & 0xFF));
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 7, longValue);
+        } else {
+            writeLength(8);
+            out.write((byte) ((longValue >> 56) & 0xFF));
+            out.write((byte) ((longValue >> 48) & 0xFF));
+            out.write((byte) ((longValue >> 40) & 0xFF));
+            out.write((byte) ((longValue >> 32) & 0xFF));
+            out.write((byte) ((longValue >> 24) & 0xFF));
+            out.write((byte) ((longValue >> 16) & 0xFF));
+            out.write((byte) ((longValue >> 8) & 0xFF));
+            out.write((byte) (longValue & 0xFF));
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 8, longValue);
+        }
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeNull(final byte type) throws IOException {
+        out.write(type);
+        writeLength(0);
+
+        logger.trace("WRITE ASN.1 NULL(type=0x%x, length=%d)", type, 0);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final byte[] value, final int offset,
+            final int length) throws IOException {
+        out.write(type);
+        writeLength(length);
+        out.write(value, offset, length);
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d)", type, length);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final ByteSequence value)
+            throws IOException {
+        out.write(type);
+        writeLength(value.length());
+        value.copyTo(out);
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d)", type, value.length());
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final String value) throws IOException {
+        out.write(type);
+
+        if (value == null) {
+            writeLength(0);
+            return this;
+        }
+
+        final byte[] bytes = StaticUtils.getBytes(value);
+        writeLength(bytes.length);
+        out.write(bytes);
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d, value=%s)", type, bytes.length, value);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeStartSequence(final byte type) throws IOException {
+        // Write the type in current stream switch to next sub-stream
+        out.write(type);
+
+        // Increment the stack depth and get the sub-stream from the stack
+        ++stackDepth;
+
+        // Make sure we have a cached sub-stream at this depth
+        if (stackDepth >= streamStack.size()) {
+            final ByteStringBuilder subStream = new ByteStringBuilder(BUFFER_INIT_SIZE);
+            streamStack.add(subStream);
+            out = subStream.asOutputStream();
+        } else {
+            out = streamStack.get(stackDepth).asOutputStream();
+        }
+
+        logger.trace("WRITE ASN.1 START SEQUENCE(type=0x%x)", type);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeStartSet(final byte type) throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        return writeStartSequence(type);
+    }
+
+    /**
+     * Writes the provided value for use as the length of an ASN.1 element.
+     *
+     * @param length
+     *            The length to encode for use in an ASN.1 element.
+     * @throws IOException
+     *             if an error occurs while writing.
+     */
+    private void writeLength(final int length) throws IOException {
+        if (length < 128) {
+            out.write((byte) length);
+        } else if ((length & 0x000000FF) == length) {
+            out.write((byte) 0x81);
+            out.write((byte) (length & 0xFF));
+        } else if ((length & 0x0000FFFF) == length) {
+            out.write((byte) 0x82);
+            out.write((byte) ((length >> 8) & 0xFF));
+            out.write((byte) (length & 0xFF));
+        } else if ((length & 0x00FFFFFF) == length) {
+            out.write((byte) 0x83);
+            out.write((byte) ((length >> 16) & 0xFF));
+            out.write((byte) ((length >> 8) & 0xFF));
+            out.write((byte) (length & 0xFF));
+        } else {
+            out.write((byte) 0x84);
+            out.write((byte) ((length >> 24) & 0xFF));
+            out.write((byte) ((length >> 16) & 0xFF));
+            out.write((byte) ((length >> 8) & 0xFF));
+            out.write((byte) (length & 0xFF));
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Reader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Reader.java
new file mode 100644
index 0000000..447455e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Reader.java
@@ -0,0 +1,421 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+
+package org.forgerock.opendj.io;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * An interface for decoding ASN.1 elements from a data source.
+ * <p>
+ * Methods for creating {@link ASN1Reader}s are provided in the {@link ASN1}
+ * class.
+ */
+public interface ASN1Reader extends Closeable {
+
+    /**
+     * Closes this ASN.1 reader.
+     *
+     * @throws IOException
+     *             If an error occurs while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Indicates whether or not the next element can be read without blocking.
+     *
+     * @return {@code true} if a complete element is available or {@code false}
+     *         otherwise.
+     * @throws DecodeException
+     *             If the available data was not valid ASN.1.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean elementAvailable() throws DecodeException, IOException;
+
+    /**
+     * Indicates whether or not the current stream, sequence, or set contains
+     * another element. Note that this method may return {@code true} even if a
+     * previous call to {@link #elementAvailable} returned {@code false},
+     * indicating that the current set or sequence contains another element but
+     * an attempt to read that element may block. This method will block if
+     * there is not enough data available to make the determination (typically
+     * only the next element's type is required).
+     *
+     * @return {@code true} if the current stream, sequence, or set contains
+     *         another element, or {@code false} if the end of the stream,
+     *         sequence, or set has been reached.
+     * @throws DecodeException
+     *             If the available data was not valid ASN.1.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean hasNextElement() throws DecodeException, IOException;
+
+    /**
+     * Returns the data length of the next element without actually reading it.
+     *
+     * @return The data length of the next element, or {@code -1} if the end of
+     *         the stream, sequence, or set has been reached.
+     * @throws DecodeException
+     *             If the available data was not valid ASN.1.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    int peekLength() throws DecodeException, IOException;
+
+    /**
+     * Returns the type of the next element without actually reading it.
+     *
+     * @return The type of the next element, or {@code -1} if the end of the
+     *         stream, sequence, or set has been reached.
+     * @throws DecodeException
+     *             If the available data was not valid ASN.1.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    byte peekType() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a boolean having the Universal Boolean ASN.1
+     * type tag.
+     *
+     * @return The decoded boolean value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as a boolean.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean readBoolean() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a boolean having the provided type tag.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return The decoded boolean value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as a boolean.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean readBoolean(byte type) throws DecodeException, IOException;
+
+    /**
+     * Finishes reading a sequence and discards any unread elements.
+     *
+     * @throws DecodeException
+     *             If an error occurs while advancing to the end of the
+     *             sequence.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     * @throws IllegalStateException
+     *             If there is no sequence being read.
+     */
+    void readEndSequence() throws DecodeException, IOException;
+
+    /**
+     * Finishes reading an explicit tag and discards any unread elements.
+     *
+     * @throws DecodeException
+     *             If an error occurs while advancing to the end of the
+     *             explicit tag.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     * @throws IllegalStateException
+     *             If there is no explicit tag being read.
+     */
+    void readEndExplicitTag() throws DecodeException, IOException;
+
+    /**
+     * Finishes reading a set and discards any unread elements.
+     *
+     * @throws DecodeException
+     *             If an error occurs while advancing to the end of the set.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     * @throws IllegalStateException
+     *             If there is no set being read.
+     */
+    void readEndSet() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an enumerated having the Universal Enumerated
+     * ASN.1 type tag.
+     *
+     * @return The decoded enumerated value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an enumerated value.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    int readEnumerated() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an enumerated having the provided type tag.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return The decoded enumerated value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an enumerated value.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    int readEnumerated(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an integer having the Universal Integer ASN.1
+     * type tag.
+     *
+     * @return The decoded integer value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an integer.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    long readInteger() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an integer having the provided type tag.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return The decoded integer value.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an integer.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    long readInteger(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a null element having the Universal Null ASN.1
+     * type tag.
+     *
+     * @throws DecodeException
+     *             If the element cannot be decoded as a null element.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readNull() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a null element having the provided type tag.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @throws DecodeException
+     *             If the element cannot be decoded as a null element.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readNull(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an octet string having the Universal Octet
+     * String ASN.1 type tag.
+     *
+     * @return The decoded octet string represented using a {@link ByteString}.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ByteString readOctetString() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an octet string having the provided type tag.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return The decoded octet string represented using a {@link ByteString}.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ByteString readOctetString(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an octet string having the provided type tag
+     * and appends it to the provided {@link ByteStringBuilder}.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @param builder
+     *            The {@link ByteStringBuilder} to append the octet string to.
+     * @return A reference to {@code builder}.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ByteStringBuilder readOctetString(byte type, ByteStringBuilder builder) throws DecodeException,
+            IOException;
+
+    /**
+     * Reads the next element as an octet string having the Universal Octet
+     * String ASN.1 type tag and appends it to the provided
+     * {@link ByteStringBuilder}.
+     *
+     * @param builder
+     *            The {@link ByteStringBuilder} to append the octet string to.
+     * @return A reference to {@code builder}.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ByteStringBuilder readOctetString(ByteStringBuilder builder) throws DecodeException,
+            IOException;
+
+    /**
+     * Reads the next element as an octet string having the Universal Octet
+     * String ASN.1 type tag and decodes the value as a UTF-8 encoded string.
+     *
+     * @return The decoded octet string as a UTF-8 encoded string.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    String readOctetStringAsString() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an octet string having the provided type tag
+     * and decodes the value as a UTF-8 encoded string.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return The decoded octet string as a UTF-8 encoded string.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an octet string.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    String readOctetStringAsString(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a sequence having the Universal Sequence ASN.1
+     * type tag. All further reads will read the elements in the sequence until
+     * {@link #readEndSequence()} is called.
+     *
+     * @throws DecodeException
+     *             If the element cannot be decoded as a sequence.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartSequence() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an explicit ignoring the ASN.1 type tag. All
+     * further reads will read the elements in the explicit tag until
+     * {@link #readEndExplicitTag()} is called.
+     *
+     * @throws DecodeException
+     *             If the element cannot be decoded as an explicit tag.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartExplicitTag() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as an explicit tag having the provided tag type.
+     * All further reads will read the elements in the explicit tag until
+     * {@link #readEndExplicitTag()} is called.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @throws DecodeException
+     *             If the element cannot be decoded as an explicit tag.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartExplicitTag(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a sequence having the provided type tag. All
+     * further reads will read the elements in the sequence until
+     * {@link #readEndSequence()} is called.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @throws DecodeException
+     *             If the element cannot be decoded as a sequence.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartSequence(byte type) throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a set having the Universal Set ASN.1 type tag.
+     * All further reads will read the elements in the set until
+     * {@link #readEndSet()} is called.
+     *
+     * @throws DecodeException
+     *             If the element cannot be decoded as a set.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartSet() throws DecodeException, IOException;
+
+    /**
+     * Reads the next element as a set having the provided type tag. All further
+     * reads will read the elements in the set until {@link #readEndSet()} is
+     * called.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @throws DecodeException
+     *             If the element cannot be decoded as a set.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    void readStartSet(byte type) throws DecodeException, IOException;
+
+    /**
+     * Skips the next element without decoding it.
+     *
+     * @return A reference to this ASN.1 reader.
+     * @throws DecodeException
+     *             If the next element could not be skipped.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ASN1Reader skipElement() throws DecodeException, IOException;
+
+    /**
+     * Skips the next element having the provided type tag without decoding it.
+     *
+     * @param type
+     *            The expected type tag of the element.
+     * @return A reference to this ASN.1 reader.
+     * @throws DecodeException
+     *             If the next element does not have the provided type tag.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    ASN1Reader skipElement(byte type) throws DecodeException, IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Writer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Writer.java
new file mode 100644
index 0000000..532daf4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1Writer.java
@@ -0,0 +1,356 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * An interface for encoding ASN.1 elements to a data source.
+ * <p>
+ * Methods for creating {@link ASN1Writer}s are provided in the {@link ASN1}
+ * class.
+ */
+public interface ASN1Writer extends Closeable, Flushable {
+
+    /**
+     * Closes this ASN.1 writer, flushing it first. Closing a previously closed
+     * ASN.1 writer has no effect. Any unfinished sequences and/or sets will be
+     * ended.
+     *
+     * @throws IOException
+     *             If an error occurs while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Flushes this ASN.1 writer so that any buffered elements are written
+     * immediately to their intended destination. Then, if that destination is
+     * another byte stream, flush it. Thus one {@code flush()} invocation will
+     * flush all the buffers in a chain of streams.
+     * <p>
+     * If the intended destination of this stream is an abstraction provided by
+     * the underlying operating system, for example a file, then flushing the
+     * stream guarantees only that bytes previously written to the stream are
+     * passed to the operating system for writing; it does not guarantee that
+     * they are actually written to a physical device such as a disk drive.
+     *
+     * @throws IOException
+     *             If an error occurs while flushing.
+     */
+    @Override
+    void flush() throws IOException;
+
+    /**
+     * Writes a boolean element using the Universal Boolean ASN.1 type tag.
+     *
+     * @param value
+     *            The boolean value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeBoolean(boolean value) throws IOException;
+
+    /**
+     * Writes a boolean element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The boolean value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeBoolean(byte type, boolean value) throws IOException;
+
+    /**
+     * Finishes writing a sequence element.
+     *
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     * @throws IllegalStateException
+     *             If there is no sequence being written.
+     */
+    ASN1Writer writeEndSequence() throws IOException;
+
+    /**
+     * Finishes writing a set element.
+     *
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     * @throws IllegalStateException
+     *             If there is no set being written.
+     */
+    ASN1Writer writeEndSet() throws IOException;
+
+    /**
+     * Writes an enumerated element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The enumerated value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeEnumerated(byte type, int value) throws IOException;
+
+    /**
+     * Writes an enumerated element using the Universal Enumerated ASN.1 type
+     * tag.
+     *
+     * @param value
+     *            The enumerated value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeEnumerated(int value) throws IOException;
+
+    /**
+     * Writes an integer element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The integer value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeInteger(byte type, int value) throws IOException;
+
+    /**
+     * Writes an integer element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The integer value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeInteger(byte type, long value) throws IOException;
+
+    /**
+     * Writes an integer element using the Universal Integer ASN.1 type tag.
+     *
+     * @param value
+     *            The integer value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeInteger(int value) throws IOException;
+
+    /**
+     * Writes an integer element using the Universal Integer ASN.1 type tag.
+     *
+     * @param value
+     *            The integer value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeInteger(long value) throws IOException;
+
+    /**
+     * Writes a null element using the Universal Null ASN.1 type tag.
+     *
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeNull() throws IOException;
+
+    /**
+     * Writes a null element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeNull(byte type) throws IOException;
+
+    /**
+     * Writes an octet string element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The byte array containing the octet string data.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte type, byte[] value) throws IOException;
+
+    /**
+     * Writes an octet string element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The byte array containing the octet string data.
+     * @param offset
+     *            The offset in the byte array.
+     * @param length
+     *            The number of bytes to write.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte type, byte[] value, int offset, int length) throws IOException;
+
+    /**
+     * Writes an octet string element using the provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The octet string value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte type, ByteSequence value) throws IOException;
+
+    /**
+     * Writes a string as a UTF-8 encoded octet string element using the
+     * provided type tag.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @param value
+     *            The string to be written as a UTF-8 encoded octet string.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte type, String value) throws IOException;
+
+    /**
+     * Writes an octet string element using the Universal Octet String ASN.1
+     * type tag.
+     *
+     * @param value
+     *            The byte array containing the octet string data.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte[] value) throws IOException;
+
+    /**
+     * Writes an octet string element using the Universal Octet String ASN.1
+     * type tag.
+     *
+     * @param value
+     *            The byte array containing the octet string data.
+     * @param offset
+     *            The offset in the byte array.
+     * @param length
+     *            The number of bytes to write.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(byte[] value, int offset, int length) throws IOException;
+
+    /**
+     * Writes an octet string element using the Universal Octet String ASN.1
+     * type tag.
+     *
+     * @param value
+     *            The octet string value.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(ByteSequence value) throws IOException;
+
+    /**
+     * Writes a string as a UTF-8 encoded octet string element using the
+     * Universal Octet String ASN.1 type tag.
+     *
+     * @param value
+     *            The string to be written as a UTF-8 encoded octet string.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeOctetString(String value) throws IOException;
+
+    /**
+     * Writes a sequence element using the Universal Sequence ASN.1 type tag.
+     * All further writes will append elements to the sequence until
+     * {@link #writeEndSequence} is called.
+     *
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeStartSequence() throws IOException;
+
+    /**
+     * Writes a sequence element using the provided type tag. All further writes
+     * will append elements to the sequence until {@link #writeEndSequence} is
+     * called.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeStartSequence(byte type) throws IOException;
+
+    /**
+     * Writes a set element using the Universal Set ASN.1 type tag. All further
+     * writes will append elements to the set until {@link #writeEndSet} is
+     * called.
+     *
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeStartSet() throws IOException;
+
+    /**
+     * Writes a set element using the provided type tag. All further writes will
+     * append elements to the set until {@link #writeEndSet} is called.
+     *
+     * @param type
+     *            The type tag of the element.
+     * @return A reference to this ASN.1 writer.
+     * @throws IOException
+     *             If an error occurs while writing the element.
+     */
+    ASN1Writer writeStartSet(byte type) throws IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Reader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Reader.java
new file mode 100644
index 0000000..7a05fd2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Reader.java
@@ -0,0 +1,154 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+
+package org.forgerock.opendj.io;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ASN1_UNEXPECTED_TAG;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * An abstract {@code ASN1Reader} which can be used as the basis for
+ * implementing new ASN1 reader implementations.
+ */
+public abstract class AbstractASN1Reader implements ASN1Reader {
+    /** Creates a new abstract ASN.1 reader. */
+    protected AbstractASN1Reader() {
+        // No implementation required.
+    }
+
+    @Override
+    public boolean readBoolean(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_BOOLEAN_TYPE;
+        }
+        checkType(type);
+        return readBoolean();
+    }
+
+    @Override
+    public int readEnumerated(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_ENUMERATED_TYPE;
+        }
+        checkType(type);
+        return readEnumerated();
+    }
+
+    @Override
+    public long readInteger(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_INTEGER_TYPE;
+        }
+        checkType(type);
+        return readInteger();
+    }
+
+    @Override
+    public void readNull(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_NULL_TYPE;
+        }
+        checkType(type);
+        readNull();
+    }
+
+    @Override
+    public ByteString readOctetString(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_OCTET_STRING_TYPE;
+        }
+        checkType(type);
+        return readOctetString();
+    }
+
+    @Override
+    public ByteStringBuilder readOctetString(byte type, final ByteStringBuilder builder)
+            throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_OCTET_STRING_TYPE;
+        }
+        checkType(type);
+        readOctetString(builder);
+        return builder;
+    }
+
+    @Override
+    public String readOctetStringAsString(byte type) throws IOException {
+        // We could cache the UTF-8 CharSet if performance proves to be an
+        // issue.
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_OCTET_STRING_TYPE;
+        }
+        checkType(type);
+        return readOctetStringAsString();
+    }
+
+    @Override
+    public void readStartExplicitTag(byte type) throws IOException {
+        if (type == 0x00) {
+            type = (ASN1.TYPE_MASK_CONTEXT | ASN1.TYPE_MASK_CONSTRUCTED);
+        }
+        checkType(type);
+        readStartExplicitTag();
+    }
+
+    @Override
+    public void readStartSequence(byte type) throws IOException {
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_SEQUENCE_TYPE;
+        }
+        checkType(type);
+        readStartSequence();
+    }
+
+    @Override
+    public void readStartSet(byte type) throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        if (type == 0x00) {
+            type = ASN1.UNIVERSAL_SET_TYPE;
+        }
+        checkType(type);
+        readStartSet();
+    }
+
+    @Override
+    public ASN1Reader skipElement(final byte expectedType) throws IOException {
+        if (peekType() != expectedType) {
+            final LocalizableMessage message =
+                    ERR_ASN1_UNEXPECTED_TAG.get(expectedType, peekType());
+            throw DecodeException.fatalError(message);
+        }
+        skipElement();
+        return this;
+    }
+
+    private void checkType(final byte expectedType) throws IOException {
+        if (peekType() != expectedType) {
+            final LocalizableMessage message =
+                    ERR_ASN1_UNEXPECTED_TAG.get(expectedType, peekType());
+            throw DecodeException.fatalError(message);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Writer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Writer.java
new file mode 100644
index 0000000..7501180
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractASN1Writer.java
@@ -0,0 +1,95 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * An abstract {@code ASN1Writer} which can be used as the basis for
+ * implementing new ASN1 writer implementations.
+ */
+public abstract class AbstractASN1Writer implements ASN1Writer {
+
+    /** Creates a new abstract ASN.1 writer. */
+    protected AbstractASN1Writer() {
+        // No implementation required.
+    }
+
+    @Override
+    public ASN1Writer writeBoolean(final boolean value) throws IOException {
+        return writeBoolean(ASN1.UNIVERSAL_BOOLEAN_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeEnumerated(final int value) throws IOException {
+        return writeEnumerated(ASN1.UNIVERSAL_ENUMERATED_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final int value) throws IOException {
+        return writeInteger(ASN1.UNIVERSAL_INTEGER_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final long value) throws IOException {
+        return writeInteger(ASN1.UNIVERSAL_INTEGER_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeNull() throws IOException {
+        return writeNull(ASN1.UNIVERSAL_NULL_TYPE);
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(byte type, byte[] value) throws IOException {
+        return writeOctetString(type, value, 0, value.length);
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(byte[] value) throws IOException {
+        return writeOctetString(value, 0, value.length);
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte[] value, final int offset, final int length)
+            throws IOException {
+        return writeOctetString(ASN1.UNIVERSAL_OCTET_STRING_TYPE, value, offset, length);
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final ByteSequence value) throws IOException {
+        return writeOctetString(ASN1.UNIVERSAL_OCTET_STRING_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final String value) throws IOException {
+        return writeOctetString(ASN1.UNIVERSAL_OCTET_STRING_TYPE, value);
+    }
+
+    @Override
+    public ASN1Writer writeStartSequence() throws IOException {
+        return writeStartSequence(ASN1.UNIVERSAL_SEQUENCE_TYPE);
+    }
+
+    @Override
+    public ASN1Writer writeStartSet() throws IOException {
+        return writeStartSet(ASN1.UNIVERSAL_SET_TYPE);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractLDAPMessageHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractLDAPMessageHandler.java
new file mode 100644
index 0000000..38de296
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/AbstractLDAPMessageHandler.java
@@ -0,0 +1,244 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Response;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+/**
+ * This class provides a skeletal implementation of the
+ * {@link LDAPMessageHandler} interface, in order to minimize the effort
+ * required to implement this interface. By default each method throws a fatal
+ * {@link DecodeException}.
+ */
+public abstract class AbstractLDAPMessageHandler implements LDAPMessageHandler {
+    /**
+     * Default constructor.
+     */
+    protected AbstractLDAPMessageHandler() {
+        // No implementation required.
+    }
+
+    @Override
+    public void abandonRequest(final int messageID, final AbandonRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void addRequest(final int messageID, final AddRequest request) throws DecodeException,
+            IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void addResult(final int messageID, final Result result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void bindRequest(final int messageID, final int version, final GenericBindRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void bindResult(final int messageID, final BindResult result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void compareRequest(final int messageID, final CompareRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void compareResult(final int messageID, final CompareResult result)
+            throws DecodeException, IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void deleteRequest(final int messageID, final DeleteRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void deleteResult(final int messageID, final Result result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public <R extends ExtendedResult> void extendedRequest(final int messageID,
+            final ExtendedRequest<R> request) throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void extendedResult(final int messageID, final ExtendedResult result)
+            throws DecodeException, IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void intermediateResponse(final int messageID, final IntermediateResponse response)
+            throws DecodeException, IOException {
+        throw newUnexpectedResponseException(messageID, response);
+    }
+
+    @Override
+    public void modifyDNRequest(final int messageID, final ModifyDNRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void modifyDNResult(final int messageID, final Result result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void modifyRequest(final int messageID, final ModifyRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void modifyResult(final int messageID, final Result result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void searchRequest(final int messageID, final SearchRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void searchResult(final int messageID, final Result result) throws DecodeException,
+            IOException {
+        throw newUnexpectedResponseException(messageID, result);
+    }
+
+    @Override
+    public void searchResultEntry(final int messageID, final SearchResultEntry entry)
+            throws DecodeException, IOException {
+        throw newUnexpectedResponseException(messageID, entry);
+    }
+
+    @Override
+    public void searchResultReference(final int messageID, final SearchResultReference reference)
+            throws DecodeException, IOException {
+        throw newUnexpectedResponseException(messageID, reference);
+    }
+
+    @Override
+    public void unbindRequest(final int messageID, final UnbindRequest request)
+            throws DecodeException, IOException {
+        throw newUnexpectedRequestException(messageID, request);
+    }
+
+    @Override
+    public void unrecognizedMessage(final int messageID, final byte messageTag,
+            final ByteString messageBytes) throws DecodeException, IOException {
+        throw newUnsupportedMessageException(messageID, messageTag, messageBytes);
+    }
+
+    /**
+     * Returns a decoding exception suitable for use when an unsupported LDAP
+     * message is received.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param messageTag
+     *            The LDAP message type.
+     * @param messageBytes
+     *            The LDAP message content.
+     * @return A decoding exception suitable for use when an unsupported LDAP
+     *         message is received.
+     */
+    protected DecodeException newUnsupportedMessageException(final int messageID,
+            final byte messageTag, final ByteString messageBytes) {
+        return DecodeException.fatalError(LocalizableMessage.raw(
+                "Unsupported LDAP message: id=%d, tag=%d, content=%s", messageID, messageTag,
+                messageBytes));
+    }
+
+    /**
+     * Returns a decoding exception suitable for use when an unexpected LDAP
+     * request is received.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The LDAP request.
+     * @return A decoding exception suitable for use when an unexpected LDAP
+     *         request is received.
+     */
+    protected DecodeException newUnexpectedRequestException(final int messageID,
+            final Request request) {
+        return DecodeException.fatalError(LocalizableMessage.raw(
+                "Unexpected LDAP request: id=%d, message=%s", messageID, request));
+    }
+
+    /**
+     * Returns a decoding exception suitable for use when an unexpected LDAP
+     * response is received.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param response
+     *            The LDAP response.
+     * @return A decoding exception suitable for use when an unexpected LDAP
+     *         response is received.
+     */
+    protected DecodeException newUnexpectedResponseException(final int messageID,
+            final Response response) {
+        return DecodeException.fatalError(LocalizableMessage.raw(
+                "Unexpected LDAP response: id=%d, message=%s", messageID, response));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAP.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAP.java
new file mode 100644
index 0000000..f431072
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAP.java
@@ -0,0 +1,833 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.FilterVisitor;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * This class contains various static utility methods encoding and decoding LDAP
+ * protocol elements.
+ *
+ * @see LDAPReader
+ * @see LDAPWriter
+ */
+public final class LDAP {
+    // @Checkstyle:ignore AvoidNestedBlocks
+
+    /**
+     * The OID for the Kerberos V GSSAPI mechanism.
+     */
+    public static final String OID_GSSAPI_KERBEROS_V = "1.2.840.113554.1.2.2";
+
+    /**
+     * The OID for the LDAP notice of disconnection extended operation.
+     */
+    public static final String OID_NOTICE_OF_DISCONNECTION = "1.3.6.1.4.1.1466.20036";
+
+    /**
+     * The protocol op type for abandon requests.
+     */
+    public static final byte OP_TYPE_ABANDON_REQUEST = 0x50;
+
+    /**
+     * The protocol op type for add requests.
+     */
+    public static final byte OP_TYPE_ADD_REQUEST = 0x68;
+
+    /**
+     * The protocol op type for add responses.
+     */
+    public static final byte OP_TYPE_ADD_RESPONSE = 0x69;
+
+    /**
+     * The protocol op type for bind requests.
+     */
+    public static final byte OP_TYPE_BIND_REQUEST = 0x60;
+
+    /**
+     * The protocol op type for bind responses.
+     */
+    public static final byte OP_TYPE_BIND_RESPONSE = 0x61;
+
+    /**
+     * The protocol op type for compare requests.
+     */
+    public static final byte OP_TYPE_COMPARE_REQUEST = 0x6E;
+
+    /**
+     * The protocol op type for compare responses.
+     */
+    public static final byte OP_TYPE_COMPARE_RESPONSE = 0x6F;
+
+    /**
+     * The protocol op type for delete requests.
+     */
+    public static final byte OP_TYPE_DELETE_REQUEST = 0x4A;
+
+    /**
+     * The protocol op type for delete responses.
+     */
+    public static final byte OP_TYPE_DELETE_RESPONSE = 0x6B;
+
+    /**
+     * The protocol op type for extended requests.
+     */
+    public static final byte OP_TYPE_EXTENDED_REQUEST = 0x77;
+
+    /**
+     * The protocol op type for extended responses.
+     */
+    public static final byte OP_TYPE_EXTENDED_RESPONSE = 0x78;
+
+    /**
+     * The protocol op type for intermediate responses.
+     */
+    public static final byte OP_TYPE_INTERMEDIATE_RESPONSE = 0x79;
+
+    /**
+     * The protocol op type for modify DN requests.
+     */
+    public static final byte OP_TYPE_MODIFY_DN_REQUEST = 0x6C;
+
+    /**
+     * The protocol op type for modify DN responses.
+     */
+    public static final byte OP_TYPE_MODIFY_DN_RESPONSE = 0x6D;
+
+    /**
+     * The protocol op type for modify requests.
+     */
+    public static final byte OP_TYPE_MODIFY_REQUEST = 0x66;
+
+    /**
+     * The protocol op type for modify responses.
+     */
+    public static final byte OP_TYPE_MODIFY_RESPONSE = 0x67;
+    /**
+     * The protocol op type for search requests.
+     */
+    public static final byte OP_TYPE_SEARCH_REQUEST = 0x63;
+    /**
+     * The protocol op type for search result done elements.
+     */
+    public static final byte OP_TYPE_SEARCH_RESULT_DONE = 0x65;
+    /**
+     * The protocol op type for search result entries.
+     */
+    public static final byte OP_TYPE_SEARCH_RESULT_ENTRY = 0x64;
+    /**
+     * The protocol op type for search result references.
+     */
+    public static final byte OP_TYPE_SEARCH_RESULT_REFERENCE = 0x73;
+    /**
+     * The protocol op type for unbind requests.
+     */
+    public static final byte OP_TYPE_UNBIND_REQUEST = 0x42;
+    /**
+     * The BER type to use for the AuthenticationChoice element in a bind
+     * request when SASL authentication is to be used.
+     */
+    public static final byte TYPE_AUTHENTICATION_SASL = (byte) 0xA3;
+    /**
+     * The BER type to use for the AuthenticationChoice element in a bind
+     * request when simple authentication is to be used.
+     */
+    public static final byte TYPE_AUTHENTICATION_SIMPLE = (byte) 0x80;
+    /**
+     * The BER type to use for encoding the sequence of controls in an LDAP
+     * message.
+     */
+    public static final byte TYPE_CONTROL_SEQUENCE = (byte) 0xA0;
+    /**
+     * The BER type to use for the OID of an extended request.
+     */
+    public static final byte TYPE_EXTENDED_REQUEST_OID = (byte) 0x80;
+    /**
+     * The BER type to use for the value of an extended request.
+     */
+    public static final byte TYPE_EXTENDED_REQUEST_VALUE = (byte) 0x81;
+    /**
+     * The BER type to use for the OID of an extended response.
+     */
+    public static final byte TYPE_EXTENDED_RESPONSE_OID = (byte) 0x8A;
+    /**
+     * The BER type to use for the value of an extended response.
+     */
+    public static final byte TYPE_EXTENDED_RESPONSE_VALUE = (byte) 0x8B;
+    /**
+     * The BER type to use for AND filter components.
+     */
+    public static final byte TYPE_FILTER_AND = (byte) 0xA0;
+    /**
+     * The BER type to use for approximate filter components.
+     */
+    public static final byte TYPE_FILTER_APPROXIMATE = (byte) 0xA8;
+    /**
+     * The BER type to use for equality filter components.
+     */
+    public static final byte TYPE_FILTER_EQUALITY = (byte) 0xA3;
+    /**
+     * The BER type to use for extensible matching filter components.
+     */
+    public static final byte TYPE_FILTER_EXTENSIBLE_MATCH = (byte) 0xA9;
+    /**
+     * The BER type to use for greater than or equal to filter components.
+     */
+    public static final byte TYPE_FILTER_GREATER_OR_EQUAL = (byte) 0xA5;
+    /**
+     * The BER type to use for less than or equal to filter components.
+     */
+    public static final byte TYPE_FILTER_LESS_OR_EQUAL = (byte) 0xA6;
+    /**
+     * The BER type to use for NOT filter components.
+     */
+    public static final byte TYPE_FILTER_NOT = (byte) 0xA2;
+    /**
+     * The BER type to use for OR filter components.
+     */
+    public static final byte TYPE_FILTER_OR = (byte) 0xA1;
+    /**
+     * The BER type to use for presence filter components.
+     */
+    public static final byte TYPE_FILTER_PRESENCE = (byte) 0x87;
+    /**
+     * The BER type to use for substring filter components.
+     */
+    public static final byte TYPE_FILTER_SUBSTRING = (byte) 0xA4;
+    /**
+     * The BER type to use for the OID of an intermediate response message.
+     */
+    public static final byte TYPE_INTERMEDIATE_RESPONSE_OID = (byte) 0x80;
+    /**
+     * The BER type to use for the value of an intermediate response message.
+     */
+    public static final byte TYPE_INTERMEDIATE_RESPONSE_VALUE = (byte) 0x81;
+    /**
+     * The BER type to use for the DN attributes flag in a matching rule
+     * assertion.
+     */
+    public static final byte TYPE_MATCHING_RULE_DN_ATTRIBUTES = (byte) 0x84;
+    /**
+     * The BER type to use for the matching rule OID in a matching rule
+     * assertion.
+     */
+    public static final byte TYPE_MATCHING_RULE_ID = (byte) 0x81;
+    /**
+     * The BER type to use for the attribute type in a matching rule assertion.
+     */
+    public static final byte TYPE_MATCHING_RULE_TYPE = (byte) 0x82;
+    /**
+     * The BER type to use for the assertion value in a matching rule assertion.
+     */
+    public static final byte TYPE_MATCHING_RULE_VALUE = (byte) 0x83;
+    /**
+     * The BER type to use for the newSuperior component of a modify DN request.
+     */
+    public static final byte TYPE_MODIFY_DN_NEW_SUPERIOR = (byte) 0x80;
+    /**
+     * The BER type to use for encoding the sequence of referral URLs in an
+     * LDAPResult element.
+     */
+    public static final byte TYPE_REFERRAL_SEQUENCE = (byte) 0xA3;
+    /**
+     * The BER type to use for the server SASL credentials in a bind response.
+     */
+    public static final byte TYPE_SERVER_SASL_CREDENTIALS = (byte) 0x87;
+    /**
+     * The BER type to use for the subAny component(s) of a substring filter.
+     */
+    public static final byte TYPE_SUBANY = (byte) 0x81;
+    /**
+     * The BER type to use for the subFinal components of a substring filter.
+     */
+    public static final byte TYPE_SUBFINAL = (byte) 0x82;
+    /**
+     * The BER type to use for the subInitial component of a substring filter.
+     */
+    public static final byte TYPE_SUBINITIAL = (byte) 0x80;
+    private static final FilterVisitor<IOException, ASN1Writer> ASN1_ENCODER =
+            new FilterVisitor<IOException, ASN1Writer>() {
+
+                @Override
+                public IOException visitAndFilter(final ASN1Writer writer,
+                        final List<Filter> subFilters) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_AND);
+                        for (final Filter subFilter : subFilters) {
+                            final IOException e = subFilter.accept(this, writer);
+                            if (e != null) {
+                                return e;
+                            }
+                        }
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitApproxMatchFilter(final ASN1Writer writer,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_APPROXIMATE);
+                        writer.writeOctetString(attributeDescription);
+                        writer.writeOctetString(assertionValue);
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitEqualityMatchFilter(final ASN1Writer writer,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_EQUALITY);
+                        writer.writeOctetString(attributeDescription);
+                        writer.writeOctetString(assertionValue);
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitExtensibleMatchFilter(final ASN1Writer writer,
+                        final String matchingRule, final String attributeDescription,
+                        final ByteString assertionValue, final boolean dnAttributes) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_EXTENSIBLE_MATCH);
+
+                        if (matchingRule != null) {
+                            writer.writeOctetString(LDAP.TYPE_MATCHING_RULE_ID, matchingRule);
+                        }
+
+                        if (attributeDescription != null) {
+                            writer.writeOctetString(LDAP.TYPE_MATCHING_RULE_TYPE,
+                                    attributeDescription);
+                        }
+
+                        writer.writeOctetString(LDAP.TYPE_MATCHING_RULE_VALUE, assertionValue);
+
+                        if (dnAttributes) {
+                            writer.writeBoolean(LDAP.TYPE_MATCHING_RULE_DN_ATTRIBUTES, true);
+                        }
+
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitGreaterOrEqualFilter(final ASN1Writer writer,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_GREATER_OR_EQUAL);
+                        writer.writeOctetString(attributeDescription);
+                        writer.writeOctetString(assertionValue);
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitLessOrEqualFilter(final ASN1Writer writer,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_LESS_OR_EQUAL);
+                        writer.writeOctetString(attributeDescription);
+                        writer.writeOctetString(assertionValue);
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitNotFilter(final ASN1Writer writer, final Filter subFilter) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_NOT);
+                        final IOException e = subFilter.accept(this, writer);
+                        if (e != null) {
+                            return e;
+                        }
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitOrFilter(final ASN1Writer writer,
+                        final List<Filter> subFilters) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_OR);
+                        for (final Filter subFilter : subFilters) {
+                            final IOException e = subFilter.accept(this, writer);
+                            if (e != null) {
+                                return e;
+                            }
+                        }
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitPresentFilter(final ASN1Writer writer,
+                        final String attributeDescription) {
+                    try {
+                        writer.writeOctetString(LDAP.TYPE_FILTER_PRESENCE, attributeDescription);
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitSubstringsFilter(final ASN1Writer writer,
+                        final String attributeDescription, final ByteString initialSubstring,
+                        final List<ByteString> anySubstrings, final ByteString finalSubstring) {
+                    try {
+                        writer.writeStartSequence(LDAP.TYPE_FILTER_SUBSTRING);
+                        writer.writeOctetString(attributeDescription);
+
+                        writer.writeStartSequence();
+                        if (initialSubstring != null) {
+                            writer.writeOctetString(LDAP.TYPE_SUBINITIAL, initialSubstring);
+                        }
+
+                        for (final ByteSequence anySubstring : anySubstrings) {
+                            writer.writeOctetString(LDAP.TYPE_SUBANY, anySubstring);
+                        }
+
+                        if (finalSubstring != null) {
+                            writer.writeOctetString(LDAP.TYPE_SUBFINAL, finalSubstring);
+                        }
+                        writer.writeEndSequence();
+
+                        writer.writeEndSequence();
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public IOException visitUnrecognizedFilter(final ASN1Writer writer,
+                        final byte filterTag, final ByteString filterBytes) {
+                    try {
+                        writer.writeOctetString(filterTag, filterBytes);
+                        return null;
+                    } catch (final IOException e) {
+                        return e;
+                    }
+                }
+            };
+
+    /**
+     * Creates a new LDAP reader which will read LDAP messages from an ASN.1
+     * reader using the provided decoding options.
+     *
+     * @param <R>
+     *            The type of ASN.1 reader used for decoding elements.
+     * @param asn1Reader
+     *            The ASN.1 reader from which LDAP messages will be read.
+     * @param options
+     *            LDAP message decoding options.
+     * @return A new LDAP reader which will read LDAP messages from an ASN.1
+     *         reader using the provided decoding options.
+     */
+    public static <R extends ASN1Reader> LDAPReader<R> getReader(final R asn1Reader,
+            final DecodeOptions options) {
+        return new LDAPReader<>(asn1Reader, options);
+    }
+
+    /**
+     * Creates a new LDAP writer which will write LDAP messages to the provided
+     * ASN.1 writer.
+     *
+     * @param <W>
+     *            The type of ASN.1 writer used for encoding elements.
+     * @param asn1Writer
+     *            The ASN.1 writer to which LDAP messages will be written.
+     * @return A new LDAP writer which will write LDAP messages to the provided
+     *         ASN.1 writer.
+     */
+    public static <W extends ASN1Writer> LDAPWriter<W> getWriter(final W asn1Writer) {
+        return new LDAPWriter<>(asn1Writer);
+    }
+
+    /**
+     * Reads the next ASN.1 element from the provided {@code ASN1Reader} as a
+     * {@code Filter}.
+     *
+     * @param reader
+     *            The {@code ASN1Reader} from which the ASN.1 encoded
+     *            {@code Filter} should be read.
+     * @return The decoded {@code Filter}.
+     * @throws IOException
+     *             If an error occurs while reading from {@code reader}.
+     */
+    public static Filter readFilter(final ASN1Reader reader) throws IOException {
+        final byte type = reader.peekType();
+        switch (type) {
+        case LDAP.TYPE_FILTER_AND:
+            return readAndFilter(reader);
+        case LDAP.TYPE_FILTER_OR:
+            return readOrFilter(reader);
+        case LDAP.TYPE_FILTER_NOT:
+            return readNotFilter(reader);
+        case LDAP.TYPE_FILTER_EQUALITY:
+            return readEqualityMatchFilter(reader);
+        case LDAP.TYPE_FILTER_GREATER_OR_EQUAL:
+            return readGreaterOrEqualMatchFilter(reader);
+        case LDAP.TYPE_FILTER_LESS_OR_EQUAL:
+            return readLessOrEqualMatchFilter(reader);
+        case LDAP.TYPE_FILTER_APPROXIMATE:
+            return readApproxMatchFilter(reader);
+        case LDAP.TYPE_FILTER_SUBSTRING:
+            return readSubstringsFilter(reader);
+        case LDAP.TYPE_FILTER_PRESENCE:
+            return Filter.present(reader.readOctetStringAsString(type));
+        case LDAP.TYPE_FILTER_EXTENSIBLE_MATCH:
+            return readExtensibleMatchFilter(reader);
+        default:
+            return Filter.unrecognized(type, reader.readOctetString(type));
+        }
+    }
+
+    /**
+     * Reads the next ASN.1 element from the provided {@code ASN1Reader} as a an
+     * {@code Entry}.
+     *
+     * @param reader
+     *            The {@code ASN1Reader} from which the ASN.1 encoded
+     *            {@code Entry} should be read.
+     * @param options
+     *            The decode options to use when decoding the entry.
+     * @return The decoded {@code Entry}.
+     * @throws IOException
+     *             If an error occurs while reading from {@code reader}.
+     */
+    public static Entry readEntry(final ASN1Reader reader, final DecodeOptions options)
+            throws IOException {
+        return readEntry(reader, OP_TYPE_SEARCH_RESULT_ENTRY, options);
+    }
+
+    /**
+     * Writes a {@code Filter} to the provided {@code ASN1Writer}.
+     *
+     * @param writer
+     *            The {@code ASN1Writer} to which the ASN.1 encoded
+     *            {@code Filter} should be written.
+     * @param filter
+     *            The filter.
+     * @throws IOException
+     *             If an error occurs while writing to {@code writer}.
+     */
+    public static void writeFilter(final ASN1Writer writer, final Filter filter) throws IOException {
+        final IOException e = filter.accept(ASN1_ENCODER, writer);
+        if (e != null) {
+            throw e;
+        }
+    }
+
+    /**
+     * Writes an {@code Entry} to the provided {@code ASN1Writer}.
+     *
+     * @param writer
+     *            The {@code ASN1Writer} to which the ASN.1 encoded
+     *            {@code Entry} should be written.
+     * @param entry
+     *            The entry.
+     * @throws IOException
+     *             If an error occurs while writing to {@code writer}.
+     */
+    public static void writeEntry(final ASN1Writer writer, final Entry entry) throws IOException {
+        writeEntry(writer, OP_TYPE_SEARCH_RESULT_ENTRY, entry);
+    }
+
+    static AttributeDescription readAttributeDescription(final String attributeDescription,
+            final Schema schema) throws DecodeException {
+        try {
+            return AttributeDescription.valueOf(attributeDescription, schema);
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+
+    static DN readDN(final String dn, final Schema schema) throws DecodeException {
+        try {
+            return DN.valueOf(dn, schema);
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+
+    static Entry readEntry(final ASN1Reader reader, final byte tagType, final DecodeOptions options)
+            throws DecodeException, IOException {
+        reader.readStartSequence(tagType);
+        final Entry entry;
+        try {
+            final String dnString = reader.readOctetStringAsString();
+            final Schema schema = options.getSchemaResolver().resolveSchema(dnString);
+            final DN dn = readDN(dnString, schema);
+            entry = options.getEntryFactory().newEntry(dn);
+            reader.readStartSequence();
+            try {
+                while (reader.hasNextElement()) {
+                    reader.readStartSequence();
+                    try {
+                        final String ads = reader.readOctetStringAsString();
+                        final AttributeDescription ad = readAttributeDescription(ads, schema);
+                        final Attribute attribute = options.getAttributeFactory().newAttribute(ad);
+                        reader.readStartSet();
+                        try {
+                            while (reader.hasNextElement()) {
+                                attribute.add(reader.readOctetString());
+                            }
+                            entry.addAttribute(attribute);
+                        } finally {
+                            reader.readEndSet();
+                        }
+                    } finally {
+                        reader.readEndSequence();
+                    }
+                }
+            } finally {
+                reader.readEndSequence();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        return entry;
+    }
+
+    static void writeAttribute(final ASN1Writer writer, final Attribute attribute)
+            throws IOException {
+        writer.writeStartSequence();
+        {
+            writer.writeOctetString(attribute.getAttributeDescriptionAsString());
+            writer.writeStartSet();
+            {
+                for (final ByteString value : attribute) {
+                    writer.writeOctetString(value);
+                }
+            }
+            writer.writeEndSet();
+        }
+        writer.writeEndSequence();
+    }
+
+    static void writeEntry(final ASN1Writer writer, final byte typeTag, final Entry entry)
+            throws IOException {
+        writer.writeStartSequence(typeTag);
+        {
+            writer.writeOctetString(entry.getName().toString());
+            writer.writeStartSequence();
+            {
+                for (final Attribute attr : entry.getAllAttributes()) {
+                    writeAttribute(writer, attr);
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writer.writeEndSequence();
+    }
+
+    private static Filter readAndFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_AND);
+        try {
+            if (reader.hasNextElement()) {
+                final List<Filter> subFilters = new LinkedList<>();
+                do {
+                    subFilters.add(readFilter(reader));
+                } while (reader.hasNextElement());
+                return Filter.and(subFilters);
+            } else {
+                // No sub-filters - this is an RFC 4526 absolute true filter.
+                return Filter.alwaysTrue();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readApproxMatchFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_APPROXIMATE);
+        try {
+            final String attributeDescription = reader.readOctetStringAsString();
+            final ByteString assertionValue = reader.readOctetString();
+            return Filter.approx(attributeDescription, assertionValue);
+        } finally {
+            reader.readEndSequence();
+        }
+
+    }
+
+    private static Filter readEqualityMatchFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_EQUALITY);
+        try {
+            final String attributeDescription = reader.readOctetStringAsString();
+            final ByteString assertionValue = reader.readOctetString();
+            return Filter.equality(attributeDescription, assertionValue);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readExtensibleMatchFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_EXTENSIBLE_MATCH);
+        try {
+            String matchingRule = null;
+            if (reader.peekType() == LDAP.TYPE_MATCHING_RULE_ID) {
+                matchingRule = reader.readOctetStringAsString(LDAP.TYPE_MATCHING_RULE_ID);
+            }
+            String attributeDescription = null;
+            if (reader.peekType() == LDAP.TYPE_MATCHING_RULE_TYPE) {
+                attributeDescription = reader.readOctetStringAsString(LDAP.TYPE_MATCHING_RULE_TYPE);
+            }
+            boolean dnAttributes = false;
+            if (reader.hasNextElement()
+                    && (reader.peekType() == LDAP.TYPE_MATCHING_RULE_DN_ATTRIBUTES)) {
+                dnAttributes = reader.readBoolean();
+            }
+            final ByteString assertionValue = reader.readOctetString(LDAP.TYPE_MATCHING_RULE_VALUE);
+            return Filter.extensible(matchingRule, attributeDescription, assertionValue,
+                    dnAttributes);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readGreaterOrEqualMatchFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_GREATER_OR_EQUAL);
+        try {
+            final String attributeDescription = reader.readOctetStringAsString();
+            final ByteString assertionValue = reader.readOctetString();
+            return Filter.greaterOrEqual(attributeDescription, assertionValue);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readLessOrEqualMatchFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_LESS_OR_EQUAL);
+        try {
+            final String attributeDescription = reader.readOctetStringAsString();
+            final ByteString assertionValue = reader.readOctetString();
+            return Filter.lessOrEqual(attributeDescription, assertionValue);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readNotFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_NOT);
+        try {
+            return Filter.not(readFilter(reader));
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readOrFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_OR);
+        try {
+            if (reader.hasNextElement()) {
+                final List<Filter> subFilters = new LinkedList<>();
+                do {
+                    subFilters.add(readFilter(reader));
+                } while (reader.hasNextElement());
+                return Filter.or(subFilters);
+            } else {
+                // No sub-filters - this is an RFC 4526 absolute false filter.
+                return Filter.alwaysFalse();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private static Filter readSubstringsFilter(final ASN1Reader reader) throws IOException {
+        reader.readStartSequence(LDAP.TYPE_FILTER_SUBSTRING);
+        try {
+            final String attributeDescription = reader.readOctetStringAsString();
+            reader.readStartSequence();
+            try {
+                // FIXME: There should be at least one element in this substring
+                // filter sequence.
+                ByteString initialSubstring = null;
+                if (reader.peekType() == LDAP.TYPE_SUBINITIAL) {
+                    initialSubstring = reader.readOctetString(LDAP.TYPE_SUBINITIAL);
+                }
+                final List<ByteString> anySubstrings;
+                if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_SUBANY)) {
+                    anySubstrings = new LinkedList<>();
+                    do {
+                        anySubstrings.add(reader.readOctetString(LDAP.TYPE_SUBANY));
+                    } while (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_SUBANY));
+                } else {
+                    anySubstrings = Collections.emptyList();
+                }
+                ByteString finalSubstring = null;
+                if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_SUBFINAL)) {
+                    finalSubstring = reader.readOctetString(LDAP.TYPE_SUBFINAL);
+                }
+                return Filter.substrings(attributeDescription, initialSubstring, anySubstrings,
+                        finalSubstring);
+            } finally {
+                reader.readEndSequence();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    /** Prevent instantiation. */
+    private LDAP() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPMessageHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPMessageHandler.java
new file mode 100644
index 0000000..c510dd6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPMessageHandler.java
@@ -0,0 +1,389 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+/**
+ * An interface for handling LDAP messages decoded using an {@link LDAPReader}.
+ */
+public interface LDAPMessageHandler {
+
+    /**
+     * Handles an LDAP abandon request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded abandon request.
+     * @throws DecodeException
+     *             If this handler does not support abandon requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void abandonRequest(int messageID, AbandonRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP add request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded add request.
+     * @throws DecodeException
+     *             If this handler does not support add requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void addRequest(int messageID, AddRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP add result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded add result.
+     * @throws DecodeException
+     *             If this handler does not support add results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void addResult(int messageID, Result result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP bind request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param version
+     *            The requested LDAP protocol version.
+     * @param request
+     *            The decoded bind request.
+     * @throws DecodeException
+     *             If this handler does not support bind requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void bindRequest(int messageID, int version, GenericBindRequest request)
+            throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP bind result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded bind result.
+     * @throws DecodeException
+     *             If this handler does not support bind results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void bindResult(int messageID, BindResult result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP compare request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded compare request.
+     * @throws DecodeException
+     *             If this handler does not support compare requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void compareRequest(int messageID, CompareRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP compare result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded compare result.
+     * @throws DecodeException
+     *             If this handler does not support compare results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void compareResult(int messageID, CompareResult result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP delete request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded delete request.
+     * @throws DecodeException
+     *             If this handler does not support delete requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void deleteRequest(int messageID, DeleteRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP delete result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded delete result.
+     * @throws DecodeException
+     *             If this handler does not support delete results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void deleteResult(int messageID, Result result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP extended request message.
+     *
+     * @param <R>
+     *            type of extended result
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded extended request.
+     * @throws DecodeException
+     *             If this handler does not support extended requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    <R extends ExtendedResult> void extendedRequest(int messageID, ExtendedRequest<R> request)
+            throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP extended result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded extended result.
+     * @throws DecodeException
+     *             If this handler does not support extended results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void extendedResult(int messageID, ExtendedResult result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP intermediate response message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param response
+     *            The decoded intermediate response.
+     * @throws DecodeException
+     *             If this handler does not support intermediate responses.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void intermediateResponse(int messageID, IntermediateResponse response) throws DecodeException,
+            IOException;
+
+    /**
+     * Handles an LDAP modify DN request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded modify DN request.
+     * @throws DecodeException
+     *             If this handler does not support modify DN requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void modifyDNRequest(int messageID, ModifyDNRequest request) throws DecodeException,
+            IOException;
+
+    /**
+     * Handles an LDAP modify DN result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded modify DN result.
+     * @throws DecodeException
+     *             If this handler does not support modify DN results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void modifyDNResult(int messageID, Result result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP modify request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded modify request.
+     * @throws DecodeException
+     *             If this handler does not support modify requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void modifyRequest(int messageID, ModifyRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP modify result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded modify result.
+     * @throws DecodeException
+     *             If this handler does not support modify results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void modifyResult(int messageID, Result result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP search request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded search request.
+     * @throws DecodeException
+     *             If this handler does not support search requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void searchRequest(int messageID, SearchRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP search result message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The decoded search result.
+     * @throws DecodeException
+     *             If this handler does not support search results.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void searchResult(int messageID, Result result) throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP search result entry message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param entry
+     *            The decoded search result entry.
+     * @throws DecodeException
+     *             If this handler does not support search result entries.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void searchResultEntry(int messageID, SearchResultEntry entry) throws DecodeException,
+            IOException;
+
+    /**
+     * Handles an LDAP search result reference message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param reference
+     *            The decoded search result reference.
+     * @throws DecodeException
+     *             If this handler does not support search result references.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             response.
+     */
+    void searchResultReference(int messageID, SearchResultReference reference)
+            throws DecodeException, IOException;
+
+    /**
+     * Handles an LDAP unbind request message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The decoded unbind request.
+     * @throws DecodeException
+     *             If this handler does not support unbind requests.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             request.
+     */
+    void unbindRequest(int messageID, UnbindRequest request) throws DecodeException, IOException;
+
+    /**
+     * Handles an unrecognized LDAP message.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param messageTag
+     *            The LDAP message type.
+     * @param messageBytes
+     *            The contents of the LDAP message.
+     * @throws DecodeException
+     *             If this handler does not support the message type.
+     * @throws IOException
+     *             If an unexpected IO error occurred while processing the
+     *             message.
+     */
+    void unrecognizedMessage(int messageID, byte messageTag, ByteString messageBytes)
+            throws DecodeException, IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPReader.java
new file mode 100644
index 0000000..2104f81
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPReader.java
@@ -0,0 +1,722 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.GenericExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericIntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Response;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * Reads LDAP messages from an underlying ASN.1 reader.
+ *
+ * @param <R>
+ *            The type of ASN.1 reader used for decoding elements.
+ */
+public final class LDAPReader<R extends ASN1Reader> {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private final DecodeOptions options;
+    private final R reader;
+
+    LDAPReader(final R asn1Reader, final DecodeOptions options) {
+        this.reader = asn1Reader;
+        this.options = options;
+    }
+
+    /**
+     * Returns the ASN.1 reader from which LDAP messages will be read.
+     *
+     * @return The ASN.1 reader from which LDAP messages will be read.
+     */
+    public R getASN1Reader() {
+        return reader;
+    }
+
+    /**
+     * Returns {@code true} if the next LDAP message can be read without
+     * blocking.
+     *
+     * @return {@code true} if the next LDAP message can be read without
+     *         blocking or {@code false} otherwise.
+     * @throws DecodeException
+     *             If the available data was not a valid LDAP message.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public boolean hasMessageAvailable() throws DecodeException, IOException {
+        return reader.elementAvailable();
+    }
+
+    /**
+     * Reads the next LDAP message from the underlying ASN.1 reader.
+     *
+     * @param handler
+     *            The message handler which will handle the decoded LDAP
+     *            message.
+     * @throws DecodeException
+     *             If the available data was not a valid LDAP message.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void readMessage(final LDAPMessageHandler handler) throws DecodeException, IOException {
+        reader.readStartSequence();
+        try {
+            final int messageID = (int) reader.readInteger();
+            readProtocolOp(messageID, handler);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private void readAbandonRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        final int msgToAbandon = (int) reader.readInteger(LDAP.OP_TYPE_ABANDON_REQUEST);
+        final AbandonRequest message = Requests.newAbandonRequest(msgToAbandon);
+        readControls(message);
+        logger.trace("DECODE LDAP ABANDON REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.abandonRequest(messageID, message);
+    }
+
+    private void readAddRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        final Entry entry = LDAP.readEntry(reader, LDAP.OP_TYPE_ADD_REQUEST, options);
+        final AddRequest message = Requests.newAddRequest(entry);
+        readControls(message);
+        logger.trace("DECODE LDAP ADD REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.addRequest(messageID, message);
+    }
+
+    private void readAddResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_ADD_RESPONSE);
+        final Result message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(
+                            diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP ADD RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.addResult(messageID, message);
+    }
+
+    private void readBindRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_BIND_REQUEST);
+        try {
+            final int protocolVersion = (int) reader.readInteger();
+            final String authName = reader.readOctetStringAsString();
+            final byte authType = reader.peekType();
+            final byte[] authBytes = reader.readOctetString(authType).toByteArray();
+            final GenericBindRequest request =
+                    Requests.newGenericBindRequest(authName, authType, authBytes);
+            readControls(request);
+            logger.trace("DECODE LDAP BIND REQUEST(messageID=%d, auth=0x%x, request=%s)",
+                messageID, request.getAuthenticationType(), request);
+
+            handler.bindRequest(messageID, protocolVersion, request);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private void readBindResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_BIND_RESPONSE);
+        final BindResult message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newBindResult(resultCode).setMatchedDN(matchedDN)
+                            .setDiagnosticMessage(diagnosticMessage);
+            readResponseReferrals(message);
+            if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_SERVER_SASL_CREDENTIALS)) {
+                message.setServerSASLCredentials(reader
+                        .readOctetString(LDAP.TYPE_SERVER_SASL_CREDENTIALS));
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP BIND RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.bindResult(messageID, message);
+    }
+
+    private void readCompareRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_COMPARE_REQUEST);
+        final CompareRequest message;
+        try {
+            final String dnString = reader.readOctetStringAsString();
+            final Schema schema = options.getSchemaResolver().resolveSchema(dnString);
+            final DN dn = LDAP.readDN(dnString, schema);
+            reader.readStartSequence();
+            try {
+                final String ads = reader.readOctetStringAsString();
+                final AttributeDescription ad = LDAP.readAttributeDescription(ads, schema);
+                final ByteString assertionValue = reader.readOctetString();
+                message = Requests.newCompareRequest(dn, ad, assertionValue);
+            } finally {
+                reader.readEndSequence();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP COMPARE REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.compareRequest(messageID, message);
+    }
+
+    private void readCompareResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_COMPARE_RESPONSE);
+        final CompareResult message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newCompareResult(resultCode).setMatchedDN(matchedDN)
+                            .setDiagnosticMessage(diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP COMPARE RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.compareResult(messageID, message);
+    }
+
+    private Control readControl() throws IOException {
+        reader.readStartSequence();
+        try {
+            final String oid = reader.readOctetStringAsString();
+            final boolean isCritical;
+            if (reader.hasNextElement() && (reader.peekType() == ASN1.UNIVERSAL_BOOLEAN_TYPE)) {
+                isCritical = reader.readBoolean();
+            } else {
+                isCritical = false;
+            }
+            final ByteString value;
+            if (reader.hasNextElement() && (reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE)) {
+                value = reader.readOctetString();
+            } else {
+                value = null;
+            }
+            return GenericControl.newControl(oid, isCritical, value);
+        } finally {
+            reader.readEndSequence();
+        }
+    }
+
+    private void readControls(final Request request) throws IOException {
+        if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_CONTROL_SEQUENCE)) {
+            reader.readStartSequence(LDAP.TYPE_CONTROL_SEQUENCE);
+            try {
+                while (reader.hasNextElement()) {
+                    request.addControl(readControl());
+                }
+            } finally {
+                reader.readEndSequence();
+            }
+        }
+    }
+
+    private void readControls(final Response response) throws IOException {
+        if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_CONTROL_SEQUENCE)) {
+            reader.readStartSequence(LDAP.TYPE_CONTROL_SEQUENCE);
+            try {
+                while (reader.hasNextElement()) {
+                    response.addControl(readControl());
+                }
+            } finally {
+                reader.readEndSequence();
+            }
+        }
+    }
+
+    private void readDeleteRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        final String dnString = reader.readOctetStringAsString(LDAP.OP_TYPE_DELETE_REQUEST);
+        final Schema schema = options.getSchemaResolver().resolveSchema(dnString);
+        final DN dn = LDAP.readDN(dnString, schema);
+        final DeleteRequest message = Requests.newDeleteRequest(dn);
+        readControls(message);
+        logger.trace("DECODE LDAP DELETE REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.deleteRequest(messageID, message);
+    }
+
+    private void readDeleteResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_DELETE_RESPONSE);
+        final Result message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(
+                            diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP DELETE RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.deleteResult(messageID, message);
+    }
+
+    private void readExtendedRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_EXTENDED_REQUEST);
+        final GenericExtendedRequest message;
+        try {
+            final String oid = reader.readOctetStringAsString(LDAP.TYPE_EXTENDED_REQUEST_OID);
+            if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_EXTENDED_REQUEST_VALUE)) {
+                final ByteString value = reader.readOctetString(LDAP.TYPE_EXTENDED_REQUEST_VALUE);
+                message = Requests.newGenericExtendedRequest(oid, value);
+            } else {
+                message = Requests.newGenericExtendedRequest(oid, null);
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP EXTENDED REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.extendedRequest(messageID, message);
+    }
+
+    private void readExtendedResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_EXTENDED_RESPONSE);
+        final GenericExtendedResult message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newGenericExtendedResult(resultCode).setMatchedDN(matchedDN)
+                            .setDiagnosticMessage(diagnosticMessage);
+            readResponseReferrals(message);
+            if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_EXTENDED_RESPONSE_OID)) {
+                message.setOID(reader.readOctetStringAsString(LDAP.TYPE_EXTENDED_RESPONSE_OID));
+            }
+            if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_EXTENDED_RESPONSE_VALUE)) {
+                message.setValue(reader.readOctetString(LDAP.TYPE_EXTENDED_RESPONSE_VALUE));
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP EXTENDED RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.extendedResult(messageID, message);
+    }
+
+    private void readIntermediateResponse(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_INTERMEDIATE_RESPONSE);
+        final GenericIntermediateResponse message;
+        try {
+            message = Responses.newGenericIntermediateResponse();
+            if (reader.hasNextElement()
+                    && (reader.peekType() == LDAP.TYPE_INTERMEDIATE_RESPONSE_OID)) {
+                message.setOID(reader.readOctetStringAsString(LDAP.TYPE_INTERMEDIATE_RESPONSE_OID));
+            }
+            if (reader.hasNextElement()
+                    && (reader.peekType() == LDAP.TYPE_INTERMEDIATE_RESPONSE_VALUE)) {
+                message.setValue(reader.readOctetString(LDAP.TYPE_INTERMEDIATE_RESPONSE_VALUE));
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP INTERMEDIATE RESPONSE(messageID=%d, response=%s)", messageID, message);
+        handler.intermediateResponse(messageID, message);
+    }
+
+    private void readModifyDNRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_MODIFY_DN_REQUEST);
+        final ModifyDNRequest message;
+        try {
+            final String dnString = reader.readOctetStringAsString();
+            final Schema schema = options.getSchemaResolver().resolveSchema(dnString);
+            final DN dn = LDAP.readDN(dnString, schema);
+            final String newRDNString = reader.readOctetStringAsString();
+            final RDN newRDN = readRDN(newRDNString, schema);
+            message = Requests.newModifyDNRequest(dn, newRDN);
+            message.setDeleteOldRDN(reader.readBoolean());
+            if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_MODIFY_DN_NEW_SUPERIOR)) {
+                final String newSuperiorString =
+                        reader.readOctetStringAsString(LDAP.TYPE_MODIFY_DN_NEW_SUPERIOR);
+                final DN newSuperior = LDAP.readDN(newSuperiorString, schema);
+                message.setNewSuperior(newSuperior);
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP MODIFY DN REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.modifyDNRequest(messageID, message);
+    }
+
+    private void readModifyDNResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_MODIFY_DN_RESPONSE);
+        final Result message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(
+                            diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP MODIFY DN RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.modifyDNResult(messageID, message);
+    }
+
+    private void readModifyRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_MODIFY_REQUEST);
+        final ModifyRequest message;
+        try {
+            final String dnString = reader.readOctetStringAsString();
+            final Schema schema = options.getSchemaResolver().resolveSchema(dnString);
+            final DN dn = LDAP.readDN(dnString, schema);
+            message = Requests.newModifyRequest(dn);
+            reader.readStartSequence();
+            try {
+                while (reader.hasNextElement()) {
+                    reader.readStartSequence();
+                    try {
+                        final int typeIntValue = reader.readEnumerated();
+                        final ModificationType type = ModificationType.valueOf(typeIntValue);
+                        if (type == null) {
+                            throw DecodeException
+                                    .error(ERR_LDAP_MODIFICATION_DECODE_INVALID_MOD_TYPE
+                                            .get(typeIntValue));
+                        }
+                        reader.readStartSequence();
+                        try {
+                            final String ads = reader.readOctetStringAsString();
+                            final AttributeDescription ad =
+                                    LDAP.readAttributeDescription(ads, schema);
+                            final Attribute attribute =
+                                    options.getAttributeFactory().newAttribute(ad);
+                            reader.readStartSet();
+                            try {
+                                while (reader.hasNextElement()) {
+                                    attribute.add(reader.readOctetString());
+                                }
+                                message.addModification(new Modification(type, attribute));
+                            } finally {
+                                reader.readEndSet();
+                            }
+                        } finally {
+                            reader.readEndSequence();
+                        }
+                    } finally {
+                        reader.readEndSequence();
+                    }
+                }
+            } finally {
+                reader.readEndSequence();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP MODIFY REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.modifyRequest(messageID, message);
+    }
+
+    private void readModifyResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_MODIFY_RESPONSE);
+        final Result message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(
+                            diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP MODIFY RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.modifyResult(messageID, message);
+    }
+
+    private void readProtocolOp(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        final byte type = reader.peekType();
+        switch (type) {
+        case LDAP.OP_TYPE_UNBIND_REQUEST: // 0x42
+            readUnbindRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_DELETE_REQUEST: // 0x4A
+            readDeleteRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_ABANDON_REQUEST: // 0x50
+            readAbandonRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_BIND_REQUEST: // 0x60
+            readBindRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_BIND_RESPONSE: // 0x61
+            readBindResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_SEARCH_REQUEST: // 0x63
+            readSearchRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_SEARCH_RESULT_ENTRY: // 0x64
+            readSearchResultEntry(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_SEARCH_RESULT_DONE: // 0x65
+            readSearchResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_MODIFY_REQUEST: // 0x66
+            readModifyRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_MODIFY_RESPONSE: // 0x67
+            readModifyResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_ADD_REQUEST: // 0x68
+            readAddRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_ADD_RESPONSE: // 0x69
+            readAddResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_DELETE_RESPONSE: // 0x6B
+            readDeleteResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_MODIFY_DN_REQUEST: // 0x6C
+            readModifyDNRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_MODIFY_DN_RESPONSE: // 0x6D
+            readModifyDNResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_COMPARE_REQUEST: // 0x6E
+            readCompareRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_COMPARE_RESPONSE: // 0x6F
+            readCompareResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_SEARCH_RESULT_REFERENCE: // 0x73
+            readSearchResultReference(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_EXTENDED_REQUEST: // 0x77
+            readExtendedRequest(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_EXTENDED_RESPONSE: // 0x78
+            readExtendedResult(messageID, handler);
+            break;
+        case LDAP.OP_TYPE_INTERMEDIATE_RESPONSE: // 0x79
+            readIntermediateResponse(messageID, handler);
+            break;
+        default:
+            handler.unrecognizedMessage(messageID, type, reader.readOctetString(type));
+            break;
+        }
+    }
+
+    private RDN readRDN(final String rdn, final Schema schema) throws DecodeException {
+        try {
+            return RDN.valueOf(rdn, schema);
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+
+    private void readResponseReferrals(final Result message) throws IOException {
+        if (reader.hasNextElement() && (reader.peekType() == LDAP.TYPE_REFERRAL_SEQUENCE)) {
+            reader.readStartSequence(LDAP.TYPE_REFERRAL_SEQUENCE);
+            try {
+                // Should have at least 1.
+                do {
+                    message.addReferralURI((reader.readOctetStringAsString()));
+                } while (reader.hasNextElement());
+            } finally {
+                reader.readEndSequence();
+            }
+        }
+    }
+
+    private void readSearchRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_SEARCH_REQUEST);
+        final SearchRequest message;
+        try {
+            final String baseDNString = reader.readOctetStringAsString();
+            final Schema schema = options.getSchemaResolver().resolveSchema(baseDNString);
+            final DN baseDN = LDAP.readDN(baseDNString, schema);
+            final int scopeIntValue = reader.readEnumerated();
+            final SearchScope scope = SearchScope.valueOf(scopeIntValue);
+            if (scope == null) {
+                throw DecodeException.error(ERR_LDAP_SEARCH_REQUEST_DECODE_INVALID_SCOPE
+                        .get(scopeIntValue));
+            }
+            final int dereferencePolicyIntValue = reader.readEnumerated();
+            final DereferenceAliasesPolicy dereferencePolicy =
+                    DereferenceAliasesPolicy.valueOf(dereferencePolicyIntValue);
+            if (dereferencePolicy == null) {
+                throw DecodeException.error(ERR_LDAP_SEARCH_REQUEST_DECODE_INVALID_DEREF
+                        .get(dereferencePolicyIntValue));
+            }
+            final int sizeLimit = (int) reader.readInteger();
+            final int timeLimit = (int) reader.readInteger();
+            final boolean typesOnly = reader.readBoolean();
+            final Filter filter = LDAP.readFilter(reader);
+            message = Requests.newSearchRequest(baseDN, scope, filter);
+            message.setDereferenceAliasesPolicy(dereferencePolicy);
+            try {
+                message.setTimeLimit(timeLimit);
+                message.setSizeLimit(sizeLimit);
+            } catch (final LocalizedIllegalArgumentException e) {
+                throw DecodeException.error(e.getMessageObject());
+            }
+            message.setTypesOnly(typesOnly);
+            reader.readStartSequence();
+            try {
+                while (reader.hasNextElement()) {
+                    message.addAttribute(reader.readOctetStringAsString());
+                }
+            } finally {
+                reader.readEndSequence();
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP SEARCH REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.searchRequest(messageID, message);
+    }
+
+    private void readSearchResult(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_SEARCH_RESULT_DONE);
+        final Result message;
+        try {
+            final ResultCode resultCode = ResultCode.valueOf(reader.readEnumerated());
+            final String matchedDN = reader.readOctetStringAsString();
+            final String diagnosticMessage = reader.readOctetStringAsString();
+            message =
+                    Responses.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(
+                            diagnosticMessage);
+            readResponseReferrals(message);
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP SEARCH RESULT(messageID=%d, result=%s)", messageID, message);
+        handler.searchResult(messageID, message);
+    }
+
+    private void readSearchResultEntry(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        final Entry entry = LDAP.readEntry(reader, LDAP.OP_TYPE_SEARCH_RESULT_ENTRY, options);
+        final SearchResultEntry message = Responses.newSearchResultEntry(entry);
+        readControls(message);
+        logger.trace("DECODE LDAP SEARCH RESULT ENTRY(messageID=%d, entry=%s)", messageID, message);
+        handler.searchResultEntry(messageID, message);
+    }
+
+    private void readSearchResultReference(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readStartSequence(LDAP.OP_TYPE_SEARCH_RESULT_REFERENCE);
+        final SearchResultReference message;
+        try {
+            message = Responses.newSearchResultReference(reader.readOctetStringAsString());
+            while (reader.hasNextElement()) {
+                message.addURI(reader.readOctetStringAsString());
+            }
+        } finally {
+            reader.readEndSequence();
+        }
+        readControls(message);
+        logger.trace("DECODE LDAP SEARCH RESULT REFERENCE(messageID=%d, reference=%s)", messageID, message);
+        handler.searchResultReference(messageID, message);
+    }
+
+    private void readUnbindRequest(final int messageID, final LDAPMessageHandler handler)
+            throws IOException {
+        reader.readNull(LDAP.OP_TYPE_UNBIND_REQUEST);
+        final UnbindRequest message = Requests.newUnbindRequest();
+        readControls(message);
+        logger.trace("DECODE LDAP UNBIND REQUEST(messageID=%d, request=%s)", messageID, message);
+        handler.unbindRequest(messageID, message);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPWriter.java
new file mode 100644
index 0000000..e74fe0c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/LDAPWriter.java
@@ -0,0 +1,689 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+/**
+ * Writes LDAP messages to an underlying ASN.1 writer.
+ * <p>
+ * Methods for creating {@link LDAPWriter}s are provided in the {@link LDAP}
+ * class.
+ *
+ * @param <W>
+ *            The type of ASN.1 writer used for encoding elements.
+ */
+public final class LDAPWriter<W extends ASN1Writer> {
+    /** @Checkstyle:ignore AvoidNestedBlocks */
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    private final W writer;
+
+    LDAPWriter(final W asn1Writer) {
+        this.writer = asn1Writer;
+    }
+
+    /**
+     * Returns the ASN.1 writer to which LDAP messages will be written.
+     *
+     * @return The ASN.1 writer to which LDAP messages will be written.
+     */
+    public W getASN1Writer() {
+        return writer;
+    }
+
+    /**
+     * Writes the provided abandon request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeAbandonRequest(final int messageID, final AbandonRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP ABANDON REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeInteger(LDAP.OP_TYPE_ABANDON_REQUEST, request.getRequestID());
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided add request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeAddRequest(final int messageID, final AddRequest request) throws IOException {
+        logger.trace("ENCODE LDAP ADD REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            LDAP.writeEntry(writer, LDAP.OP_TYPE_ADD_REQUEST, request);
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided add result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeAddResult(final int messageID, final Result result) throws IOException {
+        logger.trace("ENCODE LDAP ADD RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_ADD_RESPONSE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided bind request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param version
+     *            The requested LDAP protocol version.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeBindRequest(final int messageID, final int version,
+            final GenericBindRequest request) throws IOException {
+        logger.trace("ENCODE LDAP BIND REQUEST(messageID=%d, auth=0x%x, request=%s)",
+            messageID, request.getAuthenticationType(), request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_BIND_REQUEST);
+            {
+                writer.writeInteger(version);
+                writer.writeOctetString(request.getName());
+                writer.writeOctetString(request.getAuthenticationType(), request
+                        .getAuthenticationValue());
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided bind result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeBindResult(final int messageID, final BindResult result) throws IOException {
+        logger.trace("ENCODE LDAP BIND RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_BIND_RESPONSE, result);
+            {
+                final ByteString saslCredentials = result.getServerSASLCredentials();
+                if (saslCredentials != null && saslCredentials.length() > 0) {
+                    writer.writeOctetString(LDAP.TYPE_SERVER_SASL_CREDENTIALS, result
+                            .getServerSASLCredentials());
+                }
+            }
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided compare request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeCompareRequest(final int messageID, final CompareRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP COMPARE REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_COMPARE_REQUEST);
+            {
+                writer.writeOctetString(request.getName().toString());
+                writer.writeStartSequence();
+                {
+                    writer.writeOctetString(request.getAttributeDescription().toString());
+                    writer.writeOctetString(request.getAssertionValue());
+                }
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided compare result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeCompareResult(final int messageID, final CompareResult result)
+            throws IOException {
+        logger.trace("ENCODE LDAP COMPARE RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_COMPARE_RESPONSE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided control.
+     *
+     * @param control
+     *            The control.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeControl(final Control control) throws IOException {
+        writer.writeStartSequence();
+        {
+            writer.writeOctetString(control.getOID());
+            if (control.isCritical()) {
+                writer.writeBoolean(control.isCritical());
+            }
+            if (control.getValue() != null) {
+                writer.writeOctetString(control.getValue());
+            }
+        }
+        writer.writeEndSequence();
+    }
+
+    /**
+     * Writes the provided delete request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeDeleteRequest(final int messageID, final DeleteRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP DELETE REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeOctetString(LDAP.OP_TYPE_DELETE_REQUEST, request.getName().toString());
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided delete result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeDeleteResult(final int messageID, final Result result) throws IOException {
+        logger.trace("ENCODE LDAP DELETE RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_DELETE_RESPONSE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided extended request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeExtendedRequest(final int messageID, final ExtendedRequest<?> request)
+            throws IOException {
+        logger.trace("ENCODE LDAP EXTENDED REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_EXTENDED_REQUEST);
+            {
+                writer.writeOctetString(LDAP.TYPE_EXTENDED_REQUEST_OID, request.getOID());
+                final ByteString requestValue = request.getValue();
+                if (requestValue != null) {
+                    writer.writeOctetString(LDAP.TYPE_EXTENDED_REQUEST_VALUE, requestValue);
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided extended result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeExtendedResult(final int messageID, final ExtendedResult result)
+            throws IOException {
+        logger.trace("ENCODE LDAP EXTENDED RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_EXTENDED_RESPONSE, result);
+            {
+                final String responseName = result.getOID();
+                if (responseName != null) {
+                    writer.writeOctetString(LDAP.TYPE_EXTENDED_RESPONSE_OID, responseName);
+                }
+                final ByteString responseValue = result.getValue();
+                if (responseValue != null) {
+                    writer.writeOctetString(LDAP.TYPE_EXTENDED_RESPONSE_VALUE, responseValue);
+                }
+            }
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided intermediate response.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param response
+     *            The response.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeIntermediateResponse(final int messageID, final IntermediateResponse response)
+            throws IOException {
+        logger.trace("ENCODE LDAP INTERMEDIATE RESPONSE(messageID=%d, response=%s)", messageID, response);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_INTERMEDIATE_RESPONSE);
+            {
+                final String responseName = response.getOID();
+                if (responseName != null) {
+                    writer.writeOctetString(LDAP.TYPE_INTERMEDIATE_RESPONSE_OID, response.getOID());
+                }
+                final ByteString responseValue = response.getValue();
+                if (responseValue != null) {
+                    writer.writeOctetString(LDAP.TYPE_INTERMEDIATE_RESPONSE_VALUE, response
+                            .getValue());
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(response.getControls());
+    }
+
+    /**
+     * Writes the provided modify DN request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeModifyDNRequest(final int messageID, final ModifyDNRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP MODIFY DN REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_MODIFY_DN_REQUEST);
+            {
+                writer.writeOctetString(request.getName().toString());
+                writer.writeOctetString(request.getNewRDN().toString());
+                writer.writeBoolean(request.isDeleteOldRDN());
+                final DN newSuperior = request.getNewSuperior();
+                if (newSuperior != null) {
+                    writer.writeOctetString(LDAP.TYPE_MODIFY_DN_NEW_SUPERIOR, newSuperior
+                            .toString());
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided modify DN result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeModifyDNResult(final int messageID, final Result result) throws IOException {
+        logger.trace("ENCODE LDAP MODIFY DN RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_MODIFY_DN_RESPONSE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided modify request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeModifyRequest(final int messageID, final ModifyRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP MODIFY REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_MODIFY_REQUEST);
+            {
+                writer.writeOctetString(request.getName().toString());
+                writer.writeStartSequence();
+                {
+                    for (final Modification change : request.getModifications()) {
+                        writeChange(change);
+                    }
+                }
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided extended result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeModifyResult(final int messageID, final Result result) throws IOException {
+        logger.trace("ENCODE LDAP MODIFY RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_MODIFY_RESPONSE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided search request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeSearchRequest(final int messageID, final SearchRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP SEARCH REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_SEARCH_REQUEST);
+            {
+                writer.writeOctetString(request.getName().toString());
+                writer.writeEnumerated(request.getScope().intValue());
+                writer.writeEnumerated(request.getDereferenceAliasesPolicy().intValue());
+                writer.writeInteger(request.getSizeLimit());
+                writer.writeInteger(request.getTimeLimit());
+                writer.writeBoolean(request.isTypesOnly());
+                LDAP.writeFilter(writer, request.getFilter());
+                writer.writeStartSequence();
+                {
+                    for (final String attribute : request.getAttributes()) {
+                        writer.writeOctetString(attribute);
+                    }
+                }
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes the provided search result.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param result
+     *            The result.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeSearchResult(final int messageID, final Result result) throws IOException {
+        logger.trace("ENCODE LDAP SEARCH RESULT(messageID=%d, result=%s)", messageID, result);
+        writeMessageHeader(messageID);
+        {
+            writeResultHeader(LDAP.OP_TYPE_SEARCH_RESULT_DONE, result);
+            writeResultFooter(writer);
+        }
+        writeMessageFooter(result.getControls());
+    }
+
+    /**
+     * Writes the provided search result entry.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param entry
+     *            The entry.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeSearchResultEntry(final int messageID, final SearchResultEntry entry)
+            throws IOException {
+        logger.trace("ENCODE LDAP SEARCH RESULT ENTRY(messageID=%d, entry=%s)", messageID, entry);
+        writeMessageHeader(messageID);
+        {
+            LDAP.writeEntry(writer, LDAP.OP_TYPE_SEARCH_RESULT_ENTRY, entry);
+        }
+        writeMessageFooter(entry.getControls());
+    }
+
+    /**
+     * Writes the provided search result reference.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param reference
+     *            The reference.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeSearchResultReference(final int messageID,
+            final SearchResultReference reference) throws IOException {
+        logger.trace("ENCODE LDAP SEARCH RESULT REFERENCE(messageID=%d, reference=%s)", messageID, reference);
+        writeMessageHeader(messageID);
+        {
+            writer.writeStartSequence(LDAP.OP_TYPE_SEARCH_RESULT_REFERENCE);
+            {
+                for (final String url : reference.getURIs()) {
+                    writer.writeOctetString(url);
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writeMessageFooter(reference.getControls());
+    }
+
+    /**
+     * Writes the provided unbind request.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param request
+     *            The request.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeUnbindRequest(final int messageID, final UnbindRequest request)
+            throws IOException {
+        logger.trace("ENCODE LDAP UNBIND REQUEST(messageID=%d, request=%s)", messageID, request);
+        writeMessageHeader(messageID);
+        {
+            writer.writeNull(LDAP.OP_TYPE_UNBIND_REQUEST);
+        }
+        writeMessageFooter(request.getControls());
+    }
+
+    /**
+     * Writes a message with the provided id, tag and content bytes.
+     *
+     * @param messageID
+     *            The LDAP message ID.
+     * @param messageTag
+     *            The LDAP message type.
+     * @param messageBytes
+     *            The contents of the LDAP message.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public void writeUnrecognizedMessage(final int messageID, final byte messageTag,
+            final ByteString messageBytes) throws IOException {
+        logger.trace("ENCODE LDAP UNKNOWN MESSAGE(messageID=%d, messageTag=%x, messageBytes=%s)",
+                messageID, messageTag, messageBytes);
+        writeMessageHeader(messageID);
+        {
+            writer.writeOctetString(messageTag, messageBytes);
+        }
+        writer.writeEndSequence();
+    }
+
+    private void writeChange(final Modification change) throws IOException {
+        writer.writeStartSequence();
+        {
+            writer.writeEnumerated(change.getModificationType().intValue());
+            LDAP.writeAttribute(writer, change.getAttribute());
+        }
+        writer.writeEndSequence();
+    }
+
+    private void writeMessageFooter(final List<Control> controls) throws IOException {
+        if (!controls.isEmpty()) {
+            writer.writeStartSequence(LDAP.TYPE_CONTROL_SEQUENCE);
+            {
+                for (final Control control : controls) {
+                    writeControl(control);
+                }
+            }
+            writer.writeEndSequence();
+        }
+        writer.writeEndSequence();
+    }
+
+    private void writeMessageHeader(final int messageID) throws IOException {
+        writer.writeStartSequence();
+        writer.writeInteger(messageID);
+    }
+
+    private void writeResultFooter(final ASN1Writer writer) throws IOException {
+        writer.writeEndSequence();
+    }
+
+    private void writeResultHeader(final byte typeTag, final Result rawMessage) throws IOException {
+        writer.writeStartSequence(typeTag);
+        writer.writeEnumerated(rawMessage.getResultCode().intValue());
+        writer.writeOctetString(rawMessage.getMatchedDN());
+        writer.writeOctetString(rawMessage.getDiagnosticMessage());
+        final List<String> referralURIs = rawMessage.getReferralURIs();
+        if (!referralURIs.isEmpty()) {
+            writer.writeStartSequence(LDAP.TYPE_REFERRAL_SEQUENCE);
+            {
+                for (final String s : referralURIs) {
+                    writer.writeOctetString(s);
+                }
+            }
+            writer.writeEndSequence();
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/package-info.java
new file mode 100644
index 0000000..4a66ff4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/io/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+/**
+ * Classes and interfaces providing I/O functionality.
+ * <p>
+ * It includes facilities for encoding and decoding ASN.1 data streams, as
+ * well as LDAP protocol messages.
+ * <p>
+ * Note that this particular implementation is limited to the subset of elements
+ * that are typically used by LDAP clients. As such, it does not include all
+ * ASN.1 element types, particularly elements like OIDs, bit strings, and
+ * timestamp values.
+ */
+package org.forgerock.opendj.io;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
new file mode 100644
index 0000000..935053a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
@@ -0,0 +1,744 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import static org.forgerock.util.Reject.*;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+
+import com.forgerock.opendj.util.StaticUtils;
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * An attribute value assertion (AVA) as defined in RFC 4512 section 2.3
+ * consists of an attribute description with zero options and an attribute
+ * value.
+ * <p>
+ * The following are examples of string representations of AVAs:
+ *
+ * <pre>
+ * uid=12345
+ * ou=Engineering
+ * cn=Kurt Zeilenga
+ * </pre>
+ *
+ * Note: The name <em>AVA</em> is historical, coming from X500/LDAPv2.
+ * However, in LDAP context, this class actually represents an
+ * <code>AttributeTypeAndValue</code>.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
+ *      Lightweight Directory Access Protocol (LDAP): Directory Information
+ *      Models </a>
+ */
+public final class AVA implements Comparable<AVA> {
+
+    /**
+     * Parses the provided LDAP string representation of an AVA using the
+     * default schema.
+     *
+     * @param ava
+     *            The LDAP string representation of an AVA.
+     * @return The parsed RDN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ava} is not a valid LDAP string representation of a
+     *             AVA.
+     * @throws NullPointerException
+     *             If {@code ava} was {@code null}.
+     */
+    public static AVA valueOf(final String ava) {
+        return valueOf(ava, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an AVA using the
+     * provided schema.
+     *
+     * @param ava
+     *            The LDAP string representation of a AVA.
+     * @param schema
+     *            The schema to use when parsing the AVA.
+     * @return The parsed AVA.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ava} is not a valid LDAP string representation of a
+     *             AVA.
+     * @throws NullPointerException
+     *             If {@code ava} or {@code schema} was {@code null}.
+     */
+    public static AVA valueOf(final String ava, final Schema schema) {
+        final SubstringReader reader = new SubstringReader(ava);
+        try {
+            return decode(reader, schema);
+        } catch (final UnknownSchemaElementException e) {
+            final LocalizableMessage message =
+                    ERR_RDN_TYPE_NOT_FOUND.get(ava, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    static AVA decode(final SubstringReader reader, final Schema schema) {
+        // Skip over any spaces at the beginning.
+        reader.skipWhitespaces();
+
+        if (reader.remaining() == 0) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        final String nameOrOid = readAttributeName(reader);
+        final AttributeType attribute = schema.getAttributeType(nameOrOid);
+
+        // Skip over any spaces if we have.
+        reader.skipWhitespaces();
+
+        // Make sure that we're not at the end of the DN string because
+        // that would be invalid.
+        if (reader.remaining() == 0) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(reader.getString(), attribute
+                            .getNameOrOID());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // The next character must be an equal sign. If it is not, then
+        // that's an error.
+        final char c = reader.read();
+        if (c != '=') {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_DN_NO_EQUAL
+                            .get(reader.getString(), attribute.getNameOrOID(), c);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Skip over any spaces after the equal sign.
+        reader.skipWhitespaces();
+
+        // Parse the value for this RDN component.
+        final ByteString value = readAttributeValue(reader);
+        return new AVA(attribute, nameOrOid, value);
+    }
+
+    static void escapeAttributeValue(final String str, final StringBuilder builder) {
+        if (str.length() > 0) {
+            char c = str.charAt(0);
+            int startPos = 0;
+            if (c == ' ' || c == '#') {
+                builder.append('\\');
+                builder.append(c);
+                startPos = 1;
+            }
+            final int length = str.length();
+            for (int si = startPos; si < length; si++) {
+                c = str.charAt(si);
+                if (c < ' ') {
+                    for (final byte b : getBytes(String.valueOf(c))) {
+                        builder.append('\\');
+                        builder.append(StaticUtils.byteToLowerHex(b));
+                    }
+                } else {
+                    if ((c == ' ' && si == length - 1)
+                            || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
+                            || c == '>' || c == '\\' || c == '\u0000')) {
+                        builder.append('\\');
+                    }
+                    builder.append(c);
+                }
+            }
+        }
+    }
+
+    private static String readAttributeName(final SubstringReader reader) {
+        int length = 1;
+        reader.mark();
+
+        // The next character must be either numeric (for an OID) or
+        // alphabetic (for an attribute description).
+        char c = reader.read();
+        if (isDigit(c)) {
+            boolean lastWasPeriod = false;
+            while (reader.remaining() > 0) {
+                c = reader.read();
+                if (c == '=' || c == ' ') {
+                    // This signals the end of the OID.
+                    break;
+                } else if (c == '.') {
+                    if (lastWasPeriod) {
+                        throw illegalCharacter(reader, c);
+                    }
+                    lastWasPeriod = true;
+                } else if (!isDigit(c)) {
+                    throw illegalCharacter(reader, c);
+                } else {
+                    lastWasPeriod = false;
+                }
+                length++;
+            }
+            if (lastWasPeriod) {
+                throw illegalCharacter(reader, '.');
+            }
+        } else if (isAlpha(c)) {
+            // This must be an attribute description. In this case, we will
+            // only accept alphabetic characters, numeric digits, and the
+            // hyphen.
+            while (reader.remaining() > 0) {
+                c = reader.read();
+                if (c == '=' || c == ' ') {
+                    // This signals the end of the OID.
+                    break;
+                } else if (!isAlpha(c) && !isDigit(c) && c != '-') {
+                    throw illegalCharacter(reader, c);
+                }
+                length++;
+            }
+        } else {
+            throw illegalCharacter(reader, c);
+        }
+        // Return the position of the first non-space character after the token
+        reader.reset();
+        return reader.read(length);
+    }
+
+    private static LocalizedIllegalArgumentException illegalCharacter(
+            final SubstringReader reader, final char c) {
+        return new LocalizedIllegalArgumentException(
+                ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(reader.getString(), c, reader.pos() - 1));
+    }
+
+    private static ByteString readAttributeValue(final SubstringReader reader) {
+        // All leading spaces have already been stripped so we can start
+        // reading the value. However, it may be empty so check for that.
+        if (reader.remaining() == 0) {
+            return ByteString.empty();
+        }
+
+        // Decide how to parse based on the first character.
+        reader.mark();
+        char c = reader.read();
+        if (c == '+') {
+            // Value is empty and followed by another AVA.
+            reader.reset();
+            return ByteString.empty();
+        } else if (c == '#') {
+            // Value is HEX encoded BER.
+            return readAttributeValueAsBER(reader);
+        } else if (c == '"') {
+            // Legacy support for RFC 2253. The value should continue until the
+            // corresponding closing quotation mark and has the same format as
+            // RFC 4514 attribute values, except that special characters,
+            // excluding double quote and back-slash, do not need escaping.
+            reader.mark();
+            return readAttributeValue(reader, true);
+        } else {
+            // Otherwise, use general parsing to find the end of the value.
+            return readAttributeValue(reader, false);
+        }
+    }
+
+    private static ByteString readAttributeValueAsBER(final SubstringReader reader) {
+        // The first two characters must be hex characters.
+        reader.mark();
+        if (reader.remaining() < 2) {
+            throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(reader.getString()));
+        }
+
+        int length = 0;
+        for (int i = 0; i < 2; i++) {
+            final char c = reader.read();
+            if (isHexDigit(c)) {
+                length++;
+            } else {
+                throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
+            }
+        }
+
+        // The rest of the value must be a multiple of two hex
+        // characters. The end of the value may be designated by the
+        // end of the DN, a comma or semicolon, or a space.
+        while (reader.remaining() > 0) {
+            char c = reader.read();
+            if (isHexDigit(c)) {
+                length++;
+
+                if (reader.remaining() > 0) {
+                    c = reader.read();
+                    if (isHexDigit(c)) {
+                        length++;
+                    } else {
+                        throw new LocalizedIllegalArgumentException(
+                                ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
+                    }
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                            ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(reader.getString()));
+                }
+            } else if (c == ' ' || c == ',' || c == ';') {
+                // This denotes the end of the value.
+                break;
+            } else {
+                throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(reader.getString(), c));
+            }
+        }
+
+        // At this point, we should have a valid hex string. Convert it
+        // to a byte array and set that as the value of the provided
+        // octet string.
+        try {
+            reader.reset();
+            return ByteString.valueOfHex(reader.read(length));
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw new LocalizedIllegalArgumentException(
+                    ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(reader.getString(), e.getMessageObject()));
+        }
+    }
+
+    private static ByteString readAttributeValue(final SubstringReader reader, final boolean isQuoted) {
+        reader.reset();
+        final ByteString bytes = delimitAndEvaluateEscape(reader, isQuoted);
+        if (bytes.length() == 0) {
+            // We don't allow an empty attribute value.
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR.get(reader.getString(), reader.pos());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+        return bytes;
+    }
+
+    private static ByteString delimitAndEvaluateEscape(final SubstringReader reader, final boolean isQuoted) {
+        final StringBuilder valueBuffer = new StringBuilder();
+        StringBuilder hexBuffer = null;
+        boolean escaped = false;
+        int trailingSpaces = 0;
+        while (reader.remaining() > 0) {
+            final char c = reader.read();
+            if (escaped) {
+                // This character is escaped.
+                if (isHexDigit(c)) {
+                    // Unicode characters.
+                    if (reader.remaining() <= 0) {
+                        throw new LocalizedIllegalArgumentException(
+                                ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(reader.getString()));
+                    }
+
+                    // Check the next byte for hex.
+                    final char c2 = reader.read();
+                    if (isHexDigit(c2)) {
+                        if (hexBuffer == null) {
+                            hexBuffer = new StringBuilder();
+                        }
+                        hexBuffer.append(c);
+                        hexBuffer.append(c2);
+                        // We may be at the end.
+                        if (reader.remaining() == 0) {
+                            appendHexChars(reader, valueBuffer, hexBuffer);
+                        }
+                    } else {
+                        throw new LocalizedIllegalArgumentException(
+                                ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(reader.getString()));
+                    }
+                } else {
+                    appendHexChars(reader, valueBuffer, hexBuffer);
+                    valueBuffer.append(c);
+                }
+                escaped = false;
+            } else if (c == '\\') {
+                escaped = true;
+                trailingSpaces = 0;
+            } else if (isQuoted && c == '"') {
+                appendHexChars(reader, valueBuffer, hexBuffer);
+                reader.skipWhitespaces();
+                return ByteString.valueOfUtf8(valueBuffer);
+            } else if (!isQuoted && (c == '+' || c == ',' || c == ';')) {
+                reader.reset();
+                appendHexChars(reader, valueBuffer, hexBuffer);
+                valueBuffer.setLength(valueBuffer.length() - trailingSpaces);
+                return ByteString.valueOfUtf8(valueBuffer);
+            } else {
+                // It is definitely not a delimiter at this point.
+                appendHexChars(reader, valueBuffer, hexBuffer);
+                valueBuffer.append(c);
+                trailingSpaces = c != ' ' ? 0 : trailingSpaces + 1;
+            }
+            reader.mark();
+        }
+        if (isQuoted) {
+            // We hit the end of the AVA before the closing quote. That's an error.
+            throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE.get(reader.getString()));
+        }
+        reader.reset();
+        valueBuffer.setLength(valueBuffer.length() - trailingSpaces);
+        return ByteString.valueOfUtf8(valueBuffer);
+    }
+
+    private static void appendHexChars(final SubstringReader reader,
+                                       final StringBuilder valueBuffer,
+                                       final StringBuilder hexBuffer) {
+        if (hexBuffer == null) {
+            return;
+        }
+        final ByteString bytes = ByteString.valueOfHex(hexBuffer.toString());
+        try {
+            valueBuffer.append(new String(bytes.toByteArray(), "UTF-8"));
+        } catch (final Exception e) {
+            throw new LocalizedIllegalArgumentException(
+                    ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(reader.getString(), String.valueOf(e)));
+        }
+        // Clean up the hex buffer.
+        hexBuffer.setLength(0);
+    }
+
+    private final AttributeType attributeType;
+    private final String attributeName;
+    private final ByteString attributeValue;
+
+    /** Cached normalized value using equality matching rule. */
+    private ByteString equalityNormalizedAttributeValue;
+    /** Cached normalized value using ordering matching rule. */
+    private ByteString orderingNormalizedAttributeValue;
+
+    /**
+     * Creates a new attribute value assertion (AVA) using the provided
+     * attribute type and value.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeValue
+     *            The attribute value.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code attributeValue} was
+     *             {@code null}.
+     */
+    public AVA(final AttributeType attributeType, final Object attributeValue) {
+        this(attributeType, null, attributeValue);
+    }
+
+    /**
+     * Creates a new attribute value assertion (AVA) using the provided
+     * attribute type, name and value.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeName
+     *            The user provided attribute name.
+     * @param attributeValue
+     *            The attribute value.
+     * @throws NullPointerException
+     *             If {@code attributeType}, {@code attributeName} or {@code attributeValue} was {@code null}.
+     */
+    public AVA(final AttributeType attributeType, final String attributeName, final Object attributeValue) {
+        this.attributeType = checkNotNull(attributeType);
+        this.attributeName = computeAttributeName(attributeName, attributeType);
+        this.attributeValue = ByteString.valueOfObject(checkNotNull(attributeValue));
+    }
+
+    /**
+     * Creates a new attribute value assertion (AVA) using the provided
+     * attribute type and value decoded using the default schema.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeValue
+     *            The attribute value.
+     * @throws UnknownSchemaElementException
+     *             If {@code attributeType} was not found in the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code attributeValue} was
+     *             {@code null}.
+     */
+    public AVA(final String attributeType, final Object attributeValue) {
+        this.attributeName = checkNotNull(attributeType);
+        this.attributeType = Schema.getDefaultSchema().getAttributeType(attributeType);
+        this.attributeValue = ByteString.valueOfObject(checkNotNull(attributeValue));
+    }
+
+    private String computeAttributeName(final String attributeName, final AttributeType attributeType) {
+        return attributeName != null ? attributeName : attributeType.getNameOrOID();
+    }
+
+    @Override
+    public int compareTo(final AVA ava) {
+        final int result = attributeType.compareTo(ava.attributeType);
+        if (result != 0) {
+            return result > 0 ? 1 : -1;
+        }
+
+        final ByteString normalizedValue = getOrderingNormalizedValue();
+        final ByteString otherNormalizedValue = ava.getOrderingNormalizedValue();
+        return normalizedValue.compareTo(otherNormalizedValue);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof AVA) {
+            final AVA ava = (AVA) obj;
+
+            if (!attributeType.equals(ava.attributeType)) {
+                return false;
+            }
+
+            final ByteString normalizedValue = getEqualityNormalizedValue();
+            final ByteString otherNormalizedValue = ava.getEqualityNormalizedValue();
+            return normalizedValue.equals(otherNormalizedValue);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the attribute type associated with this AVA.
+     *
+     * @return The attribute type associated with this AVA.
+     */
+    public AttributeType getAttributeType() {
+        return attributeType;
+    }
+
+    /**
+     * Returns the attribute name associated with this AVA.
+     *
+     * @return The attribute name associated with this AVA.
+     */
+    public String getAttributeName() {
+        return attributeName;
+    }
+
+    /**
+     * Returns the attribute value associated with this AVA.
+     *
+     * @return The attribute value associated with this AVA.
+     */
+    public ByteString getAttributeValue() {
+        return attributeValue;
+    }
+
+    @Override
+    public int hashCode() {
+        return attributeType.hashCode() * 31 + getEqualityNormalizedValue().hashCode();
+    }
+
+    /**
+     * Returns a single valued attribute having the same attribute type and
+     * value as this AVA.
+     *
+     * @return A single valued attribute having the same attribute type and
+     *         value as this AVA.
+     */
+    public Attribute toAttribute() {
+        AttributeDescription ad = AttributeDescription.create(attributeType);
+        return new LinkedAttribute(ad, attributeValue);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        return toString(builder).toString();
+    }
+
+    StringBuilder toString(final StringBuilder builder) {
+        if (attributeName.equals(attributeType.getOID())) {
+            builder.append(attributeType.getOID());
+            builder.append("=#");
+            builder.append(attributeValue.toHexString());
+        } else {
+            builder.append(attributeName);
+            builder.append("=");
+
+            if (!attributeType.getSyntax().isHumanReadable()) {
+                builder.append("#");
+                builder.append(attributeValue.toHexString());
+            } else {
+                escapeAttributeValue(attributeValue.toString(), builder);
+            }
+        }
+        return builder;
+    }
+
+    private ByteString getEqualityNormalizedValue() {
+        final ByteString normalizedValue = equalityNormalizedAttributeValue;
+
+        if (normalizedValue != null) {
+            return normalizedValue;
+        }
+
+        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
+        if (matchingRule != null) {
+            try {
+                equalityNormalizedAttributeValue =
+                        matchingRule.normalizeAttributeValue(attributeValue);
+            } catch (final DecodeException de) {
+                // Unable to normalize, so default to byte-wise comparison.
+                equalityNormalizedAttributeValue = attributeValue;
+            }
+        } else {
+            // No matching rule, so default to byte-wise comparison.
+            equalityNormalizedAttributeValue = attributeValue;
+        }
+
+        return equalityNormalizedAttributeValue;
+    }
+
+    private ByteString getOrderingNormalizedValue() {
+        final ByteString normalizedValue = orderingNormalizedAttributeValue;
+
+        if (normalizedValue != null) {
+            return normalizedValue;
+        }
+
+        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
+        if (matchingRule != null) {
+            try {
+                orderingNormalizedAttributeValue =
+                        matchingRule.normalizeAttributeValue(attributeValue);
+            } catch (final DecodeException de) {
+                // Unable to normalize, so default to equality matching.
+                orderingNormalizedAttributeValue = getEqualityNormalizedValue();
+            }
+        } else {
+            // No matching rule, so default to equality matching.
+            orderingNormalizedAttributeValue = getEqualityNormalizedValue();
+        }
+
+        return orderingNormalizedAttributeValue;
+    }
+
+    /**
+     * Returns the normalized byte string representation of this AVA.
+     * <p>
+     * The representation is not a valid AVA.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized byte string.
+     * @return The normalized byte string representation.
+     * @see DN#toNormalizedByteString()
+     */
+    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
+        builder.appendUtf8(toLowerCase(attributeType.getNameOrOID()));
+        builder.appendUtf8("=");
+        final ByteString value = getEqualityNormalizedValue();
+        if (value.length() > 0) {
+            builder.appendBytes(escapeBytes(value));
+        }
+        return builder;
+    }
+
+    /**
+     * Returns the normalized readable string representation of this AVA.
+     * <p>
+     * The representation is not a valid AVA.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized string.
+     * @return The normalized readable string representation.
+     * @see DN#toNormalizedUrlSafeString()
+     */
+    StringBuilder toNormalizedUrlSafe(final StringBuilder builder) {
+        builder.append(toLowerCase(attributeType.getNameOrOID()));
+        builder.append('=');
+        final ByteString value = getEqualityNormalizedValue();
+
+        if (value.length() == 0) {
+            return builder;
+        }
+        final boolean hasAttributeName = !attributeType.getNames().isEmpty();
+        final boolean isHumanReadable = attributeType.getSyntax().isHumanReadable();
+        if (!hasAttributeName || !isHumanReadable) {
+            builder.append(value.toPercentHexString());
+        } else {
+            // try to decode value as UTF-8 string
+            final CharBuffer buffer = CharBuffer.allocate(value.length());
+            final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder()
+                .onMalformedInput(CodingErrorAction.REPORT)
+                .onUnmappableCharacter(CodingErrorAction.REPORT);
+            if (value.copyTo(buffer, decoder)) {
+                buffer.flip();
+                try {
+                    // URL encoding encodes space char as '+' instead of using hex code
+                    final String val = URLEncoder.encode(buffer.toString(), "UTF-8").replaceAll("\\+", "%20");
+                    builder.append(val);
+                } catch (UnsupportedEncodingException e) {
+                    // should never happen
+                    builder.append(value.toPercentHexString());
+                }
+            } else {
+                builder.append(value.toPercentHexString());
+            }
+        }
+        return builder;
+    }
+
+    /**
+     * Return a new byte string with bytes 0x00, 0x01 and 0x02 escaped.
+     * <p>
+     * These bytes are reserved to represent respectively the RDN separator,
+     * the AVA separator and the escape byte in a normalized byte string.
+     */
+    private ByteString escapeBytes(final ByteString value) {
+        if (!needEscaping(value)) {
+            return value;
+        }
+
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        for (int i = 0; i < value.length(); i++) {
+            final byte b = value.byteAt(i);
+            if (isByteToEscape(b)) {
+                builder.appendByte(DN.NORMALIZED_ESC_BYTE);
+            }
+            builder.appendByte(b);
+        }
+        return builder.toByteString();
+    }
+
+    private boolean needEscaping(final ByteString value) {
+        for (int i = 0; i < value.length(); i++) {
+            if (isByteToEscape(value.byteAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isByteToEscape(final byte b) {
+        return b == DN.NORMALIZED_RDN_SEPARATOR || b == DN.NORMALIZED_AVA_SEPARATOR || b == DN.NORMALIZED_ESC_BYTE;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnection.java
new file mode 100644
index 0000000..a29ba85
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnection.java
@@ -0,0 +1,97 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+
+/**
+ * An abstract connection whose synchronous methods are implemented in terms of
+ * asynchronous methods.
+ */
+public abstract class AbstractAsynchronousConnection extends AbstractConnection {
+    /** Creates a new abstract asynchronous connection. */
+    protected AbstractAsynchronousConnection() {
+        // No implementation required.
+    }
+
+    @Override
+    public Result add(final AddRequest request) throws LdapException {
+        return blockingGetOrThrow(addAsync(request));
+    }
+
+    @Override
+    public BindResult bind(final BindRequest request) throws LdapException {
+        return blockingGetOrThrow(bindAsync(request));
+    }
+
+    @Override
+    public CompareResult compare(final CompareRequest request) throws LdapException {
+        return blockingGetOrThrow(compareAsync(request));
+    }
+
+    @Override
+    public Result delete(final DeleteRequest request) throws LdapException {
+        return blockingGetOrThrow(deleteAsync(request));
+    }
+
+    @Override
+    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
+            final IntermediateResponseHandler handler) throws LdapException {
+        return blockingGetOrThrow(extendedRequestAsync(request, handler));
+    }
+
+    @Override
+    public Result modify(final ModifyRequest request) throws LdapException {
+        return blockingGetOrThrow(modifyAsync(request));
+    }
+
+    @Override
+    public Result modifyDN(final ModifyDNRequest request) throws LdapException {
+        return blockingGetOrThrow(modifyDNAsync(request));
+    }
+
+    @Override
+    public Result search(final SearchRequest request, final SearchResultHandler handler) throws LdapException {
+        return blockingGetOrThrow(searchAsync(request, handler));
+    }
+
+    private <T extends Result> T blockingGetOrThrow(LdapPromise<T> promise) throws LdapException {
+        try {
+            return promise.getOrThrow();
+        } catch (InterruptedException e) {
+            throw interrupted(e);
+        }
+    }
+
+    /** Handle thread interruption. */
+    private LdapException interrupted(InterruptedException e) {
+        return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java
new file mode 100644
index 0000000..3c43771
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java
@@ -0,0 +1,300 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+
+import org.forgerock.util.Reject;
+
+/**
+ * This class provides a skeletal implementation of the {@code Attribute}
+ * interface, to minimize the effort required to implement this interface.
+ */
+public abstract class AbstractAttribute extends AbstractSet<ByteString> implements Attribute {
+
+    /**
+     * Returns {@code true} if {@code object} is an attribute which is equal to
+     * {@code attribute}. Two attributes are considered equal if their attribute
+     * descriptions are equal, they both have the same number of attribute
+     * values, and every attribute value contained in the first attribute is
+     * also contained in the second attribute.
+     *
+     * @param attribute
+     *            The attribute to be tested for equality.
+     * @param object
+     *            The object to be tested for equality with the attribute.
+     * @return {@code true} if {@code object} is an attribute which is equal to
+     *         {@code attribute}, or {@code false} if not.
+     */
+    static boolean equals(final Attribute attribute, final Object object) {
+        if (attribute == object) {
+            return true;
+        }
+        if (!(object instanceof Attribute)) {
+            return false;
+        }
+
+        final Attribute other = (Attribute) object;
+        return attribute.getAttributeDescription().equals(other.getAttributeDescription())
+                && attribute.size() == other.size()
+                && attribute.containsAll(other);
+    }
+
+    /**
+     * Returns the hash code for {@code attribute}. It will be calculated as the
+     * sum of the hash codes of the attribute description and all of the
+     * attribute values.
+     *
+     * @param attribute
+     *            The attribute whose hash code should be calculated.
+     * @return The hash code for {@code attribute}.
+     */
+    static int hashCode(final Attribute attribute) {
+        int hashCode = attribute.getAttributeDescription().hashCode();
+        for (final ByteString value : attribute) {
+            hashCode += normalizeValue(attribute, value).hashCode();
+        }
+        return hashCode;
+    }
+
+    /**
+     * Returns the normalized form of {@code value} normalized using
+     * {@code attribute}'s equality matching rule.
+     *
+     * @param attribute
+     *            The attribute whose equality matching rule should be used for
+     *            normalization.
+     * @param value
+     *            The attribute value to be normalized.
+     * @return The normalized form of {@code value} normalized using
+     *         {@code attribute}'s equality matching rule.
+     */
+    static ByteString normalizeValue(final Attribute attribute, final ByteString value) {
+        final AttributeDescription attributeDescription = attribute.getAttributeDescription();
+        final AttributeType attributeType = attributeDescription.getAttributeType();
+        final MatchingRule matchingRule = attributeType.getEqualityMatchingRule();
+
+        try {
+            return matchingRule.normalizeAttributeValue(value);
+        } catch (final Exception e) {
+            // Fall back to provided value.
+            return value;
+        }
+    }
+
+    /**
+     * Returns a string representation of {@code attribute}.
+     *
+     * @param attribute
+     *            The attribute whose string representation should be returned.
+     * @return The string representation of {@code attribute}.
+     */
+    static String toString(final Attribute attribute) {
+        final StringBuilder builder = new StringBuilder();
+        builder.append('"');
+        builder.append(attribute.getAttributeDescriptionAsString());
+        builder.append("\":[");
+        boolean firstValue = true;
+        for (final ByteString value : attribute) {
+            if (!firstValue) {
+                builder.append(',');
+            }
+            builder.append('"');
+            builder.append(value);
+            builder.append('"');
+            firstValue = false;
+        }
+        builder.append(']');
+        return builder.toString();
+    }
+
+    /** Sole constructor. */
+    protected AbstractAttribute() {
+        // No implementation required.
+    }
+
+    @Override
+    public abstract boolean add(ByteString value);
+
+    @Override
+    public boolean add(final Object... values) {
+        Reject.ifNull(values);
+        boolean modified = false;
+        for (final Object value : values) {
+            modified |= add(ByteString.valueOfObject(value));
+        }
+        return modified;
+    }
+
+    @Override
+    public boolean addAll(final Collection<? extends ByteString> values) {
+        return addAll(values, null);
+    }
+
+    @Override
+    public <T> boolean addAll(final Collection<T> values,
+            final Collection<? super T> duplicateValues) {
+        boolean modified = false;
+        for (final T value : values) {
+            if (add(value)) {
+                modified = true;
+            } else if (duplicateValues != null) {
+                duplicateValues.add(value);
+            }
+        }
+        return modified;
+    }
+
+    @Override
+    public abstract boolean contains(Object value);
+
+    @Override
+    public boolean containsAll(final Collection<?> values) {
+        for (final Object value : values) {
+            if (!contains(value)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(final Object object) {
+        return equals(this, object);
+    }
+
+    @Override
+    public ByteString firstValue() {
+        return iterator().next();
+    }
+
+    @Override
+    public String firstValueAsString() {
+        return firstValue().toString();
+    }
+
+    @Override
+    public abstract AttributeDescription getAttributeDescription();
+
+    @Override
+    public String getAttributeDescriptionAsString() {
+        return getAttributeDescription().toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode(this);
+    }
+
+    @Override
+    public AttributeParser parse() {
+        return AttributeParser.parseAttribute(this);
+    }
+
+    @Override
+    public abstract Iterator<ByteString> iterator();
+
+    @Override
+    public abstract boolean remove(Object value);
+
+    @Override
+    public boolean removeAll(final Collection<?> values) {
+        return removeAll(values, null);
+    }
+
+    @Override
+    public <T> boolean removeAll(final Collection<T> values,
+            final Collection<? super T> missingValues) {
+        boolean modified = false;
+        for (final T value : values) {
+            if (remove(value)) {
+                modified = true;
+            } else if (missingValues != null) {
+                missingValues.add(value);
+            }
+        }
+        return modified;
+    }
+
+    @Override
+    public boolean retainAll(final Collection<?> values) {
+        return retainAll(values, null);
+    }
+
+    @Override
+    public <T> boolean retainAll(final Collection<T> values,
+            final Collection<? super T> missingValues) {
+        if (values.isEmpty()) {
+            if (isEmpty()) {
+                return false;
+            } else {
+                clear();
+                return true;
+            }
+        }
+
+        if (isEmpty()) {
+            if (missingValues != null) {
+                missingValues.addAll(values);
+            }
+            return false;
+        }
+
+        final Map<ByteString, T> valuesToRetain = new HashMap<>(values.size());
+        for (final T value : values) {
+            valuesToRetain.put(normalizeValue(this, ByteString.valueOfObject(value)), value);
+        }
+
+        boolean modified = false;
+        final Iterator<ByteString> iterator = iterator();
+        while (iterator.hasNext()) {
+            final ByteString value = iterator.next();
+            final ByteString normalizedValue = normalizeValue(this, value);
+            if (valuesToRetain.remove(normalizedValue) == null) {
+                modified = true;
+                iterator.remove();
+            }
+        }
+
+        if (missingValues != null) {
+            missingValues.addAll(valuesToRetain.values());
+        }
+
+        return modified;
+    }
+
+    @Override
+    public abstract int size();
+
+    @Override
+    public ByteString[] toArray() {
+        return toArray(new ByteString[size()]);
+    }
+
+    @Override
+    public String toString() {
+        return toString(this);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
new file mode 100644
index 0000000..c039f34
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
@@ -0,0 +1,440 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.requests.Requests.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class provides a skeletal implementation of the {@code Connection}
+ * interface, to minimize the effort required to implement this interface.
+ */
+public abstract class AbstractConnection implements Connection {
+    private static final class SingleEntryHandler implements SearchResultHandler {
+        private volatile SearchResultEntry firstEntry;
+        private volatile SearchResultReference firstReference;
+        private volatile int entryCount;
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            if (firstEntry == null) {
+                firstEntry = entry;
+            }
+            entryCount++;
+            return true;
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            if (firstReference == null) {
+                firstReference = reference;
+            }
+            return true;
+        }
+
+        /**
+         * Filter the provided error in order to transform size limit exceeded
+         * error to a client side error, or leave it as is for any other error.
+         *
+         * @param error
+         *            to filter
+         * @return provided error in most case, or
+         *         <code>ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED</code>
+         *         error if provided error is
+         *         <code>ResultCode.SIZE_LIMIT_EXCEEDED</code>
+         */
+        private LdapException filterError(final LdapException error) {
+            if (error.getResult().getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
+                return newLdapException(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT.get().toString());
+            } else {
+                return error;
+            }
+        }
+
+        /**
+         * Check for any error related to number of search result at client-side
+         * level: no result, too many result, search result reference. This
+         * method should be called only after search operation is finished.
+         *
+         * @return The single search result entry.
+         * @throws LdapException
+         *             If an error is detected.
+         */
+        private SearchResultEntry getSingleEntry() throws LdapException {
+            if (entryCount == 0) {
+                // Did not find any entries.
+                throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
+                        ERR_NO_SEARCH_RESULT_ENTRIES.get().toString());
+            } else if (entryCount > 1) {
+                // Got more entries than expected.
+                throw newLdapException(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(entryCount).toString());
+            } else if (firstReference != null) {
+                // Got an unexpected search result reference.
+                throw newLdapException(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(firstReference.getURIs().iterator().next())
+                        .toString());
+            } else {
+                return firstEntry;
+            }
+        }
+    }
+
+    /** Visitor used for processing synchronous change requests. */
+    private static final ChangeRecordVisitor<Object, Connection> SYNC_VISITOR =
+            new ChangeRecordVisitor<Object, Connection>() {
+
+                @Override
+                public Object visitChangeRecord(final Connection p, final AddRequest change) {
+                    try {
+                        return p.add(change);
+                    } catch (final LdapException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public Object visitChangeRecord(final Connection p, final DeleteRequest change) {
+                    try {
+                        return p.delete(change);
+                    } catch (final LdapException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public Object visitChangeRecord(final Connection p, final ModifyDNRequest change) {
+                    try {
+                        return p.modifyDN(change);
+                    } catch (final LdapException e) {
+                        return e;
+                    }
+                }
+
+                @Override
+                public Object visitChangeRecord(final Connection p, final ModifyRequest change) {
+                    try {
+                        return p.modify(change);
+                    } catch (final LdapException e) {
+                        return e;
+                    }
+                }
+            };
+
+    /** Creates a new abstract connection. */
+    protected AbstractConnection() {
+        // No implementation required.
+    }
+
+    @Override
+    public Result add(final Entry entry) throws LdapException {
+        return add(Requests.newAddRequest(entry));
+    }
+
+    @Override
+    public Result add(final String... ldifLines) throws LdapException {
+        return add(Requests.newAddRequest(ldifLines));
+    }
+
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request) {
+        return addAsync(request, null);
+    }
+
+    @Override
+    public Result applyChange(final ChangeRecord request) throws LdapException {
+        final Object result = request.accept(SYNC_VISITOR, this);
+        if (result instanceof Result) {
+            return (Result) result;
+        } else {
+            throw (LdapException) result;
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> applyChangeAsync(ChangeRecord request) {
+        return applyChangeAsync(request, null);
+    }
+
+    @Override
+    public LdapPromise<Result> applyChangeAsync(final ChangeRecord request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final ChangeRecordVisitor<LdapPromise<Result>, Connection> visitor =
+            new ChangeRecordVisitor<LdapPromise<Result>, Connection>() {
+
+                @Override
+                public LdapPromise<Result> visitChangeRecord(final Connection p, final AddRequest change) {
+                    return p.addAsync(change, intermediateResponseHandler);
+                }
+
+                @Override
+                public LdapPromise<Result> visitChangeRecord(final Connection p, final DeleteRequest change) {
+                    return p.deleteAsync(change, intermediateResponseHandler);
+                }
+
+                @Override
+                public LdapPromise<Result> visitChangeRecord(final Connection p, final ModifyDNRequest change) {
+                    return p.modifyDNAsync(change, intermediateResponseHandler);
+                }
+
+                @Override
+                public LdapPromise<Result> visitChangeRecord(final Connection p, final ModifyRequest change) {
+                    return p.modifyAsync(change, intermediateResponseHandler);
+                }
+            };
+        return request.accept(visitor, this);
+    }
+
+    @Override
+    public BindResult bind(final String name, final char[] password) throws LdapException {
+        return bind(Requests.newSimpleBindRequest(name, password));
+    }
+
+    @Override
+    public LdapPromise<BindResult> bindAsync(final BindRequest request) {
+        return bindAsync(request, null);
+    }
+
+    @Override
+    public void close() {
+        close(Requests.newUnbindRequest(), null);
+    }
+
+    @Override
+    public CompareResult compare(final String name, final String attributeDescription, final String assertionValue)
+            throws LdapException {
+        return compare(Requests.newCompareRequest(name, attributeDescription, assertionValue));
+    }
+
+    @Override
+    public LdapPromise<CompareResult> compareAsync(final CompareRequest request) {
+        return compareAsync(request, null);
+    }
+
+    @Override
+    public Result delete(final String name) throws LdapException {
+        return delete(Requests.newDeleteRequest(name));
+    }
+
+    @Override
+    public LdapPromise<Result> deleteAsync(final DeleteRequest request) {
+        return deleteAsync(request, null);
+    }
+
+    @Override
+    public Result deleteSubtree(final String name) throws LdapException {
+        return delete(Requests.newDeleteRequest(name).addControl(SubtreeDeleteRequestControl.newControl(true)));
+    }
+
+    @Override
+    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request) throws LdapException {
+        return extendedRequest(request, null);
+    }
+
+    @Override
+    public GenericExtendedResult extendedRequest(final String requestName, final ByteString requestValue)
+            throws LdapException {
+        return extendedRequest(Requests.newGenericExtendedRequest(requestName, requestValue));
+    }
+
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request) {
+        return extendedRequestAsync(request, null);
+    }
+
+    @Override
+    public Result modify(final String... ldifLines) throws LdapException {
+        return modify(Requests.newModifyRequest(ldifLines));
+    }
+
+    @Override
+    public LdapPromise<Result> modifyAsync(final ModifyRequest request) {
+        return modifyAsync(request, null);
+    }
+
+    @Override
+    public Result modifyDN(final String name, final String newRDN) throws LdapException {
+        return modifyDN(Requests.newModifyDNRequest(name, newRDN));
+    }
+
+    @Override
+    public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request) {
+        return modifyDNAsync(request, null);
+    }
+
+    @Override
+    public SearchResultEntry readEntry(final DN baseObject, final String... attributeDescriptions)
+            throws LdapException {
+        final SearchRequest request =
+            Requests.newSingleEntrySearchRequest(baseObject, SearchScope.BASE_OBJECT, Filter.objectClassPresent(),
+                attributeDescriptions);
+        return searchSingleEntry(request);
+    }
+
+    @Override
+    public SearchResultEntry readEntry(final String baseObject, final String... attributeDescriptions)
+            throws LdapException {
+        return readEntry(DN.valueOf(baseObject), attributeDescriptions);
+    }
+
+    @Override
+    public LdapPromise<SearchResultEntry> readEntryAsync(final DN name,
+            final Collection<String> attributeDescriptions) {
+        final SearchRequest request = Requests.newSingleEntrySearchRequest(name, SearchScope.BASE_OBJECT,
+                Filter.objectClassPresent());
+        if (attributeDescriptions != null) {
+            request.getAttributes().addAll(attributeDescriptions);
+        }
+        return searchSingleEntryAsync(request);
+    }
+
+    @Override
+    public ConnectionEntryReader search(final SearchRequest request) {
+        return new ConnectionEntryReader(this, request);
+    }
+
+    @Override
+    public Result search(final SearchRequest request, final Collection<? super SearchResultEntry> entries)
+            throws LdapException {
+        return search(request, entries, null);
+    }
+
+    @Override
+    public Result search(final SearchRequest request, final Collection<? super SearchResultEntry> entries,
+        final Collection<? super SearchResultReference> references) throws LdapException {
+        Reject.ifNull(request, entries);
+        // FIXME: does this need to be thread safe?
+        final SearchResultHandler handler = new SearchResultHandler() {
+            @Override
+            public boolean handleEntry(final SearchResultEntry entry) {
+                entries.add(entry);
+                return true;
+            }
+
+            @Override
+            public boolean handleReference(final SearchResultReference reference) {
+                if (references != null) {
+                    references.add(reference);
+                }
+                return true;
+            }
+        };
+
+        return search(request, handler);
+    }
+
+    @Override
+    public ConnectionEntryReader search(final String baseObject, final SearchScope scope, final String filter,
+        final String... attributeDescriptions) {
+        return search(newSearchRequest(baseObject, scope, filter, attributeDescriptions));
+    }
+
+    @Override
+    public LdapPromise<Result> searchAsync(final SearchRequest request, final SearchResultHandler resultHandler) {
+        return searchAsync(request, null, resultHandler);
+    }
+
+    @Override
+    public SearchResultEntry searchSingleEntry(final SearchRequest request) throws LdapException {
+        final SingleEntryHandler handler = new SingleEntryHandler();
+        try {
+            search(enforceSingleEntrySearchRequest(request), handler);
+            return handler.getSingleEntry();
+        } catch (final LdapException e) {
+            throw handler.filterError(e);
+        }
+    }
+
+    @Override
+    public SearchResultEntry searchSingleEntry(final String baseObject, final SearchScope scope, final String filter,
+        final String... attributeDescriptions) throws LdapException {
+        final SearchRequest request =
+            Requests.newSingleEntrySearchRequest(baseObject, scope, filter, attributeDescriptions);
+        return searchSingleEntry(request);
+    }
+
+    @Override
+    public LdapPromise<SearchResultEntry> searchSingleEntryAsync(final SearchRequest request) {
+        final SingleEntryHandler handler = new SingleEntryHandler();
+        return asPromise(searchAsync(enforceSingleEntrySearchRequest(request), handler).then(
+                new Function<Result, SearchResultEntry, LdapException>() {
+                    @Override
+                    public SearchResultEntry apply(final Result value) throws LdapException {
+                        return handler.getSingleEntry();
+                    }
+                }, new Function<LdapException, SearchResultEntry, LdapException>() {
+                    @Override
+                    public SearchResultEntry apply(final LdapException error) throws LdapException {
+                        throw handler.filterError(error);
+                    }
+                }));
+    }
+
+    /**
+     * Ensure that a single entry search request is returned, based on provided request.
+     *
+     * @param request
+     *            to be checked
+     * @return a single entry search request, equal to or based on the provided request
+     */
+    private SearchRequest enforceSingleEntrySearchRequest(final SearchRequest request) {
+        if (request.isSingleEntrySearch()) {
+            return request;
+        } else {
+            return Requests.copyOfSearchRequest(request).setSizeLimit(1);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Sub-classes should provide an implementation which returns an appropriate
+     * description of the connection which may be used for debugging purposes.
+     * </p>
+     */
+    @Override
+    public abstract String toString();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnectionWrapper.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnectionWrapper.java
new file mode 100644
index 0000000..126a0f3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractConnectionWrapper.java
@@ -0,0 +1,635 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An abstract base class from which connection wrappers may be easily
+ * implemented. The default implementation of each method is to delegate to the
+ * wrapped connection.
+ *
+ * @param <C>
+ *            The type of wrapped connection.
+ */
+public abstract class AbstractConnectionWrapper<C extends Connection> implements Connection {
+    /** The wrapped connection. */
+    protected final C connection;
+
+    /**
+     * Creates a new connection wrapper.
+     *
+     * @param connection
+     *            The connection to be wrapped.
+     */
+    protected AbstractConnectionWrapper(final C connection) {
+        Reject.ifNull(connection);
+        this.connection = connection;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+        return connection.abandonAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result add(final AddRequest request) throws LdapException {
+        return connection.add(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result add(final Entry entry) throws LdapException {
+        return connection.add(entry);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result add(final String... ldifLines) throws LdapException {
+        return connection.add(ldifLines);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request) {
+        return connection.addAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.addAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public void addConnectionEventListener(final ConnectionEventListener listener) {
+        connection.addConnectionEventListener(listener);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result applyChange(final ChangeRecord request) throws LdapException {
+        return connection.applyChange(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> applyChangeAsync(ChangeRecord request) {
+        return connection.applyChangeAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> applyChangeAsync(ChangeRecord request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.applyChangeAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public BindResult bind(final BindRequest request) throws LdapException {
+        return connection.bind(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public BindResult bind(final String name, final char[] password) throws LdapException {
+        return connection.bind(name, password);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<BindResult> bindAsync(final BindRequest request) {
+        return connection.bindAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<BindResult> bindAsync(BindRequest request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.bindAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public void close() {
+        connection.close();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public void close(final UnbindRequest request, final String reason) {
+        connection.close(request, reason);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public CompareResult compare(final CompareRequest request) throws LdapException {
+        return connection.compare(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public CompareResult compare(final String name, final String attributeDescription, final String assertionValue)
+            throws LdapException {
+        return connection.compare(name, attributeDescription, assertionValue);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<CompareResult> compareAsync(final CompareRequest request) {
+        return connection.compareAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<CompareResult> compareAsync(CompareRequest request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.compareAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result delete(final DeleteRequest request) throws LdapException {
+        return connection.delete(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result delete(final String name) throws LdapException {
+        return connection.delete(name);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> deleteAsync(final DeleteRequest request) {
+        return connection.deleteAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> deleteAsync(DeleteRequest request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.deleteAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result deleteSubtree(final String name) throws LdapException {
+        return connection.deleteSubtree(name);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request) throws LdapException {
+        return connection.extendedRequest(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
+            final IntermediateResponseHandler handler) throws LdapException {
+        return connection.extendedRequest(request, handler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public GenericExtendedResult extendedRequest(final String requestName, final ByteString requestValue)
+            throws LdapException {
+        return connection.extendedRequest(requestName, requestValue);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request) {
+        return connection.extendedRequestAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.extendedRequestAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public boolean isClosed() {
+        return connection.isClosed();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public boolean isValid() {
+        return connection.isValid();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result modify(final ModifyRequest request) throws LdapException {
+        return connection.modify(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result modify(final String... ldifLines) throws LdapException {
+        return connection.modify(ldifLines);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> modifyAsync(final ModifyRequest request) {
+        return connection.modifyAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> modifyAsync(ModifyRequest request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return connection.modifyAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result modifyDN(final ModifyDNRequest request) throws LdapException {
+        return connection.modifyDN(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result modifyDN(final String name, final String newRDN) throws LdapException {
+        return connection.modifyDN(name, newRDN);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request) {
+        return connection.modifyDNAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
+            IntermediateResponseHandler intermediateResponseHandler) {
+        return modifyDNAsync(request, intermediateResponseHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public SearchResultEntry readEntry(final DN name, final String... attributeDescriptions) throws LdapException {
+        return connection.readEntry(name, attributeDescriptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public SearchResultEntry readEntry(final String name, final String... attributeDescriptions) throws LdapException {
+        return connection.readEntry(name, attributeDescriptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<SearchResultEntry> readEntryAsync(final DN name,
+            final Collection<String> attributeDescriptions) {
+        return connection.readEntryAsync(name, attributeDescriptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public void removeConnectionEventListener(final ConnectionEventListener listener) {
+        connection.removeConnectionEventListener(listener);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public ConnectionEntryReader search(final SearchRequest request) {
+        return connection.search(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result search(final SearchRequest request,
+            final Collection<? super SearchResultEntry> entries) throws LdapException {
+        return connection.search(request, entries);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result search(final SearchRequest request,
+            final Collection<? super SearchResultEntry> entries,
+            final Collection<? super SearchResultReference> references) throws LdapException {
+        return connection.search(request, entries, references);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public Result search(final SearchRequest request, final SearchResultHandler handler) throws LdapException {
+        return connection.search(request, handler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public ConnectionEntryReader search(final String baseObject, final SearchScope scope,
+            final String filter, final String... attributeDescriptions) {
+        return connection.search(baseObject, scope, filter, attributeDescriptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> searchAsync(final SearchRequest request,
+            final SearchResultHandler resultHandler) {
+        return connection.searchAsync(request, resultHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<Result> searchAsync(SearchRequest request,
+            IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) {
+        return connection.searchAsync(request, intermediateResponseHandler, entryHandler);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public SearchResultEntry searchSingleEntry(final SearchRequest request) throws LdapException {
+        return connection.searchSingleEntry(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public SearchResultEntry searchSingleEntry(final String baseObject, final SearchScope scope, final String filter,
+            final String... attributeDescriptions) throws LdapException {
+        return connection.searchSingleEntry(baseObject, scope, filter, attributeDescriptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public LdapPromise<SearchResultEntry> searchSingleEntryAsync(final SearchRequest request) {
+        return connection.searchSingleEntryAsync(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
+    public String toString() {
+        return connection.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
new file mode 100644
index 0000000..766107b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
@@ -0,0 +1,260 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import com.forgerock.opendj.util.Iterables;
+import com.forgerock.opendj.util.Predicate;
+import org.forgerock.util.Reject;
+
+/**
+ * This class provides a skeletal implementation of the {@code Entry} interface,
+ * to minimize the effort required to implement this interface.
+ */
+public abstract class AbstractEntry implements Entry {
+
+    /** Predicate used for findAttributes. */
+    private static final Predicate<Attribute, AttributeDescription> FIND_ATTRIBUTES_PREDICATE =
+            new Predicate<Attribute, AttributeDescription>() {
+
+                @Override
+                public boolean matches(final Attribute value, final AttributeDescription p) {
+                    return value.getAttributeDescription().isSubTypeOf(p);
+                }
+
+            };
+
+    /**
+     * Sole constructor.
+     */
+    protected AbstractEntry() {
+        // No implementation required.
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute) {
+        return addAttribute(attribute, null);
+    }
+
+    @Override
+    public Entry addAttribute(final String attributeDescription, final Object... values) {
+        addAttribute(new LinkedAttribute(attributeDescription, values), null);
+        return this;
+    }
+
+    @Override
+    public boolean containsAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        final Attribute a = getAttribute(attribute.getAttributeDescription());
+        if (a == null) {
+            if (missingValues != null) {
+                missingValues.addAll(attribute);
+            }
+            return false;
+        } else {
+            boolean result = true;
+            for (final ByteString value : attribute) {
+                if (!a.contains(value)) {
+                    if (missingValues != null) {
+                        missingValues.add(value);
+                    }
+                    result = false;
+                }
+            }
+            return result;
+        }
+    }
+
+    @Override
+    public boolean containsAttribute(final String attributeDescription, final Object... values) {
+        return containsAttribute(new LinkedAttribute(attributeDescription, values), null);
+    }
+
+    @Override
+    public boolean equals(final Object object) {
+        if (this == object) {
+            return true;
+        } else if (object instanceof Entry) {
+            final Entry other = (Entry) object;
+            if (!getName().equals(other.getName())) {
+                return false;
+            }
+            // Distinguished name is the same, compare attributes.
+            if (getAttributeCount() != other.getAttributeCount()) {
+                return false;
+            }
+            for (final Attribute attribute : getAllAttributes()) {
+                final Attribute otherAttribute =
+                        other.getAttribute(attribute.getAttributeDescription());
+                if (!attribute.equals(otherAttribute)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+
+        return Iterables.filteredIterable(getAllAttributes(), FIND_ATTRIBUTES_PREDICATE,
+                attributeDescription);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+        return getAllAttributes(AttributeDescription.valueOf(attributeDescription));
+    }
+
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        for (final Attribute attribute : getAllAttributes()) {
+            final AttributeDescription ad = attribute.getAttributeDescription();
+            if (isAssignable(attributeDescription, ad)) {
+                return attribute;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Attribute getAttribute(final String attributeDescription) {
+        return getAttribute(AttributeDescription.valueOf(attributeDescription));
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = getName().hashCode();
+        for (final Attribute attribute : getAllAttributes()) {
+            hashCode += attribute.hashCode();
+        }
+        return hashCode;
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+        return AttributeParser.parseAttribute(getAttribute(attributeDescription));
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
+        return AttributeParser.parseAttribute(getAttribute(attributeDescription));
+    }
+
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        final Iterator<Attribute> i = getAllAttributes().iterator();
+        final AttributeDescription attributeDescription = attribute.getAttributeDescription();
+        while (i.hasNext()) {
+            final Attribute oldAttribute = i.next();
+            if (isAssignable(attributeDescription, oldAttribute.getAttributeDescription())) {
+                if (attribute.isEmpty()) {
+                    i.remove();
+                    return true;
+                } else {
+                    final boolean modified = oldAttribute.removeAll(attribute, missingValues);
+                    if (oldAttribute.isEmpty()) {
+                        i.remove();
+                        return true;
+                    }
+                    return modified;
+                }
+            }
+        }
+        // Not found.
+        if (missingValues != null) {
+            missingValues.addAll(attribute);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean removeAttribute(final AttributeDescription attributeDescription) {
+        return removeAttribute(Attributes.emptyAttribute(attributeDescription), null);
+    }
+
+    @Override
+    public Entry removeAttribute(final String attributeDescription, final Object... values) {
+        removeAttribute(new LinkedAttribute(attributeDescription, values), null);
+        return this;
+    }
+
+    @Override
+    public boolean replaceAttribute(final Attribute attribute) {
+        if (attribute.isEmpty()) {
+            return removeAttribute(attribute.getAttributeDescription());
+        } else {
+            /*
+             * For consistency with addAttribute and removeAttribute, preserve
+             * the existing attribute if it already exists.
+             */
+            final Attribute oldAttribute = getAttribute(attribute.getAttributeDescription());
+            if (oldAttribute != null) {
+                oldAttribute.clear();
+                oldAttribute.addAll(attribute);
+            } else {
+                addAttribute(attribute, null);
+            }
+            return true;
+        }
+    }
+
+    @Override
+    public Entry replaceAttribute(final String attributeDescription, final Object... values) {
+        replaceAttribute(new LinkedAttribute(attributeDescription, values));
+        return this;
+    }
+
+    @Override
+    public Entry setName(final String dn) {
+        return setName(DN.valueOf(dn));
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append('"');
+        builder.append(getName());
+        builder.append("\":{");
+        boolean firstValue = true;
+        for (final Attribute attribute : getAllAttributes()) {
+            if (!firstValue) {
+                builder.append(',');
+            }
+            builder.append(attribute);
+            firstValue = false;
+        }
+        builder.append('}');
+        return builder.toString();
+    }
+
+    private boolean isAssignable(final AttributeDescription from, final AttributeDescription to) {
+        if (!from.isPlaceHolder()) {
+            return from.equals(to);
+        } else {
+            return from.matches(to);
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractFilterVisitor.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractFilterVisitor.java
new file mode 100644
index 0000000..33ec38c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractFilterVisitor.java
@@ -0,0 +1,173 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.List;
+
+/**
+ * An abstract filter visitor whose default implementation for all
+ * {@code Visitor} methods is to invoke {@link #visitDefaultFilter(Object)}.
+ * <p>
+ * Implementations can override the methods on a case by case behavior.
+ *
+ * @param <R>
+ *            The return type of this visitor's methods. Use
+ *            {@link java.lang.Void} for visitors that do not need to return
+ *            results.
+ * @param <P>
+ *            The type of the additional parameter to this visitor's methods.
+ *            Use {@link java.lang.Void} for visitors that do not need an
+ *            additional parameter.
+ */
+public abstract class AbstractFilterVisitor<R, P> implements FilterVisitor<R, P> {
+    /** Default constructor. */
+    protected AbstractFilterVisitor() {
+        // Nothing to do.
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitAndFilter(final P p, final List<Filter> subFilters) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitApproxMatchFilter(final P p, final String attributeDescription,
+            final ByteString assertionValue) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * Visits any filters which are not explicitly handled by other visitor
+     * methods.
+     * <p>
+     * The default implementation of this method is to return {@code null}.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @return A visitor specified result.
+     */
+    public R visitDefaultFilter(final P p) {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitEqualityMatchFilter(final P p, final String attributeDescription,
+            final ByteString assertionValue) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitExtensibleMatchFilter(final P p, final String matchingRule,
+            final String attributeDescription, final ByteString assertionValue,
+            final boolean dnAttributes) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitGreaterOrEqualFilter(final P p, final String attributeDescription,
+            final ByteString assertionValue) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitLessOrEqualFilter(final P p, final String attributeDescription,
+            final ByteString assertionValue) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitNotFilter(final P p, final Filter subFilter) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitOrFilter(final P p, final List<Filter> subFilters) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitPresentFilter(final P p, final String attributeDescription) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitSubstringsFilter(final P p, final String attributeDescription,
+            final ByteString initialSubstring, final List<ByteString> anySubstrings,
+            final ByteString finalSubstring) {
+        return visitDefaultFilter(p);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call {@link #visitDefaultFilter(Object)}.
+     */
+    @Override
+    public R visitUnrecognizedFilter(final P p, final byte filterTag, final ByteString filterBytes) {
+        return visitDefaultFilter(p);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java
new file mode 100644
index 0000000..5cb8d3d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractMapEntry.java
@@ -0,0 +1,123 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.forgerock.util.Reject;
+
+/** Abstract implementation for {@code Map} based entries. */
+abstract class AbstractMapEntry extends AbstractEntry {
+    private final Map<AttributeDescription, Attribute> attributes;
+    private DN name;
+
+    /**
+     * Creates an empty entry using the provided distinguished name and
+     * {@code Map}.
+     *
+     * @param name
+     *            The distinguished name of this entry.
+     * @param attributes
+     *            The attribute map.
+     */
+    AbstractMapEntry(final DN name, final Map<AttributeDescription, Attribute> attributes) {
+        this.name = name;
+        this.attributes = attributes;
+    }
+
+    @Override
+    public final boolean addAttribute(final Attribute attribute,
+            final Collection<? super ByteString> duplicateValues) {
+        final AttributeDescription attributeDescription = attribute.getAttributeDescription();
+        final Attribute oldAttribute = getAttribute(attributeDescription);
+        if (oldAttribute != null) {
+            return oldAttribute.addAll(attribute, duplicateValues);
+        } else {
+            attributes.put(attributeDescription, attribute);
+            return true;
+        }
+    }
+
+    @Override
+    public final Entry clearAttributes() {
+        attributes.clear();
+        return this;
+    }
+
+    @Override
+    public final Iterable<Attribute> getAllAttributes() {
+        return attributes.values();
+    }
+
+    @Override
+    public final Attribute getAttribute(final AttributeDescription attributeDescription) {
+        final Attribute attribute = attributes.get(attributeDescription);
+        if (attribute == null && attributeDescription.isPlaceHolder()) {
+            // Fall-back to inefficient search using place-holder.
+            return super.getAttribute(attributeDescription);
+        } else {
+            return attribute;
+        }
+    }
+
+    @Override
+    public final int getAttributeCount() {
+        return attributes.size();
+    }
+
+    @Override
+    public final DN getName() {
+        return name;
+    }
+
+    @Override
+    public final boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        final AttributeDescription attributeDescription = attribute.getAttributeDescription();
+        if (attribute.isEmpty()) {
+            return attributes.remove(attributeDescription) != null
+                || (attributeDescription.isPlaceHolder()
+                    // Fall-back to inefficient remove using place-holder.
+                    && super.removeAttribute(attribute, missingValues));
+        } else {
+            final Attribute oldAttribute = getAttribute(attributeDescription);
+            if (oldAttribute != null) {
+                final boolean modified = oldAttribute.removeAll(attribute, missingValues);
+                if (oldAttribute.isEmpty()) {
+                    // Use old attribute's description in case it is different
+                    // (e.g. this may be the case when using place-holders).
+                    attributes.remove(oldAttribute.getAttributeDescription());
+                    return true;
+                }
+                return modified;
+            } else {
+                if (missingValues != null) {
+                    missingValues.addAll(attribute);
+                }
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public final Entry setName(final DN dn) {
+        Reject.ifNull(dn);
+        this.name = dn;
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractSynchronousConnection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractSynchronousConnection.java
new file mode 100644
index 0000000..c328682
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AbstractSynchronousConnection.java
@@ -0,0 +1,153 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
+/**
+ * An abstract connection whose asynchronous methods are implemented in terms of
+ * synchronous methods.
+ * <p>
+ * <b>NOTE:</b> this implementation does not support intermediate response
+ * handlers except for extended operations, because they are not supported by
+ * the equivalent synchronous methods.
+ */
+public abstract class AbstractSynchronousConnection extends AbstractConnection {
+    /** Creates a new abstract synchronous connection. */
+    protected AbstractSynchronousConnection() {
+        // No implementation required.
+    }
+
+    /**
+     * Abandon operations are not supported because operations are performed
+     * synchronously and the ID of the request to be abandoned cannot be
+     * determined. Thread interruption must be used in order to cancel a blocked
+     * request.
+     *
+     * @param request
+     *            {@inheritDoc}
+     * @return {@inheritDoc}
+     * @throws UnsupportedOperationException
+     *             Always thrown: abandon requests are not supported for
+     *             synchronous connections.
+     */
+    @Override
+    public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+        throw new UnsupportedOperationException("Abandon requests are not supported for synchronous connections");
+    }
+
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(add(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<BindResult> bindAsync(final BindRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(bind(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(compare(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> deleteAsync(final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(delete(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(extendedRequest(request, intermediateResponseHandler));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> modifyAsync(final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(modify(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        try {
+            return thenOnResult(modifyDN(request));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> searchAsync(final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
+        try {
+            return thenOnResult(search(request, entryHandler));
+        } catch (final LdapException e) {
+            return onException(e);
+        }
+    }
+
+    private <R extends Result> LdapPromise<R> onException(final LdapException e) {
+        return newFailedLdapPromise(e);
+    }
+
+    private <R extends Result> LdapPromise<R> thenOnResult(final R result) {
+        return newSuccessfulLdapPromise(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AddressMask.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AddressMask.java
new file mode 100644
index 0000000..13d222e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AddressMask.java
@@ -0,0 +1,512 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.lang.reflect.Method;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.BitSet;
+import java.util.Collection;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+/**
+ * An address mask can be used to perform efficient comparisons against IP
+ * addresses to determine whether a particular IP address is in a given range.
+ */
+public final class AddressMask {
+    /**
+     * Types of rules we have. IPv4 - ipv4 rule IPv6 - ipv6 rule (begin with '['
+     * or contains an ':'). HOST - hostname match (foo.sun.com) HOSTPATTERN -
+     * host pattern match (begin with '.') ALLWILDCARD - *.*.*.* (first HOST is
+     * applied then ipv4)
+     */
+    enum RuleType {
+        ALLWILDCARD, HOST, HOSTPATTERN, IPv4, IPv6
+    }
+
+    /** IPv4 values for number of bytes and max CIDR prefix. */
+    private static final int IN4ADDRSZ = 4;
+    private static final int IPV4MAXPREFIX = 32;
+
+    /** IPv6 values for number of bytes and max CIDR prefix. */
+    private static final int IN6ADDRSZ = 16;
+    private static final int IPV6MAXPREFIX = 128;
+
+    /**
+     * Returns {@code true} if an address matches any of the provided address
+     * masks.
+     *
+     * @param address
+     *            The address.
+     * @param masks
+     *            A collection of address masks to check.
+     * @return {@code true} if an address matches any of the provided address
+     *         masks.
+     */
+    public static boolean matchesAny(final Collection<AddressMask> masks, final InetAddress address) {
+        if (address != null) {
+            for (final AddressMask mask : masks) {
+                if (mask.matches(address)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Parses the provided string as an address mask.
+     *
+     * @param mask
+     *            The address mask string to be parsed.
+     * @return The parsed address mask.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided string cannot be decoded as an address mask.
+     */
+    public static AddressMask valueOf(final String mask) {
+        return new AddressMask(mask);
+    }
+
+    /** Array that holds each component of a hostname. */
+    private String[] hostName;
+
+    /** Holds a hostname pattern (ie, rule that begins with '.');'. */
+    private String hostPattern;
+
+    /** Holds binary representations of rule and mask respectively. */
+    private byte[] ruleMask, prefixMask;
+
+    /** Holds string passed into the constructor. */
+    private final String ruleString;
+
+    /** Type of rule determined. */
+    private RuleType ruleType;
+
+    /** Bit array that holds wildcard info for above binary arrays. */
+    private final BitSet wildCard = new BitSet();
+
+    private AddressMask(final String rule) {
+        determineRuleType(rule);
+        switch (ruleType) {
+        case IPv6:
+            processIPv6(rule);
+            break;
+
+        case IPv4:
+            processIpv4(rule);
+            break;
+
+        case HOST:
+            processHost(rule);
+            break;
+
+        case HOSTPATTERN:
+            processHostPattern(rule);
+            break;
+
+        case ALLWILDCARD:
+            processAllWilds(rule);
+        }
+        ruleString = rule;
+    }
+
+    /**
+     * Returns {@code true} if this address mask matches the provided address.
+     *
+     * @param address
+     *            The address.
+     * @return {@code true} if this address mask matches the provided address.
+     */
+    public boolean matches(final InetAddress address) {
+        boolean ret = false;
+
+        switch (ruleType) {
+        case IPv6:
+        case IPv4:
+            // this Address mask is an IPv4 rule
+            ret = matchAddress(address.getAddress());
+            break;
+
+        case HOST:
+            // HOST rule use hostname
+            ret = matchHostName(address.getHostName());
+            break;
+
+        case HOSTPATTERN:
+            // HOSTPATTERN rule
+            ret = matchPattern(address.getHostName());
+            break;
+
+        case ALLWILDCARD:
+            // first try ipv4 addr match, then hostname
+            ret = matchAddress(address.getAddress());
+            if (!ret) {
+                ret = matchHostName(address.getHostName());
+            }
+            break;
+        }
+        return ret;
+    }
+
+    /**
+     * Returns the string representation of this address mask.
+     *
+     * @return The string representation of this address mask.
+     */
+    @Override
+    public String toString() {
+        return ruleString;
+    }
+
+    /**
+     * Try to determine what type of rule string this is. See RuleType above for
+     * valid types.
+     *
+     * @param ruleString
+     *            The rule string to be examined.
+     * @throws LocalizedIllegalArgumentException
+     *             If the rule type cannot be determined from the rule string.
+     */
+    private void determineRuleType(final String ruleString) {
+        // Rule ending with '.' is invalid'
+        if (ruleString.endsWith(".")) {
+            throw genericDecodeError();
+        } else if (ruleString.startsWith(".")) {
+            ruleType = RuleType.HOSTPATTERN;
+        } else if (ruleString.startsWith("[") || ruleString.indexOf(':') != -1) {
+            ruleType = RuleType.IPv6;
+        } else {
+            int wildcardsCount = 0;
+            final String[] s = ruleString.split("\\.", -1);
+            /*
+             * Try to figure out how many wildcards and if the rule is hostname
+             * (can't begin with digit) or ipv4 address. Default to IPv4 ruletype.
+             */
+            ruleType = RuleType.HOST;
+            for (final String value : s) {
+                if ("*".equals(value)) {
+                    wildcardsCount++;
+                    continue;
+                }
+                // Looks like an ipv4 address
+                if (Character.isDigit(value.charAt(0))) {
+                    ruleType = RuleType.IPv4;
+                    break;
+                }
+            }
+            // All wildcards (*.*.*.*)
+            if (wildcardsCount == s.length) {
+                ruleType = RuleType.ALLWILDCARD;
+            }
+        }
+    }
+
+    /**
+     * Try to match remote client address using prefix mask and rule mask.
+     *
+     * @param remoteMask
+     *            The byte array with remote client address.
+     * @return <CODE>true</CODE> if remote client address matches or
+     *         <CODE>false</CODE>if not.
+     */
+    private boolean matchAddress(final byte[] remoteMask) {
+        if (ruleType == RuleType.ALLWILDCARD) {
+            return true;
+        }
+        if (prefixMask == null) {
+            return false;
+        }
+        if (remoteMask.length != prefixMask.length) {
+            return false;
+        }
+        for (int i = 0; i < prefixMask.length; i++) {
+            if (!wildCard.get(i)
+                    && (ruleMask[i] & prefixMask[i]) != (remoteMask[i] & prefixMask[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Try to match remote client host name against rule host name.
+     *
+     * @param remoteHostName
+     *            The remote host name string.
+     * @return <CODE>true</CODE>if the remote client host name matches
+     *         <CODE>false</CODE> if it does not.
+     */
+    private boolean matchHostName(final String remoteHostName) {
+        final String[] s = remoteHostName.split("\\.", -1);
+        if (s.length != hostName.length) {
+            return false;
+        }
+        if (ruleType == RuleType.ALLWILDCARD) {
+            return true;
+        }
+        for (int i = 0; i < s.length; i++) {
+            // skip if wildcard
+            if (!"*".equals(hostName[i])
+                    && !s[i].equalsIgnoreCase(hostName[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Try to match remote host name string against the pattern rule.
+     *
+     * @param remoteHostName
+     *            The remote client host name.
+     * @return <CODE>true</CODE>if the remote host name matches or
+     *         <CODE>false</CODE>if not.
+     */
+    private boolean matchPattern(final String remoteHostName) {
+        final int len = remoteHostName.length() - hostPattern.length();
+        return len > 0
+                && remoteHostName.regionMatches(true, len, hostPattern, 0, hostPattern.length());
+    }
+
+    /**
+     * Build the prefix mask of prefix len bits set in the array.
+     *
+     * @param prefix
+     *            The len of the prefix to use.
+     */
+    private void prefixMask(int prefix) {
+        int i;
+        for (i = 0; prefix > 8; i++) {
+            this.prefixMask[i] = (byte) 0xff;
+            prefix -= 8;
+        }
+        this.prefixMask[i] = (byte) (0xff << 8 - prefix);
+    }
+
+    /**
+     * The rule string is all wildcards. Set both address wildcard bitmask and
+     * hostname wildcard array.
+     *
+     * @param rule
+     *            The rule string containing all wildcards.
+     */
+    private void processAllWilds(final String rule) {
+        final String[] s = rule.split("\\.", -1);
+        if (s.length == IN4ADDRSZ) {
+            for (int i = 0; i < IN4ADDRSZ; i++) {
+                wildCard.set(i);
+            }
+        }
+        hostName = rule.split("\\.", -1);
+    }
+
+    /**
+     * Examine rule string and build a hostname string array of its parts.
+     *
+     * @param rule
+     *            The rule string.
+     * @throws LocalizedIllegalArgumentException
+     *             If the rule string is not a valid host name.
+     */
+    private void processHost(final String rule) {
+        // Note that '*' is valid in host rule
+        final String[] s = rule.split("^[0-9a-zA-z-.*]+");
+        if (s.length > 0) {
+            throw genericDecodeError();
+        }
+        hostName = rule.split("\\.", -1);
+    }
+
+    /**
+     * Examine the rule string of a host pattern and set the host pattern from
+     * the rule.
+     *
+     * @param rule
+     *            The rule string to examine.
+     * @throws LocalizedIllegalArgumentException
+     *             If the rule string is not a valid host pattern rule.
+     */
+    private void processHostPattern(final String rule) {
+        // quick check for invalid chars like " "
+        final String[] s = rule.split("^[0-9a-zA-z-.]+");
+        if (s.length > 0) {
+            throw genericDecodeError();
+        }
+        hostPattern = rule;
+    }
+
+    /**
+     * The rule string is an IPv4 rule. Build both the prefix mask array and
+     * rule mask from the string.
+     *
+     * @param rule
+     *            The rule string containing the IPv4 rule.
+     * @throws LocalizedIllegalArgumentException
+     *             If the rule string is not a valid IPv4 rule.
+     */
+    private void processIpv4(final String rule) {
+        final String[] s = rule.split("/", -1);
+        this.ruleMask = new byte[IN4ADDRSZ];
+        this.prefixMask = new byte[IN4ADDRSZ];
+        prefixMask(processPrefix(s, IPV4MAXPREFIX));
+        processIPv4Subnet(s.length == 0 ? rule : s[0]);
+    }
+
+    /**
+     * Examine the subnet part of a rule string and build a byte array
+     * representation of it.
+     *
+     * @param subnet
+     *            The subnet string part of the rule.
+     * @throws LocalizedIllegalArgumentException
+     *             If the subnet string is not a valid IPv4 subnet string.
+     */
+    private void processIPv4Subnet(final String subnet) {
+        final String[] s = subnet.split("\\.", -1);
+        try {
+            // Make sure we have four parts
+            if (s.length != IN4ADDRSZ) {
+                throw genericDecodeError();
+            }
+            for (int i = 0; i < IN4ADDRSZ; i++) {
+                final String quad = s[i].trim();
+                if ("*".equals(quad)) {
+                    wildCard.set(i); // see wildcard mark bitset
+                } else {
+                    final long val = Integer.parseInt(quad);
+                    // must be between 0-255
+                    if (val < 0 || val > 0xff) {
+                        throw genericDecodeError();
+                    }
+                    ruleMask[i] = (byte) (val & 0xff);
+                }
+            }
+        } catch (final NumberFormatException nfex) {
+            throw genericDecodeError();
+        }
+    }
+
+    /**
+     * The rule string is an IPv6 rule. Build both the prefix mask array and
+     * rule mask from the string.
+     *
+     * @param rule
+     *            The rule string containing the IPv6 rule.
+     * @throws LocalizedIllegalArgumentException
+     *             If the rule string is not a valid IPv6 rule.
+     */
+    private void processIPv6(final String rule) {
+        final String[] s = rule.split("/", -1);
+        final String address = s[0];
+
+        // Try to avoid calling InetAddress.getByName() because it may do a reverse lookup.
+        final String ipv6Literal;
+        if (address.charAt(0) == '[' && address.charAt(address.length() - 1) == ']') {
+            // isIPv6LiteralAddress must be invoked without surrounding brackets.
+            ipv6Literal = address.substring(1, address.length() - 1);
+        } else {
+            ipv6Literal = address;
+        }
+
+        boolean isValid;
+        try {
+            // Use reflection to avoid dependency on Sun JRE.
+            final Class<?> ipUtils = Class.forName("sun.net.util.IPAddressUtil");
+            final Method method = ipUtils.getMethod("isIPv6LiteralAddress", String.class);
+            isValid = (Boolean) method.invoke(null, ipv6Literal);
+        } catch (Exception e) {
+            /*
+             * Unable to invoke Sun private API. Assume it's ok, but accept that
+             * a DNS query may be performed if it is not valid.
+             */
+            isValid = true;
+        }
+        if (!isValid) {
+            throw genericDecodeError();
+        }
+
+        final InetAddress addr;
+        try {
+            addr = InetAddress.getByName(address);
+        } catch (final UnknownHostException ex) {
+            throw genericDecodeError();
+        }
+        if (addr instanceof Inet6Address) {
+            this.ruleType = RuleType.IPv6;
+            final Inet6Address addr6 = (Inet6Address) addr;
+            this.ruleMask = addr6.getAddress();
+            this.prefixMask = new byte[IN6ADDRSZ];
+            prefixMask(processPrefix(s, IPV6MAXPREFIX));
+        } else {
+            /*
+             * The address might be an IPv4-compat address. Throw an error if
+             * the rule has a prefix.
+             */
+            if (s.length == 2) {
+                throw genericDecodeError();
+            }
+            this.ruleMask = addr.getAddress();
+            this.ruleType = RuleType.IPv4;
+            this.prefixMask = new byte[IN4ADDRSZ];
+            prefixMask(processPrefix(s, IPV4MAXPREFIX));
+        }
+    }
+
+    /**
+     * Examine rule string for correct prefix usage.
+     *
+     * @param s
+     *            The string array with rule string add and prefix strings.
+     * @param maxPrefix
+     *            The max value the prefix can be.
+     * @return The prefix integer value.
+     * @throws LocalizedIllegalArgumentException
+     *             If the string array and prefix are not valid.
+     */
+    private int processPrefix(final String[] s, final int maxPrefix) {
+        int prefix = maxPrefix;
+        try {
+            // can only have one prefix value and a subnet string
+            if (s.length < 1 || s.length > 2) {
+                throw genericDecodeError();
+            } else if (s.length == 2) {
+                // can't have wildcard with a prefix
+                if (s[0].indexOf('*') > -1) {
+                    throw new LocalizedIllegalArgumentException(
+                            ERR_ADDRESSMASK_WILDCARD_DECODE_ERROR.get());
+                }
+                prefix = Integer.parseInt(s[1]);
+            }
+            // must be between 0-maxprefix
+            if (prefix < 0 || prefix > maxPrefix) {
+                throw new LocalizedIllegalArgumentException(ERR_ADDRESSMASK_PREFIX_DECODE_ERROR
+                        .get());
+            }
+        } catch (final NumberFormatException nfex) {
+            throw genericDecodeError();
+        }
+        return prefix;
+    }
+
+    private LocalizedIllegalArgumentException genericDecodeError() {
+        return new LocalizedIllegalArgumentException(ERR_ADDRESSMASK_FORMAT_DECODE_ERROR.get());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Assertion.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Assertion.java
new file mode 100644
index 0000000..dbb76ac
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Assertion.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+
+/**
+ * A compiled attribute value assertion.
+ */
+public interface Assertion {
+
+    /** An assertion that always return UNDEFINED for matches and that creates a match all query. */
+    Assertion UNDEFINED_ASSERTION = new Assertion() {
+        @Override
+        public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+            return ConditionResult.UNDEFINED;
+        }
+
+        @Override
+        public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+            // Subclassing this class will always work, albeit inefficiently.
+            // This is better than throwing an exception for no good reason.
+            return factory.createMatchAllQuery();
+        }
+    };
+
+    /**
+     * Indicates whether the provided attribute value should be considered a
+     * match for this assertion value according to the matching rule.
+     *
+     * @param normalizedAttributeValue
+     *            The normalized attribute value.
+     * @return {@code TRUE} if the attribute value should be considered a match
+     *         for the provided assertion value, {@code FALSE} if it does not
+     *         match, or {@code UNDEFINED} if the result is undefined.
+     */
+    ConditionResult matches(ByteSequence normalizedAttributeValue);
+
+    /**
+     * Returns an index query appropriate for the provided attribute
+     * value assertion.
+     *
+     * @param <T>
+     *          The type of index query created by the {@code factory}.
+     * @param factory
+     *          The index query factory which should be used to
+     *          construct the index query.
+     * @return The index query appropriate for the provided attribute
+     *         value assertion.
+     * @throws DecodeException
+     *           If an error occurs while generating the index query.
+     */
+    <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AssertionFailureException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AssertionFailureException.java
new file mode 100644
index 0000000..4a63a00
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AssertionFailureException.java
@@ -0,0 +1,33 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * failed because the filter contained in an assertion control failed to match
+ * the target entry. More specifically, this exception is used for the
+ * {@link ResultCode#ASSERTION_FAILED ASSERTION_FAILED} result code.
+ */
+@SuppressWarnings("serial")
+public class AssertionFailureException extends LdapException {
+    AssertionFailureException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attribute.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attribute.java
new file mode 100644
index 0000000..63c482a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attribute.java
@@ -0,0 +1,438 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * An attribute, comprising of an attribute description and zero or more
+ * attribute values.
+ * <p>
+ * Any methods which perform comparisons between attribute values use the
+ * equality matching rule associated with the attribute description.
+ * <p>
+ * Any methods which accept {@code Object} based attribute values convert the
+ * attribute values to instances of {@code ByteString} using
+ * {@link ByteString#valueOfObject(Object)}.
+ */
+public interface Attribute extends Set<ByteString> {
+    // TODO: matching against attribute value assertions.
+
+    /**
+     * Adds {@code value} to this attribute if it is not already present
+     * (optional operation). If this attribute already contains {@code value},
+     * the call leaves the attribute unchanged and returns {@code false}.
+     *
+     * @param value
+     *            The attribute value to be added to this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support addition of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code value} was {@code null}.
+     */
+    @Override
+    boolean add(ByteString value);
+
+    /**
+     * Adds all of the provided attribute values to this attribute if they are
+     * not already present (optional operation).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param values
+     *            The attribute values to be added to this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support addition of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    boolean add(Object... values);
+
+    /**
+     * Adds all of the attribute values contained in {@code values} to this
+     * attribute if they are not already present (optional operation).
+     * <p>
+     * An invocation of this method is equivalent to:
+     *
+     * <pre>
+     * attribute.addAll(values, null);
+     * </pre>
+     *
+     * @param values
+     *            The attribute values to be added to this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support addition of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    @Override
+    boolean addAll(Collection<? extends ByteString> values);
+
+    /**
+     * Adds all of the attribute values contained in {@code values} to this
+     * attribute if they are not already present (optional operation). Any
+     * attribute values which are already present will be added to
+     * {@code duplicateValues} if specified.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param <T>
+     *            The type of the attribute value objects being added.
+     * @param values
+     *            The attribute values to be added to this attribute.
+     * @param duplicateValues
+     *            A collection into which duplicate values will be added, or
+     *            {@code null} if duplicate values should not be saved.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support addition of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    <T> boolean addAll(Collection<T> values, Collection<? super T> duplicateValues);
+
+    /**
+     * Removes all of the attribute values from this attribute (optional
+     * operation). This attribute will be empty after this call returns.
+     *
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     */
+    @Override
+    void clear();
+
+    /**
+     * Returns {@code true} if this attribute contains {@code value}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            The attribute value whose presence in this attribute is to be
+     *            tested.
+     * @return {@code true} if this attribute contains {@code value}, or
+     *         {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code value} was {@code null}.
+     */
+    @Override
+    boolean contains(Object value);
+
+    /**
+     * Returns {@code true} if this attribute contains all of the attribute
+     * values contained in {@code values}.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param values
+     *            The attribute values whose presence in this attribute is to be
+     *            tested.
+     * @return {@code true} if this attribute contains all of the attribute
+     *         values contained in {@code values}, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    @Override
+    boolean containsAll(Collection<?> values);
+
+    /**
+     * Returns {@code true} if {@code object} is an attribute which is equal to
+     * this attribute. Two attributes are considered equal if their attribute
+     * descriptions are equal, they both have the same number of attribute
+     * values, and every attribute value contained in the first attribute is
+     * also contained in the second attribute.
+     *
+     * @param object
+     *            The object to be tested for equality with this attribute.
+     * @return {@code true} if {@code object} is an attribute which is equal to
+     *         this attribute, or {@code false} if not.
+     */
+    @Override
+    boolean equals(Object object);
+
+    /**
+     * Returns the first attribute value in this attribute.
+     *
+     * @return The first attribute value in this attribute.
+     * @throws NoSuchElementException
+     *             If this attribute is empty.
+     */
+    ByteString firstValue();
+
+    /**
+     * Returns the first attribute value in this attribute decoded as a UTF-8
+     * string.
+     *
+     * @return The first attribute value in this attribute decoded as a UTF-8
+     *         string.
+     * @throws NoSuchElementException
+     *             If this attribute is empty.
+     */
+    String firstValueAsString();
+
+    /**
+     * Returns the attribute description of this attribute, which includes its
+     * attribute type and any options.
+     *
+     * @return The attribute description.
+     */
+    AttributeDescription getAttributeDescription();
+
+    /**
+     * Returns the string representation of the attribute description of this
+     * attribute, which includes its attribute type and any options.
+     *
+     * @return The string representation of the attribute description.
+     */
+    String getAttributeDescriptionAsString();
+
+    /**
+     * Returns the hash code for this attribute. It will be calculated as the
+     * sum of the hash codes of the attribute description and all of the
+     * attribute values.
+     *
+     * @return The hash code for this attribute.
+     */
+    @Override
+    int hashCode();
+
+    /**
+     * Returns {@code true} if this attribute contains no attribute values.
+     *
+     * @return {@code true} if this attribute contains no attribute values.
+     */
+    @Override
+    boolean isEmpty();
+
+    /**
+     * Returns an iterator over the attribute values in this attribute. The
+     * attribute values are returned in no particular order, unless the
+     * implementation of this attribute provides such a guarantee.
+     *
+     * @return An iterator over the attribute values in this attribute.
+     */
+    @Override
+    Iterator<ByteString> iterator();
+
+    /**
+     * Returns a parser for this attribute which can be used for decoding values
+     * as different types of object.
+     *
+     * @return A parser for this attribute.
+     */
+    AttributeParser parse();
+
+    /**
+     * Removes {@code value} from this attribute if it is present (optional
+     * operation). If this attribute does not contain {@code value}, the call
+     * leaves the attribute unchanged and returns {@code false}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            The attribute value to be removed from this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code value} was {@code null}.
+     */
+    @Override
+    boolean remove(Object value);
+
+    /**
+     * Removes all of the attribute values contained in {@code values} from this
+     * attribute if they are present (optional operation).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * An invocation of this method is equivalent to:
+     *
+     * <pre>
+     * attribute.removeAll(values, null);
+     * </pre>
+     *
+     * @param values
+     *            The attribute values to be removed from this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    @Override
+    boolean removeAll(Collection<?> values);
+
+    /**
+     * Removes all of the attribute values contained in {@code values} from this
+     * attribute if they are present (optional operation). Any attribute values
+     * which are not already present will be added to {@code missingValues} if
+     * specified.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param <T>
+     *            The type of the attribute value objects being removed.
+     * @param values
+     *            The attribute values to be removed from this attribute.
+     * @param missingValues
+     *            A collection into which missing values will be added, or
+     *            {@code null} if missing values should not be saved.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    <T> boolean removeAll(Collection<T> values, Collection<? super T> missingValues);
+
+    /**
+     * Retains only the attribute values in this attribute which are contained
+     * in {@code values} (optional operation).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * An invocation of this method is equivalent to:
+     *
+     * <pre>
+     * attribute.retainAll(values, null);
+     * </pre>
+     *
+     * @param values
+     *            The attribute values to be retained in this attribute.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    @Override
+    boolean retainAll(Collection<?> values);
+
+    /**
+     * Retains only the attribute values in this attribute which are contained
+     * in {@code values} (optional operation). Any attribute values which are
+     * not already present will be added to {@code missingValues} if specified.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param <T>
+     *            The type of the attribute value objects being retained.
+     * @param values
+     *            The attribute values to be retained in this attribute.
+     * @param missingValues
+     *            A collection into which missing values will be added, or
+     *            {@code null} if missing values should not be saved.
+     * @return {@code true} if this attribute changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this attribute does not support removal of attribute
+     *             values.
+     * @throws NullPointerException
+     *             If {@code values} was {@code null}.
+     */
+    <T> boolean retainAll(Collection<T> values, Collection<? super T> missingValues);
+
+    /**
+     * Returns the number of attribute values in this attribute.
+     *
+     * @return The number of attribute values in this attribute.
+     */
+    @Override
+    int size();
+
+    /**
+     * Returns an array containing all of the attribute values contained in this
+     * attribute.
+     * <p>
+     * If this attribute makes any guarantees as to what order its attribute
+     * values are returned by its iterator, this method must return the
+     * attribute values in the same order.
+     * <p>
+     * The returned array will be "safe" in that no references to it are
+     * maintained by this attribute. The caller is thus free to modify the
+     * returned array.
+     *
+     * @return An array containing all of the attribute values contained in this
+     *         attribute.
+     */
+    @Override
+    ByteString[] toArray();
+
+    /**
+     * Returns an array containing all of the attribute values in this
+     * attribute; the runtime type of the returned array is that of the
+     * specified array.
+     * <p>
+     * If the set fits in the specified array, it is returned therein.
+     * Otherwise, a new array is allocated with the runtime type of the
+     * specified array and the size of this attribute. If this attribute fits in
+     * the specified array with room to spare (i.e., the array has more elements
+     * than this attribute), the elements in the array immediately following the
+     * end of the set is set to {@code null}.
+     * <p>
+     * If this attribute makes any guarantees as to what order its attribute
+     * values are returned by its iterator, this method must return the
+     * attribute values in the same order.
+     *
+     * @param <T>
+     *            The type of elements contained in {@code array}.
+     * @param array
+     *            An array into which the elements of this attribute should be
+     *            put.
+     * @return An array containing all of the attribute values contained in this
+     *         attribute.
+     * @throws ArrayStoreException
+     *             If the runtime type of {@code array} is not a supertype of
+     *             {@code ByteString}.
+     * @throws NullPointerException
+     *             If {@code array} was {@code null}.
+     */
+    @Override
+    <T> T[] toArray(T[] array);
+
+    /**
+     * Returns a string representation of this attribute.
+     *
+     * @return The string representation of this attribute.
+     */
+    @Override
+    String toString();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
new file mode 100644
index 0000000..754c654
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
@@ -0,0 +1,1300 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.ASCIICharProp;
+import com.forgerock.opendj.util.Iterators;
+
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * An attribute description as defined in RFC 4512 section 2.5. Attribute
+ * descriptions are used to identify an attribute in an entry and are composed
+ * of an attribute type and a set of zero or more attribute options.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.5">RFC 4512 -
+ *      Lightweight Directory Access Protocol (LDAP): Directory Information
+ *      Models </a>
+ */
+public final class AttributeDescription implements Comparable<AttributeDescription> {
+    private static abstract class Impl implements Iterable<String> {
+        protected Impl() {
+            // Nothing to do.
+        }
+
+        public abstract int compareTo(Impl other);
+
+        public abstract boolean hasOption(String normalizedOption);
+
+        public abstract boolean equals(Impl other);
+
+        public abstract String firstNormalizedOption();
+
+        @Override
+        public abstract int hashCode();
+
+        public abstract boolean hasOptions();
+
+        public abstract boolean isSubTypeOf(Impl other);
+
+        public abstract boolean isSuperTypeOf(Impl other);
+
+        public abstract int size();
+    }
+
+    private static final class MultiOptionImpl extends Impl {
+
+        private final String[] normalizedOptions;
+        private final String[] options;
+
+        private MultiOptionImpl(final String[] options, final String[] normalizedOptions) {
+            if (normalizedOptions.length < 2) {
+                throw new AssertionError();
+            }
+
+            this.options = options;
+            this.normalizedOptions = normalizedOptions;
+        }
+
+        @Override
+        public int compareTo(final Impl other) {
+            final int thisSize = normalizedOptions.length;
+            final int otherSize = other.size();
+
+            if (thisSize < otherSize) {
+                return -1;
+            } else if (thisSize > otherSize) {
+                return 1;
+            } else {
+                // Same number of options.
+                final MultiOptionImpl otherImpl = (MultiOptionImpl) other;
+                for (int i = 0; i < thisSize; i++) {
+                    final String o1 = normalizedOptions[i];
+                    final String o2 = otherImpl.normalizedOptions[i];
+                    final int result = o1.compareTo(o2);
+                    if (result != 0) {
+                        return result;
+                    }
+                }
+
+                // All options the same.
+                return 0;
+            }
+        }
+
+        @Override
+        public boolean hasOption(final String normalizedOption) {
+            final int sz = normalizedOptions.length;
+            for (int i = 0; i < sz; i++) {
+                if (normalizedOptions[i].equals(normalizedOption)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean equals(final Impl other) {
+            if (other instanceof MultiOptionImpl) {
+                final MultiOptionImpl tmp = (MultiOptionImpl) other;
+                return Arrays.equals(normalizedOptions, tmp.normalizedOptions);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public String firstNormalizedOption() {
+            return normalizedOptions[0];
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(normalizedOptions);
+        }
+
+        @Override
+        public boolean hasOptions() {
+            return true;
+        }
+
+        @Override
+        public boolean isSubTypeOf(final Impl other) {
+            // Must contain a super-set of other's options.
+            if (other == ZERO_OPTION_IMPL) {
+                return true;
+            } else if (other.size() == 1) {
+                return hasOption(other.firstNormalizedOption());
+            } else if (other.size() > size()) {
+                return false;
+            } else {
+                // Check this contains other's options.
+                // This could be optimized more if required, but it's probably not worth it.
+                final MultiOptionImpl tmp = (MultiOptionImpl) other;
+                for (final String normalizedOption : tmp.normalizedOptions) {
+                    if (!hasOption(normalizedOption)) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+
+        @Override
+        public boolean isSuperTypeOf(final Impl other) {
+            // Must contain a sub-set of other's options.
+            for (final String normalizedOption : normalizedOptions) {
+                if (!other.hasOption(normalizedOption)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return Iterators.arrayIterator(options);
+        }
+
+        @Override
+        public int size() {
+            return normalizedOptions.length;
+        }
+
+    }
+
+    private static final class SingleOptionImpl extends Impl {
+
+        private final String normalizedOption;
+        private final String option;
+
+        private SingleOptionImpl(final String option, final String normalizedOption) {
+            this.option = option;
+            this.normalizedOption = normalizedOption;
+        }
+
+        @Override
+        public int compareTo(final Impl other) {
+            if (other == ZERO_OPTION_IMPL) {
+                // If other has zero options then this sorts after.
+                return 1;
+            } else if (other.size() == 1) {
+                // Same number of options, so compare.
+                return normalizedOption.compareTo(other.firstNormalizedOption());
+            } else {
+                // Other has more options, so comes after.
+                return -1;
+            }
+        }
+
+        @Override
+        public boolean hasOption(final String normalizedOption) {
+            return this.normalizedOption.equals(normalizedOption);
+        }
+
+        @Override
+        public boolean equals(final Impl other) {
+            return other.size() == 1 && other.hasOption(normalizedOption);
+        }
+
+        @Override
+        public String firstNormalizedOption() {
+            return normalizedOption;
+        }
+
+        @Override
+        public int hashCode() {
+            return normalizedOption.hashCode();
+        }
+
+        @Override
+        public boolean hasOptions() {
+            return true;
+        }
+
+        @Override
+        public boolean isSubTypeOf(final Impl other) {
+            // Other must have no options or the same option.
+            return other == ZERO_OPTION_IMPL || equals(other);
+        }
+
+        @Override
+        public boolean isSuperTypeOf(final Impl other) {
+            // Other must have this option.
+            return other.hasOption(normalizedOption);
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return Iterators.singletonIterator(option);
+        }
+
+        @Override
+        public int size() {
+            return 1;
+        }
+
+    }
+
+    private static final class ZeroOptionImpl extends Impl {
+        private ZeroOptionImpl() {
+            // Nothing to do.
+        }
+
+        @Override
+        public int compareTo(final Impl other) {
+            // If other has options then this sorts before.
+            return this == other ? 0 : -1;
+        }
+
+        @Override
+        public boolean hasOption(final String normalizedOption) {
+            return false;
+        }
+
+        @Override
+        public boolean equals(final Impl other) {
+            return this == other;
+        }
+
+        @Override
+        public String firstNormalizedOption() {
+            // No first option.
+            return null;
+        }
+
+        @Override
+        public int hashCode() {
+            // Use attribute type hash code.
+            return 0;
+        }
+
+        @Override
+        public boolean hasOptions() {
+            return false;
+        }
+
+        @Override
+        public boolean isSubTypeOf(final Impl other) {
+            // Can only be a sub-type if other has no options.
+            return this == other;
+        }
+
+        @Override
+        public boolean isSuperTypeOf(final Impl other) {
+            // Will always be a super-type.
+            return true;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return Iterators.emptyIterator();
+        }
+
+        @Override
+        public int size() {
+            return 0;
+        }
+
+    }
+
+    private static final ThreadLocal<WeakHashMap<Schema, Map<String, AttributeDescription>>> CACHE =
+            new ThreadLocal<WeakHashMap<Schema, Map<String, AttributeDescription>>>() {
+
+                @Override
+                protected WeakHashMap<Schema, Map<String, AttributeDescription>> initialValue() {
+                    return new WeakHashMap<>();
+                }
+            };
+
+    /** Object class attribute description. */
+    private static final ZeroOptionImpl ZERO_OPTION_IMPL = new ZeroOptionImpl();
+
+    private static final AttributeDescription OBJECT_CLASS;
+    static {
+        final AttributeType attributeType = Schema.getCoreSchema().getAttributeType("2.5.4.0");
+        final String attributeName = attributeType.getNameOrOID();
+        OBJECT_CLASS = new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL);
+    }
+
+    /**
+     * This is the size of the per-thread per-schema attribute description
+     * cache. We should be conservative here in case there are many
+     * threads.
+     */
+    private static final int ATTRIBUTE_DESCRIPTION_CACHE_SIZE = 512;
+
+    /**
+     * Returns an attribute description having the same attribute type and
+     * options as this attribute description as well as the provided option.
+     *
+     * @param option
+     *            The attribute option.
+     * @return The new attribute description containing {@code option}.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code option} was
+     *             {@code null}.
+     */
+    public AttributeDescription withOption(final String option) {
+        Reject.ifNull(option);
+
+        final String normalizedOption = toLowerCase(option);
+        if (optionsPimpl.hasOption(normalizedOption)) {
+            return this;
+        }
+
+        final String newAttributeDescription = appendOption(attributeDescription, option);
+
+        final Impl impl = optionsPimpl;
+        if (impl instanceof ZeroOptionImpl) {
+            return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                    new SingleOptionImpl(option, normalizedOption));
+        } else if (impl instanceof SingleOptionImpl) {
+            final SingleOptionImpl simpl = (SingleOptionImpl) impl;
+
+            final String[] newOptions = new String[2];
+            newOptions[0] = simpl.option;
+            newOptions[1] = option;
+
+            final String[] newNormalizedOptions = new String[2];
+            if (normalizedOption.compareTo(simpl.normalizedOption) < 0) {
+                newNormalizedOptions[0] = normalizedOption;
+                newNormalizedOptions[1] = simpl.normalizedOption;
+            } else {
+                newNormalizedOptions[0] = simpl.normalizedOption;
+                newNormalizedOptions[1] = normalizedOption;
+            }
+
+            return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                    new MultiOptionImpl(newOptions, newNormalizedOptions));
+        } else {
+            final MultiOptionImpl mimpl = (MultiOptionImpl) impl;
+
+            final int sz1 = mimpl.options.length;
+            final String[] newOptions = Arrays.copyOf(mimpl.options, sz1 + 1);
+            newOptions[sz1] = option;
+
+            final int sz2 = mimpl.normalizedOptions.length;
+            final String[] newNormalizedOptions = new String[sz2 + 1];
+            boolean inserted = false;
+            for (int i = 0; i < sz2; i++) {
+                if (!inserted) {
+                    final String s = mimpl.normalizedOptions[i];
+                    if (normalizedOption.compareTo(s) < 0) {
+                        newNormalizedOptions[i] = normalizedOption;
+                        newNormalizedOptions[i + 1] = s;
+                        inserted = true;
+                    } else {
+                        newNormalizedOptions[i] = s;
+                    }
+                } else {
+                    newNormalizedOptions[i + 1] = mimpl.normalizedOptions[i];
+                }
+            }
+
+            if (!inserted) {
+                newNormalizedOptions[sz2] = normalizedOption;
+            }
+
+            return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                    new MultiOptionImpl(newOptions, newNormalizedOptions));
+        }
+    }
+
+    /**
+     * Returns an attribute description having the same attribute type and
+     * options as this attribute description except for the provided option.
+     * <p>
+     * This method is idempotent: if this attribute description does not contain
+     * the provided option then this attribute description will be returned.
+     *
+     * @param option
+     *            The attribute option.
+     * @return The new attribute description excluding {@code option}.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code option} was
+     *             {@code null}.
+     */
+    public AttributeDescription withoutOption(final String option) {
+        Reject.ifNull(option);
+
+        final String normalizedOption = toLowerCase(option);
+        if (!optionsPimpl.hasOption(normalizedOption)) {
+            return this;
+        }
+
+        final String oldAttributeDescription = attributeDescription;
+        final StringBuilder builder =
+                new StringBuilder(oldAttributeDescription.length() - option.length() - 1);
+
+        final String normalizedOldAttributeDescription = toLowerCase(oldAttributeDescription);
+        final int index = normalizedOldAttributeDescription.indexOf(normalizedOption);
+        builder.append(oldAttributeDescription, 0, index - 1 /* to semi-colon */);
+        builder.append(oldAttributeDescription, index + option.length(), oldAttributeDescription
+                .length());
+        final String newAttributeDescription = builder.toString();
+
+        final Impl impl = optionsPimpl;
+        if (impl instanceof ZeroOptionImpl) {
+            throw new IllegalStateException("ZeroOptionImpl unexpected");
+        } else if (impl instanceof SingleOptionImpl) {
+            return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                    ZERO_OPTION_IMPL);
+        } else {
+            final MultiOptionImpl mimpl = (MultiOptionImpl) impl;
+            if (mimpl.options.length == 2) {
+                final String remainingOption;
+                final String remainingNormalizedOption;
+
+                if (toLowerCase(mimpl.options[0]).equals(normalizedOption)) {
+                    remainingOption = mimpl.options[1];
+                } else {
+                    remainingOption = mimpl.options[0];
+                }
+
+                if (mimpl.normalizedOptions[0].equals(normalizedOption)) {
+                    remainingNormalizedOption = mimpl.normalizedOptions[1];
+                } else {
+                    remainingNormalizedOption = mimpl.normalizedOptions[0];
+                }
+
+                return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                        new SingleOptionImpl(remainingOption, remainingNormalizedOption));
+            } else {
+                final String[] newOptions = new String[mimpl.options.length - 1];
+                final String[] newNormalizedOptions =
+                        new String[mimpl.normalizedOptions.length - 1];
+
+                for (int i = 0, j = 0; i < mimpl.options.length; i++) {
+                    if (!toLowerCase(mimpl.options[i]).equals(normalizedOption)) {
+                        newOptions[j++] = mimpl.options[i];
+                    }
+                }
+
+                for (int i = 0, j = 0; i < mimpl.normalizedOptions.length; i++) {
+                    if (!mimpl.normalizedOptions[i].equals(normalizedOption)) {
+                        newNormalizedOptions[j++] = mimpl.normalizedOptions[i];
+                    }
+                }
+
+                return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType,
+                        new MultiOptionImpl(newOptions, newNormalizedOptions));
+            }
+        }
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute type and no options.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} was {@code null}.
+     */
+    public static AttributeDescription create(final AttributeType attributeType) {
+        return create(attributeType.getNameOrOID(), attributeType);
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute name, type and no options.
+     *
+     * @param attributeName
+     *            The attribute name.
+     * @param attributeType
+     *            The attribute type.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} was {@code null}.
+     * @deprecated This method may be removed at any time
+     * @since OPENDJ-2803 Migrate Attribute
+     */
+    @Deprecated
+    public static AttributeDescription create(final String attributeName, final AttributeType attributeType) {
+        Reject.ifNull(attributeName, attributeType);
+
+        // Use object identity in case attribute type does not come from core schema.
+        if (attributeType == OBJECT_CLASS.getAttributeType()) {
+            return OBJECT_CLASS;
+        }
+        return new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL);
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute type and single option.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param option
+     *            The attribute option.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code option} was {@code null}.
+     */
+    public static AttributeDescription create(final AttributeType attributeType, final String option) {
+        return create(attributeType.getNameOrOID(), attributeType, option);
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute name, type and single option.
+     *
+     * @param attributeName
+     *            The attribute name.
+     * @param attributeType
+     *            The attribute type.
+     * @param option
+     *            The attribute option.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code option} was {@code null}.
+     * @deprecated This method may be removed at any time
+     * @since OPENDJ-2803 Migrate Attribute
+     */
+    @Deprecated
+    public static AttributeDescription create(
+            final String attributeName, final AttributeType attributeType, final String option) {
+        Reject.ifNull(attributeName, attributeType, option);
+
+        final String attributeDescription = appendOption(attributeName, option);
+        final String normalizedOption = toLowerCase(option);
+
+        return new AttributeDescription(attributeDescription, attributeName, attributeType,
+            new SingleOptionImpl(option, normalizedOption));
+    }
+
+    private static String appendOption(final String oid, final String option) {
+        final StringBuilder builder = new StringBuilder(oid.length() + option.length() + 1);
+        builder.append(oid);
+        builder.append(';');
+        builder.append(option);
+        return builder.toString();
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute name, type and options.
+     *
+     * @param attributeName
+     *            The attribute name.
+     * @param attributeType
+     *            The attribute type.
+     * @param options
+     *            The attribute options.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code options} was {@code null}.
+     * @deprecated This method may be removed at any time
+     * @since OPENDJ-2803 Migrate Attribute
+     */
+    @Deprecated
+    public static AttributeDescription create(
+            final String attributeName, final AttributeType attributeType, final String... options) {
+        Reject.ifNull(options);
+        return create(attributeName, attributeType, Arrays.asList(options));
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute type and options.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param options
+     *            The attribute options.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code options} was {@code null}.
+     */
+    public static AttributeDescription create(final AttributeType attributeType, final String... options) {
+        Reject.ifNull(options);
+        return create(attributeType.getNameOrOID(), attributeType, Arrays.asList(options));
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute type and options.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param options
+     *            The attribute options.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code options} was {@code null}.
+     */
+    public static AttributeDescription create(final AttributeType attributeType, final Collection<String> options) {
+        return create(attributeType.getNameOrOID(), attributeType, options);
+    }
+
+    /**
+     * Creates an attribute description having the provided attribute name, type and options.
+     *
+     * @param attributeName
+     *            The attribute name.
+     * @param attributeType
+     *            The attribute type.
+     * @param options
+     *            The attribute options.
+     * @return The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code options} was {@code null}.
+     * @deprecated This method may be removed at any time
+     * @since OPENDJ-2803 Migrate Attribute
+     */
+    @Deprecated
+    public static AttributeDescription create(
+            final String attributeName, final AttributeType attributeType, final Collection<String> options) {
+        Reject.ifNull(attributeName, attributeType);
+
+        final Collection<String> opts = options != null ? options : Collections.<String> emptySet();
+        switch (opts.size()) {
+        case 0:
+            return create(attributeName, attributeType);
+        case 1:
+            return create(attributeName, attributeType, opts.iterator().next());
+        default:
+            final String[] optionsList = new String[opts.size()];
+            final String[] normalizedOptions = new String[opts.size()];
+
+            final Iterator<String> it = opts.iterator();
+            final StringBuilder builder =
+                    new StringBuilder(attributeName.length() + it.next().length() + it.next().length() + 2);
+            builder.append(attributeName);
+
+            int i = 0;
+            for (final String option : opts) {
+                builder.append(';');
+                builder.append(option);
+                optionsList[i] = option;
+                normalizedOptions[i++] = toLowerCase(option);
+            }
+            Arrays.sort(normalizedOptions);
+
+            final String attributeDescription = builder.toString();
+            return new AttributeDescription(attributeDescription, attributeName, attributeType,
+                    new MultiOptionImpl(optionsList, normalizedOptions));
+        }
+    }
+
+    /**
+     * Returns an attribute description representing the object class attribute
+     * type with no options.
+     *
+     * @return The object class attribute description.
+     */
+    public static AttributeDescription objectClass() {
+        return OBJECT_CLASS;
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an attribute
+     * description using the default schema.
+     *
+     * @param attributeDescription
+     *            The LDAP string representation of an attribute description.
+     * @return The parsed attribute description.
+     * @throws UnknownSchemaElementException
+     *             If {@code attributeDescription} contains an attribute type
+     *             which is not contained in the default schema and the schema
+     *             is strict.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} is not a valid LDAP string
+     *             representation of an attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public static AttributeDescription valueOf(final String attributeDescription) {
+        return valueOf(attributeDescription, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an attribute
+     * description using the provided schema.
+     *
+     * @param attributeDescription
+     *            The LDAP string representation of an attribute description.
+     * @param schema
+     *            The schema to use when parsing the attribute description.
+     * @return The parsed attribute description.
+     * @throws UnknownSchemaElementException
+     *             If {@code attributeDescription} contains an attribute type
+     *             which is not contained in the provided schema and the schema
+     *             is strict.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} is not a valid LDAP string
+     *             representation of an attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code schema} was
+     *             {@code null}.
+     */
+    @SuppressWarnings("serial")
+    public static AttributeDescription valueOf(final String attributeDescription,
+            final Schema schema) {
+        Reject.ifNull(attributeDescription, schema);
+
+        // First look up the attribute description in the cache.
+        final WeakHashMap<Schema, Map<String, AttributeDescription>> threadLocalMap = CACHE.get();
+        Map<String, AttributeDescription> schemaLocalMap = threadLocalMap.get(schema);
+
+        AttributeDescription ad = null;
+        if (schemaLocalMap == null) {
+            schemaLocalMap =
+                    new LinkedHashMap<String, AttributeDescription>(
+                            ATTRIBUTE_DESCRIPTION_CACHE_SIZE, 0.75f, true) {
+                        @Override
+                        protected boolean removeEldestEntry(
+                                final Map.Entry<String, AttributeDescription> eldest) {
+                            return size() > ATTRIBUTE_DESCRIPTION_CACHE_SIZE;
+                        }
+                    };
+            threadLocalMap.put(schema, schemaLocalMap);
+        } else {
+            ad = schemaLocalMap.get(attributeDescription);
+        }
+
+        // Cache miss: decode and cache.
+        if (ad == null) {
+            ad = valueOf0(attributeDescription, schema);
+            schemaLocalMap.put(attributeDescription, ad);
+        }
+
+        return ad;
+    }
+
+    private static int skipTrailingWhiteSpace(final String attributeDescription, int i,
+            final int length) {
+        char c;
+        while (i < length) {
+            c = attributeDescription.charAt(i);
+            if (c != ' ') {
+                final LocalizableMessage message =
+                        ERR_ATTRIBUTE_DESCRIPTION_INTERNAL_WHITESPACE.get(attributeDescription);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+            i++;
+        }
+        return i;
+    }
+
+    /** Uncached valueOf implementation. */
+    private static AttributeDescription valueOf0(final String attributeDescription, final Schema schema) {
+        final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+        int i = 0;
+        final int length = attributeDescription.length();
+        char c = 0;
+
+        // Skip leading white space.
+        while (i < length) {
+            c = attributeDescription.charAt(i);
+            if (c != ' ') {
+                break;
+            }
+            i++;
+        }
+
+        // If we're already at the end then the attribute description only
+        // contained whitespace.
+        if (i == length) {
+            final LocalizableMessage message =
+                    ERR_ATTRIBUTE_DESCRIPTION_EMPTY.get(attributeDescription);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Validate the first non-whitespace character.
+        ASCIICharProp cp = ASCIICharProp.valueOf(c);
+        if (cp == null) {
+            final LocalizableMessage message =
+                    ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription, c, i);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Mark the attribute type start position.
+        final int attributeTypeStart = i;
+        if (cp.isLetter()) {
+            // Non-numeric OID: letter + zero or more keychars.
+            i++;
+            while (i < length) {
+                c = attributeDescription.charAt(i);
+                if (c == ';' || c == ' ') {
+                    break;
+                }
+
+                cp = ASCIICharProp.valueOf(c);
+                if (!cp.isKeyChar(allowMalformedNamesAndOptions)) {
+                    final LocalizableMessage message =
+                            ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription,
+                                    c, i);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+                i++;
+            }
+
+            // (charAt(i) == ';' || c == ' ' || i == length)
+        } else if (cp.isDigit()) {
+            // Numeric OID: decimal digit + zero or more dots or decimals.
+            i++;
+            while (i < length) {
+                c = attributeDescription.charAt(i);
+                if (c == ';' || c == ' ') {
+                    break;
+                }
+
+                cp = ASCIICharProp.valueOf(c);
+                if (c != '.' && !cp.isDigit()) {
+                    final LocalizableMessage message =
+                            ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription,
+                                    c, i);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+                i++;
+            }
+
+            // (charAt(i) == ';' || charAt(i) == ' ' || i == length)
+        } else {
+            final LocalizableMessage message =
+                    ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription, c, i);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Skip trailing white space.
+        final int attributeTypeEnd = i;
+        if (c == ' ') {
+            i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
+        }
+
+        // Determine the portion of the string containing the attribute type name.
+        String oid;
+        if (attributeTypeStart == 0 && attributeTypeEnd == length) {
+            oid = attributeDescription;
+        } else {
+            oid = attributeDescription.substring(attributeTypeStart, attributeTypeEnd);
+        }
+
+        if (oid.length() == 0) {
+            final LocalizableMessage message =
+                    ERR_ATTRIBUTE_DESCRIPTION_NO_TYPE.get(attributeDescription);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Get the attribute type from the schema.
+        final AttributeType attributeType = schema.getAttributeType(oid);
+
+        // If we're already at the end of the attribute description then it
+        // does not contain any options.
+        if (i == length) {
+            // Use object identity in case attribute type does not come from core schema.
+            if (attributeType == OBJECT_CLASS.getAttributeType()
+                    && attributeDescription.equals(OBJECT_CLASS.toString())) {
+                return OBJECT_CLASS;
+            }
+            return new AttributeDescription(attributeDescription, oid, attributeType, ZERO_OPTION_IMPL);
+        }
+
+        // At this point 'i' must point at a semi-colon.
+        i++;
+        StringBuilder builder = null;
+        int optionStart = i;
+        while (i < length) {
+            c = attributeDescription.charAt(i);
+            if (c == ' ' || c == ';') {
+                break;
+            }
+
+            cp = ASCIICharProp.valueOf(c);
+            if (!cp.isKeyChar(allowMalformedNamesAndOptions)) {
+                final LocalizableMessage message =
+                        ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription, c, i);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            if (builder == null) {
+                if (cp.isUpperCase()) {
+                    // Need to normalize the option.
+                    builder = new StringBuilder(length - optionStart);
+                    builder.append(attributeDescription, optionStart, i);
+                    builder.append(cp.toLowerCase());
+                }
+            } else {
+                builder.append(cp.toLowerCase());
+            }
+            i++;
+        }
+
+        String option = attributeDescription.substring(optionStart, i);
+        String normalizedOption;
+        if (builder != null) {
+            normalizedOption = builder.toString();
+        } else {
+            normalizedOption = option;
+        }
+
+        if (option.length() == 0) {
+            final LocalizableMessage message =
+                    ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Skip trailing white space.
+        if (c == ' ') {
+            i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
+        }
+
+        // If we're already at the end of the attribute description then it
+        // only contains a single option.
+        if (i == length) {
+            return new AttributeDescription(attributeDescription, oid, attributeType,
+                    new SingleOptionImpl(option, normalizedOption));
+        }
+
+        // Multiple options need sorting and duplicates removed - we could
+        // optimize a bit further here for 2 option attribute descriptions.
+        final List<String> options = new LinkedList<>();
+        options.add(option);
+
+        final SortedSet<String> normalizedOptions = new TreeSet<>();
+        normalizedOptions.add(normalizedOption);
+
+        while (i < length) {
+            // At this point 'i' must point at a semi-colon.
+            i++;
+            builder = null;
+            optionStart = i;
+            while (i < length) {
+                c = attributeDescription.charAt(i);
+                if (c == ' ' || c == ';') {
+                    break;
+                }
+
+                cp = ASCIICharProp.valueOf(c);
+                if (!cp.isKeyChar(allowMalformedNamesAndOptions)) {
+                    final LocalizableMessage message =
+                            ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription,
+                                    c, i);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                if (builder == null) {
+                    if (cp.isUpperCase()) {
+                        // Need to normalize the option.
+                        builder = new StringBuilder(length - optionStart);
+                        builder.append(attributeDescription, optionStart, i);
+                        builder.append(cp.toLowerCase());
+                    }
+                } else {
+                    builder.append(cp.toLowerCase());
+                }
+                i++;
+            }
+
+            option = attributeDescription.substring(optionStart, i);
+            if (builder != null) {
+                normalizedOption = builder.toString();
+            } else {
+                normalizedOption = option;
+            }
+
+            if (option.length() == 0) {
+                final LocalizableMessage message =
+                        ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip trailing white space.
+            if (c == ' ') {
+                i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
+            }
+
+            options.add(option);
+            normalizedOptions.add(normalizedOption);
+        }
+
+        return new AttributeDescription(attributeDescription, oid, attributeType,
+                new MultiOptionImpl(options.toArray(new String[options.size()]),
+                                    normalizedOptions.toArray(new String[normalizedOptions.size()])));
+    }
+
+    private final String attributeDescription;
+    private final String nameOrOid;
+    private final AttributeType attributeType;
+    private final Impl optionsPimpl;
+
+    /** Private constructor. */
+    private AttributeDescription(final String attributeDescription, final String attributeName,
+            final AttributeType attributeType, final Impl pimpl) {
+        this.attributeDescription = attributeDescription;
+        this.nameOrOid = attributeName;
+        this.attributeType = attributeType;
+        this.optionsPimpl = pimpl;
+    }
+
+    /**
+     * Compares this attribute description to the provided attribute
+     * description. The attribute types are compared first and then, if equal,
+     * the options are normalized, sorted, and compared.
+     *
+     * @param other
+     *            The attribute description to be compared.
+     * @return A negative integer, zero, or a positive integer as this attribute
+     *         description is less than, equal to, or greater than the specified
+     *         attribute description.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    @Override
+    public int compareTo(final AttributeDescription other) {
+        final int result = attributeType.compareTo(other.attributeType);
+        if (result != 0) {
+            return result;
+        } else {
+            // Attribute type is the same, so compare options.
+            return optionsPimpl.compareTo(other.optionsPimpl);
+        }
+    }
+
+    /**
+     * Indicates whether or not this attribute description contains the provided
+     * option.
+     *
+     * @param option
+     *            The option for which to make the determination.
+     * @return {@code true} if this attribute description has the provided
+     *         option, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code option} was {@code null}.
+     */
+    public boolean hasOption(final String option) {
+        final String normalizedOption = toLowerCase(option);
+        return optionsPimpl.hasOption(normalizedOption);
+    }
+
+    /**
+     * Indicates whether the provided object is an attribute description which
+     * is equal to this attribute description. It will be considered equal if
+     * the attribute types are {@link AttributeType#equals equal} and normalized
+     * sorted list of options are identical.
+     *
+     * @param o
+     *            The object for which to make the determination.
+     * @return {@code true} if the provided object is an attribute description
+     *         that is equal to this attribute description, or {@code false} if
+     *         not.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof AttributeDescription) {
+            final AttributeDescription other = (AttributeDescription) o;
+            return attributeType.equals(other.attributeType) && optionsPimpl.equals(other.optionsPimpl);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the attribute type associated with this attribute description.
+     *
+     * @return The attribute type associated with this attribute description.
+     */
+    public AttributeType getAttributeType() {
+        return attributeType;
+    }
+
+    /**
+     * Returns the attribute name or the oid provided by the user associated with this attribute
+     * description.
+     *
+     * @return The attribute name or the oid provided by the user associated with this attribute
+     *         description.
+     * @deprecated This method may be removed at any time
+     * @since OPENDJ-2803 Migrate Attribute
+     */
+    @Deprecated
+    public String getNameOrOID() {
+        return nameOrOid;
+    }
+
+    /**
+     * Returns an {@code Iterable} containing the options contained in this
+     * attribute description. Attempts to remove options using an iterator's
+     * {@code remove()} method are not permitted and will result in an
+     * {@code UnsupportedOperationException} being thrown.
+     *
+     * @return An {@code Iterable} containing the options.
+     */
+    public Iterable<String> getOptions() {
+        return optionsPimpl;
+    }
+
+    /**
+     * Returns the hash code for this attribute description. It will be
+     * calculated as the sum of the hash codes of the attribute type and
+     * normalized sorted list of options.
+     *
+     * @return The hash code for this attribute description.
+     */
+    @Override
+    public int hashCode() {
+        // FIXME: should we cache this?
+        return attributeType.hashCode() * 31 + optionsPimpl.hashCode();
+    }
+
+    /**
+     * Indicates whether or not this attribute description has any options.
+     *
+     * @return {@code true} if this attribute description has any options, or
+     *         {@code false} if not.
+     */
+    public boolean hasOptions() {
+        return optionsPimpl.hasOptions();
+    }
+
+    /**
+     * Indicates whether or not this attribute description is the
+     * {@code objectClass} attribute description with no options.
+     *
+     * @return {@code true} if this attribute description is the
+     *         {@code objectClass} attribute description with no options, or
+     *         {@code false} if not.
+     */
+    public boolean isObjectClass() {
+        return attributeType.isObjectClass() && !hasOptions();
+    }
+
+    /**
+     * Indicates whether this attribute description is a temporary place-holder
+     * allocated dynamically by a non-strict schema when no corresponding
+     * registered attribute type was found.
+     * <p>
+     * Place holder attribute descriptions have an attribute type whose OID is
+     * the normalized attribute name with the string {@code -oid} appended. In
+     * addition, they will use the directory string syntax and case ignore
+     * matching rule.
+     *
+     * @return {@code true} if this is a temporary place-holder attribute
+     *         description allocated dynamically by a non-strict schema when no
+     *         corresponding registered attribute type was found.
+     * @see Schema#getAttributeType(String)
+     * @see AttributeType#isPlaceHolder()
+     */
+    public boolean isPlaceHolder() {
+        return attributeType.isPlaceHolder();
+    }
+
+    /**
+     * Indicates whether or not this attribute description is a sub-type of the
+     * provided attribute description as defined in RFC 4512 section 2.5.
+     * Specifically, this method will return {@code true} if and only if the
+     * following conditions are both {@code true}:
+     * <ul>
+     * <li>This attribute description has an attribute type which
+     * {@link AttributeType#matches matches}, or is a sub-type of, the attribute
+     * type in the provided attribute description.
+     * <li>This attribute description contains all of the options contained in
+     * the provided attribute description.
+     * </ul>
+     * Note that this method will return {@code true} if this attribute
+     * description is equal to the provided attribute description.
+     *
+     * @param other
+     *            The attribute description for which to make the determination.
+     * @return {@code true} if this attribute description is a sub-type of the
+     *         provided attribute description, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public boolean isSubTypeOf(final AttributeDescription other) {
+        return attributeType.isSubTypeOf(other.attributeType)
+            && optionsPimpl.isSubTypeOf(other.optionsPimpl);
+    }
+
+    /**
+     * Indicates whether or not this attribute description is a super-type of
+     * the provided attribute description as defined in RFC 4512 section 2.5.
+     * Specifically, this method will return {@code true} if and only if the
+     * following conditions are both {@code true}:
+     * <ul>
+     * <li>This attribute description has an attribute type which
+     * {@link AttributeType#matches matches}, or is a super-type of, the
+     * attribute type in the provided attribute description.
+     * <li>This attribute description contains a sub-set of the options
+     * contained in the provided attribute description.
+     * </ul>
+     * Note that this method will return {@code true} if this attribute
+     * description is equal to the provided attribute description.
+     *
+     * @param other
+     *            The attribute description for which to make the determination.
+     * @return {@code true} if this attribute description is a super-type of the
+     *         provided attribute description, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public boolean isSuperTypeOf(final AttributeDescription other) {
+        return attributeType.isSuperTypeOf(other.attributeType)
+            && optionsPimpl.isSuperTypeOf(other.optionsPimpl);
+    }
+
+    /**
+     * Indicates whether the provided attribute description matches this
+     * attribute description. It will be considered a match if the attribute
+     * types {@link AttributeType#matches match} and the normalized sorted list
+     * of options are identical.
+     *
+     * @param other
+     *            The attribute description for which to make the determination.
+     * @return {@code true} if the provided attribute description matches this
+     *         attribute description, or {@code false} if not.
+     */
+    public boolean matches(final AttributeDescription other) {
+        if (this == other) {
+            return true;
+        } else {
+            return attributeType.matches(other.attributeType) && optionsPimpl.equals(other.optionsPimpl);
+        }
+    }
+
+    /**
+     * Returns the string representation of this attribute description as
+     * defined in RFC4512 section 2.5.
+     *
+     * @return The string representation of this attribute description.
+     */
+    @Override
+    public String toString() {
+        return attributeDescription;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFactory.java
new file mode 100644
index 0000000..5e61bef
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFactory.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * Attribute factories are included with a set of {@code DecodeOptions} in order
+ * to allow application to control how {@code Attribute} instances are created
+ * when decoding requests and responses.
+ *
+ * @see Attribute
+ * @see DecodeOptions
+ */
+public interface AttributeFactory {
+    /**
+     * Creates an attribute using the provided attribute description and no
+     * values.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @return The new attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Attribute newAttribute(AttributeDescription attributeDescription);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java
new file mode 100644
index 0000000..0fa2109
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java
@@ -0,0 +1,412 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.Attributes.renameAttribute;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+import com.forgerock.opendj.util.Iterables;
+
+/**
+ * A configurable factory for filtering the attributes exposed by an entry. An
+ * {@code AttributeFilter} is useful for performing fine-grained access control,
+ * selecting attributes based on search request criteria, and selecting
+ * attributes based on post- and pre- read request control criteria.
+ * <p>
+ * In cases where methods accept a string based list of attribute descriptions,
+ * the following special attribute descriptions are permitted:
+ * <ul>
+ * <li><b>*</b> - include all user attributes
+ * <li><b>+</b> - include all operational attributes
+ * <li><b>1.1</b> - exclude all attributes
+ * <li><b>@<i>objectclass</i></b> - include all attributes identified by the
+ * named object class.
+ * </ul>
+ */
+public final class AttributeFilter {
+    // TODO: exclude specific attributes, matched values, custom predicates, etc.
+    private boolean includeAllOperationalAttributes;
+    /** Depends on constructor. */
+    private boolean includeAllUserAttributes;
+    private boolean typesOnly;
+
+    /**
+     * Use a map so that we can perform membership checks as well as recover the
+     * user requested attribute description.
+     */
+    private Map<AttributeDescription, AttributeDescription> requestedAttributes = Collections
+            .emptyMap();
+
+    /**
+     * Creates a new attribute filter which will include all user attributes but
+     * no operational attributes.
+     */
+    public AttributeFilter() {
+        includeAllUserAttributes = true;
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the default schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     */
+    public AttributeFilter(final Collection<String> attributeDescriptions) {
+        this(attributeDescriptions, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the provided schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @param schema
+     *            The schema The schema to use when parsing attribute
+     *            descriptions and object class names.
+     */
+    public AttributeFilter(final Collection<String> attributeDescriptions, final Schema schema) {
+        if (attributeDescriptions == null || attributeDescriptions.isEmpty()) {
+            // Fast-path for common case.
+            includeAllUserAttributes = true;
+        } else {
+            for (final String attribute : attributeDescriptions) {
+                includeAttribute(attribute, schema);
+            }
+        }
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the default schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     */
+    public AttributeFilter(final String... attributeDescriptions) {
+        this(Arrays.asList(attributeDescriptions));
+    }
+
+    /**
+     * Returns a modifiable filtered copy of the provided entry.
+     *
+     * @param entry
+     *            The entry to be filtered and copied.
+     * @return The modifiable filtered copy of the provided entry.
+     */
+    public Entry filteredCopyOf(final Entry entry) {
+        return new LinkedHashMapEntry(filteredViewOf(entry));
+    }
+
+    /**
+     * Returns an unmodifiable filtered view of the provided entry. The returned
+     * entry supports all operations except those which modify the contents of
+     * the entry.
+     *
+     * @param entry
+     *            The entry to be filtered.
+     * @return The unmodifiable filtered view of the provided entry.
+     */
+    public Entry filteredViewOf(final Entry entry) {
+        return new AbstractEntry() {
+
+            @Override
+            public boolean addAttribute(final Attribute attribute,
+                    final Collection<? super ByteString> duplicateValues) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Entry clearAttributes() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Iterable<Attribute> getAllAttributes() {
+                /*
+                 * Unfortunately we cannot efficiently re-use the iterators in
+                 * {@code Iterators} because we need to transform and filter in
+                 * a single step. Transformation is required in order to ensure
+                 * that we return an attribute whose name is the same as the one
+                 * requested by the user.
+                 */
+                return new Iterable<Attribute>() {
+                    private boolean hasNextMustIterate = true;
+                    private final Iterator<Attribute> iterator = entry.getAllAttributes().iterator();
+                    private Attribute next = null;
+
+                    @Override
+                    public Iterator<Attribute> iterator() {
+                        return new Iterator<Attribute>() {
+                            @Override
+                            public boolean hasNext() {
+                                if (hasNextMustIterate) {
+                                    hasNextMustIterate = false;
+                                    while (iterator.hasNext()) {
+                                        final Attribute attribute = iterator.next();
+                                        final AttributeDescription ad = attribute.getAttributeDescription();
+                                        final AttributeType at = ad.getAttributeType();
+                                        final AttributeDescription requestedAd = requestedAttributes.get(ad);
+                                        if (requestedAd != null) {
+                                            next = renameAttribute(attribute, requestedAd);
+                                            return true;
+                                        } else if ((at.isOperational() && includeAllOperationalAttributes)
+                                                || (!at.isOperational() && includeAllUserAttributes)) {
+                                            next = attribute;
+                                            return true;
+                                        }
+                                    }
+                                    next = null;
+                                    return false;
+                                } else {
+                                    return next != null;
+                                }
+                            }
+
+                            @Override
+                            public Attribute next() {
+                                if (!hasNext()) {
+                                    throw new NoSuchElementException();
+                                }
+                                hasNextMustIterate = true;
+                                return filterAttribute(next);
+                            }
+
+                            @Override
+                            public void remove() {
+                                throw new UnsupportedOperationException();
+                            }
+                        };
+                    }
+
+                    @Override
+                    public String toString() {
+                        return Iterables.toString(this);
+                    }
+                };
+            }
+
+            @Override
+            public Attribute getAttribute(final AttributeDescription attributeDescription) {
+                /*
+                 * It is tempting to filter based on the passed in attribute
+                 * description, but we may get inaccurate results due to
+                 * placeholder attribute names.
+                 */
+                final Attribute attribute = entry.getAttribute(attributeDescription);
+                if (attribute != null) {
+                    final AttributeDescription ad = attribute.getAttributeDescription();
+                    final AttributeType at = ad.getAttributeType();
+                    final AttributeDescription requestedAd = requestedAttributes.get(ad);
+                    if (requestedAd != null) {
+                        return filterAttribute(renameAttribute(attribute, requestedAd));
+                    } else if ((at.isOperational() && includeAllOperationalAttributes)
+                            || (!at.isOperational() && includeAllUserAttributes)) {
+                        return filterAttribute(attribute);
+                    }
+                }
+                return null;
+            }
+
+            @Override
+            public int getAttributeCount() {
+                return Iterables.size(getAllAttributes());
+            }
+
+            @Override
+            public DN getName() {
+                return entry.getName();
+            }
+
+            @Override
+            public Entry setName(final DN dn) {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be included in
+     * filtered entries. By default operational attributes are not included.
+     *
+     * @param include
+     *            {@code true} if operational attributes should be included in
+     *            filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAllOperationalAttributes(final boolean include) {
+        this.includeAllOperationalAttributes = include;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be included in
+     * filtered entries. By default user attributes are included.
+     *
+     * @param include
+     *            {@code true} if user attributes should be included in filtered
+     *            entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAllUserAttributes(final boolean include) {
+        this.includeAllUserAttributes = include;
+        return this;
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final AttributeDescription attributeDescription) {
+        allocatedRequestedAttributes();
+        requestedAttributes.put(attributeDescription, attributeDescription);
+        return this;
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries. The attribute will be decoded using the default schema. See the
+     * class description for details regarding the types of supported attribute
+     * description.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final String attributeDescription) {
+        return includeAttribute(attributeDescription, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries. The attribute will be decoded using the provided schema. See the
+     * class description for details regarding the types of supported attribute
+     * description.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @param schema
+     *            The schema The schema to use when parsing attribute
+     *            descriptions and object class names.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final String attributeDescription, final Schema schema) {
+        if (attributeDescription.equals("*")) {
+            includeAllUserAttributes = true;
+        } else if (attributeDescription.equals("+")) {
+            includeAllOperationalAttributes = true;
+        } else if (attributeDescription.equals("1.1")) {
+            // Ignore - by default no attributes are included.
+        } else if (attributeDescription.startsWith("@") && attributeDescription.length() > 1) {
+            final String objectClassName = attributeDescription.substring(1);
+            final ObjectClass objectClass = schema.getObjectClass(objectClassName);
+            if (objectClass != null) {
+                allocatedRequestedAttributes();
+                for (final AttributeType at : objectClass.getRequiredAttributes()) {
+                    final AttributeDescription ad = AttributeDescription.create(at);
+                    requestedAttributes.put(ad, ad);
+                }
+                for (final AttributeType at : objectClass.getOptionalAttributes()) {
+                    final AttributeDescription ad = AttributeDescription.create(at);
+                    requestedAttributes.put(ad, ad);
+                }
+            }
+        } else {
+            allocatedRequestedAttributes();
+            final AttributeDescription ad =
+                    AttributeDescription.valueOf(attributeDescription, schema);
+            requestedAttributes.put(ad, ad);
+        }
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        if (!includeAllOperationalAttributes
+                && !includeAllUserAttributes
+                && requestedAttributes.isEmpty()) {
+            return "1.1";
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        if (includeAllUserAttributes) {
+            builder.append('*');
+        }
+        if (includeAllOperationalAttributes) {
+            if (builder.length() > 0) {
+                builder.append(", ");
+            }
+            builder.append('+');
+        }
+        for (final AttributeDescription requestedAttribute : requestedAttributes.keySet()) {
+            if (builder.length() > 0) {
+                builder.append(", ");
+            }
+            builder.append(requestedAttribute);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Specifies whether or not filtered attributes are to contain both
+     * attribute descriptions and values, or just attribute descriptions.
+     *
+     * @param typesOnly
+     *            {@code true} if only attribute descriptions (and not values)
+     *            are to be included, or {@code false} (the default) if both
+     *            attribute descriptions and values are to be included.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter typesOnly(final boolean typesOnly) {
+        this.typesOnly = typesOnly;
+        return this;
+    }
+
+    private void allocatedRequestedAttributes() {
+        if (requestedAttributes.isEmpty()) {
+            requestedAttributes = new HashMap<>();
+        }
+    }
+
+    private Attribute filterAttribute(final Attribute attribute) {
+        return typesOnly
+            ? Attributes.emptyAttribute(attribute.getAttributeDescription())
+            : attribute;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
new file mode 100644
index 0000000..e65e5b4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
@@ -0,0 +1,669 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import static com.forgerock.opendj.util.Collections2.*;
+
+/**
+ * A fluent API for parsing attributes as different types of object. An
+ * attribute parser is obtained from an entry using the method
+ * {@link Entry#parseAttribute} or from an attribute using
+ * {@link Attribute#parse}.
+ * <p>
+ * Methods throw an {@code IllegalArgumentException} when a value cannot be
+ * parsed (e.g. because its syntax is invalid). Methods which return a
+ * {@code Set} always return a modifiable non-{@code null} result, even if the
+ * attribute is {@code null} or empty.
+ * <p>
+ * Examples:
+ *
+ * <pre>
+ * Entry entry = ...;
+ *
+ * Calendar timestamp = entry.parseAttribute("createTimestamp").asCalendar();
+ * boolean isEnabled = entry.parseAttribute("enabled").asBoolean(false);
+ *
+ * Entry group = ...;
+ * Schema schema = ...;
+ *
+ * Set&lt;DN&gt; members = group.parseAttribute("member").usingSchema(schema).asSetOfDN();
+ * </pre>
+ *
+ * @see Entry#parseAttribute
+ * @see Attribute#parse
+ */
+public final class AttributeParser {
+    // TODO: enums, filters, rdns?
+
+    private static final AttributeParser NULL_INSTANCE = new AttributeParser(null);
+
+    /**
+     * Returns an attribute parser for the provided attribute. {@code null}
+     * attributes are permitted and will be treated as if an empty attribute was
+     * provided.
+     *
+     * @param attribute
+     *            The attribute to be parsed, which may be {@code null}.
+     * @return The attribute parser.
+     */
+    public static AttributeParser parseAttribute(final Attribute attribute) {
+        return isEmpty(attribute) ? NULL_INSTANCE : new AttributeParser(attribute);
+    }
+
+    private static boolean isEmpty(final Attribute attribute) {
+        return attribute == null || attribute.isEmpty();
+    }
+
+    private final Attribute attribute;
+    private Schema schema;
+
+    private AttributeParser(final Attribute attribute) {
+        this.attribute = attribute;
+    }
+
+    /**
+     * Returns the first value decoded as a {@code T} using the provided
+     * {@link Function}, or {@code null} if the attribute does not contain any
+     * values.
+     *
+     * @param <T>
+     *            The type of the value to be decoded.
+     * @param f
+     *            The function which should be used to decode the value.
+     * @return The first value decoded as a {@code T}.
+     */
+    public <T> T as(final Function<ByteString, ? extends T, NeverThrowsException> f) {
+        return as(f, null);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code T} using the provided
+     * {@link Function}, or {@code defaultValue} if the attribute does not
+     * contain any values.
+     *
+     * @param <T>
+     *            The type of the value to be decoded.
+     * @param f
+     *            The function which should be used to decode the value.
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code T}.
+     */
+    public <T> T as(final Function<ByteString, ? extends T, NeverThrowsException> f, final T defaultValue) {
+        if (!isEmpty(attribute)) {
+            return f.apply(attribute.firstValue());
+        } else {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Returns the first value decoded as an {@code AttributeDescription} using
+     * the schema associated with this parser, or {@code null} if the attribute
+     * does not contain any values.
+     *
+     * @return The first value decoded as an {@code AttributeDescription}.
+     */
+    public AttributeDescription asAttributeDescription() {
+        return asAttributeDescription((AttributeDescription) null);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code AttributeDescription} using
+     * the schema associated with this parser, or {@code defaultValue} if the
+     * attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code AttributeDescription}.
+     */
+    public AttributeDescription asAttributeDescription(final AttributeDescription defaultValue) {
+        return as(Functions.byteStringToAttributeDescription(getSchema()), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code AttributeDescription} using
+     * the schema associated with this parser, or {@code defaultValue} if the
+     * attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code AttributeDescription}.
+     */
+    public AttributeDescription asAttributeDescription(final String defaultValue) {
+        return asAttributeDescription(AttributeDescription.valueOf(defaultValue, getSchema()));
+    }
+
+    /**
+     * Returns the first value decoded as a boolean, or {@code null} if the
+     * attribute does not contain any values.
+     *
+     * @return The first value decoded as a boolean.
+     */
+    public Boolean asBoolean() {
+        return isEmpty(attribute) ? null : asBoolean(false /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Boolean}, or
+     * {@code defaultValue} if the attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code Boolean}.
+     */
+    public boolean asBoolean(final boolean defaultValue) {
+        return as(Functions.byteStringToBoolean(), defaultValue);
+    }
+
+    /**
+     * Returns the first value, or {@code null} if the attribute does not
+     * contain any values.
+     *
+     * @return The first value.
+     */
+    public ByteString asByteString() {
+        return asByteString(null);
+    }
+
+    /**
+     * Returns the first value, or {@code defaultValue} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value.
+     */
+    public ByteString asByteString(final ByteString defaultValue) {
+        return as(Functions.<ByteString> identityFunction(), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code DN} using the schema
+     * associated with this parser, or {@code null} if the attribute does not
+     * contain any values.
+     *
+     * @return The first value decoded as a {@code DN}.
+     */
+    public DN asDN() {
+        return asDN((DN) null);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code DN} using the schema
+     * associated with this parser, or {@code defaultValue} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code DN}.
+     */
+    public DN asDN(final DN defaultValue) {
+        return as(Functions.byteStringToDN(getSchema()), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code DN} using the schema
+     * associated with this parser, or {@code defaultValue} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code DN}.
+     */
+    public DN asDN(final String defaultValue) {
+        return asDN(DN.valueOf(defaultValue, getSchema()));
+    }
+
+    /**
+     * Returns the first value decoded as a {@code GeneralizedTime} using the
+     * generalized time syntax, or {@code null} if the attribute does not
+     * contain any values.
+     *
+     * @return The first value decoded as a {@code GeneralizedTime}.
+     */
+    public GeneralizedTime asGeneralizedTime() {
+        return asGeneralizedTime(null);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code GeneralizedTime} using the
+     * generalized time syntax, or {@code defaultValue} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code GeneralizedTime}.
+     */
+    public GeneralizedTime asGeneralizedTime(final GeneralizedTime defaultValue) {
+        return as(Functions.byteStringToGeneralizedTime(), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Integer}, or {@code null} if
+     * the attribute does not contain any values.
+     *
+     * @return The first value decoded as an {@code Integer}.
+     */
+    public Integer asInteger() {
+        return isEmpty(attribute) ? null : asInteger(0 /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Integer}, or
+     * {@code defaultValue} if the attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code Integer}.
+     */
+    public int asInteger(final int defaultValue) {
+        return as(Functions.byteStringToInteger(), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code Long}, or {@code null} if the
+     * attribute does not contain any values.
+     *
+     * @return The first value decoded as a {@code Long}.
+     */
+    public Long asLong() {
+        return isEmpty(attribute) ? null : asLong(0L /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code Long}, or
+     * {@code defaultValue} if the attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code Long}.
+     */
+    public long asLong(final long defaultValue) {
+        return as(Functions.byteStringToLong(), defaultValue);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code T}s using the provided
+     * {@link Function}, or {@code defaultValues} if the attribute does not
+     * contain any values.
+     *
+     * @param <T>
+     *            The type of the values to be decoded.
+     * @param f
+     *            The function which should be used to decode values.
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code T}s.
+     */
+    public <T> Set<T> asSetOf(final Function<ByteString, ? extends T, NeverThrowsException> f,
+            final Collection<? extends T> defaultValues) {
+        if (!isEmpty(attribute)) {
+            final LinkedHashSet<T> result = new LinkedHashSet<>(attribute.size());
+            for (final ByteString b : attribute) {
+                result.add(f.apply(b));
+            }
+            return result;
+        } else if (defaultValues != null) {
+            return new LinkedHashSet<>(defaultValues);
+        } else {
+            return new LinkedHashSet<>(0);
+        }
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code T}s using the provided
+     * {@link Function}, or {@code defaultValues} if the attribute does not
+     * contain any values.
+     *
+     * @param <T>
+     *            The type of the values to be decoded.
+     * @param f
+     *            The function which should be used to decode values.
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code T}s.
+     */
+    @SafeVarargs
+    @SuppressWarnings("varargs")
+    public final <T> Set<T> asSetOf(final Function<ByteString, ? extends T, NeverThrowsException> f,
+            final T... defaultValues) {
+        return asSetOf(f, Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or an empty set if the
+     * attribute does not contain any values.
+     *
+     * @return The values decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription() {
+        return asSetOfAttributeDescription(Collections.<AttributeDescription> emptySet());
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or {@code defaultValues} if
+     * the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription(
+            final AttributeDescription... defaultValues) {
+        return asSetOfAttributeDescription(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or {@code defaultValues} if
+     * the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription(
+            final Collection<AttributeDescription> defaultValues) {
+        return asSetOf(Functions.byteStringToAttributeDescription(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or {@code defaultValues} if
+     * the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription(final String... defaultValues) {
+        return asSetOfAttributeDescription(transformedCollection(Arrays.asList(defaultValues),
+                Functions.stringToAttributeDescription(getSchema()), null));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Boolean}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Boolean}s.
+     */
+    public Set<Boolean> asSetOfBoolean(final Boolean... defaultValues) {
+        return asSetOfBoolean(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Boolean}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Boolean}s.
+     */
+    public Set<Boolean> asSetOfBoolean(final Collection<Boolean> defaultValues) {
+        return asSetOf(Functions.byteStringToBoolean(), defaultValues);
+    }
+
+    /**
+     * Returns the values contained in the attribute, or {@code defaultValues}
+     * if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values contained in the attribute.
+     */
+    public Set<ByteString> asSetOfByteString(final ByteString... defaultValues) {
+        return asSetOfByteString(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values contained in the attribute, or {@code defaultValues}
+     * if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values contained in the attribute.
+     */
+    public Set<ByteString> asSetOfByteString(final Collection<ByteString> defaultValues) {
+        return asSetOf(Functions.<ByteString> identityFunction(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or an empty set if the attribute does not
+     * contain any values.
+     *
+     * @return The values decoded as a set of {@code DN}s.
+     */
+    public Set<DN> asSetOfDN() {
+        return asSetOfDN(Collections.<DN> emptySet());
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or {@code defaultValues} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code DN}s.
+     */
+    public Set<DN> asSetOfDN(final Collection<DN> defaultValues) {
+        return asSetOf(Functions.byteStringToDN(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or {@code defaultValues} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code DN}s.
+     */
+    public Set<DN> asSetOfDN(final DN... defaultValues) {
+        return asSetOfDN(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or {@code defaultValues} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code DN}s.
+     */
+    public Set<DN> asSetOfDN(final String... defaultValues) {
+        return asSetOfDN(transformedCollection(Arrays.asList(defaultValues), Functions
+                .stringToDN(getSchema()), null));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code GeneralizedTime}s using the
+     * generalized time syntax, or {@code defaultValues} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code GeneralizedTime}s.
+     */
+    public Set<GeneralizedTime> asSetOfGeneralizedTime(
+            final Collection<GeneralizedTime> defaultValues) {
+        return asSetOf(Functions.byteStringToGeneralizedTime(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code GeneralizedTime}s using the
+     * generalized time syntax, or {@code defaultValues} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code GeneralizedTime}s.
+     */
+    public Set<GeneralizedTime> asSetOfGeneralizedTime(final GeneralizedTime... defaultValues) {
+        return asSetOfGeneralizedTime(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Integer}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Integer}s.
+     */
+    public Set<Integer> asSetOfInteger(final Collection<Integer> defaultValues) {
+        return asSetOf(Functions.byteStringToInteger(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Integer}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Integer}s.
+     */
+    public Set<Integer> asSetOfInteger(final Integer... defaultValues) {
+        return asSetOfInteger(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Long}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Long}s.
+     */
+    public Set<Long> asSetOfLong(final Collection<Long> defaultValues) {
+        return asSetOf(Functions.byteStringToLong(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Long}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Long}s.
+     */
+    public Set<Long> asSetOfLong(final Long... defaultValues) {
+        return asSetOfLong(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code String}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code String}s.
+     */
+    public Set<String> asSetOfString(final Collection<String> defaultValues) {
+        return asSetOf(Functions.byteStringToString(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code String}s, or
+     * {@code defaultValues} if the attribute does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code String}s.
+     */
+    public Set<String> asSetOfString(final String... defaultValues) {
+        return asSetOfString(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the first value decoded as a {@code String}, or {@code null} if
+     * the attribute does not contain any values.
+     *
+     * @return The first value decoded as a {@code String}.
+     */
+    public String asString() {
+        return asString(null);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code String}, or
+     * {@code defaultValue} if the attribute does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code String}.
+     */
+    public String asString(final String defaultValue) {
+        return as(Functions.byteStringToString(), defaultValue);
+    }
+
+    /**
+     * Throws a {@code NoSuchElementException} if the attribute referenced by
+     * this parser is {@code null} or empty.
+     *
+     * @return A reference to this attribute parser.
+     * @throws NoSuchElementException
+     *             If the attribute referenced by this parser is {@code null} or
+     *             empty.
+     */
+    public AttributeParser requireValue() {
+        if (isEmpty(attribute)) {
+            throw new NoSuchElementException();
+        } else {
+            return this;
+        }
+    }
+
+    /**
+     * Sets the {@code Schema} which will be used when parsing schema sensitive
+     * values such as DNs and attribute descriptions.
+     *
+     * @param schema
+     *            The {@code Schema} which will be used when parsing schema
+     *            sensitive values.
+     * @return This attribute parser.
+     */
+    public AttributeParser usingSchema(final Schema schema) {
+        // Avoid modifying the null instance: a schema will not be needed
+        // anyway.
+        if (this != NULL_INSTANCE) {
+            this.schema = schema;
+        }
+        return this;
+    }
+
+    private Schema getSchema() {
+        return schema == null ? Schema.getDefaultSchema() : schema;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attributes.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attributes.java
new file mode 100644
index 0000000..022d367
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Attributes.java
@@ -0,0 +1,592 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.Iterators;
+
+/**
+ * This class contains methods for creating and manipulating attributes.
+ */
+public final class Attributes {
+
+    /**
+     * Empty attribute.
+     */
+    private static final class EmptyAttribute extends AbstractAttribute {
+
+        private final AttributeDescription attributeDescription;
+
+        private EmptyAttribute(final AttributeDescription attributeDescription) {
+            this.attributeDescription = attributeDescription;
+        }
+
+        @Override
+        public boolean add(final ByteString value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean contains(final Object value) {
+            return false;
+        }
+
+        @Override
+        public AttributeDescription getAttributeDescription() {
+            return attributeDescription;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+
+        @Override
+        public Iterator<ByteString> iterator() {
+            return Iterators.emptyIterator();
+        }
+
+        @Override
+        public boolean remove(final Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int size() {
+            return 0;
+        }
+
+    }
+
+    /**
+     * Renamed attribute.
+     */
+    private static final class RenamedAttribute implements Attribute {
+
+        private final Attribute attribute;
+        private final AttributeDescription attributeDescription;
+
+        private RenamedAttribute(final Attribute attribute,
+                final AttributeDescription attributeDescription) {
+            this.attribute = attribute;
+            this.attributeDescription = attributeDescription;
+        }
+
+        @Override
+        public boolean add(final ByteString value) {
+            return attribute.add(value);
+        }
+
+        @Override
+        public boolean add(final Object... values) {
+            return attribute.add(values);
+        }
+
+        @Override
+        public boolean addAll(final Collection<? extends ByteString> values) {
+            return attribute.addAll(values);
+        }
+
+        @Override
+        public <T> boolean addAll(final Collection<T> values,
+                final Collection<? super T> duplicateValues) {
+            return attribute.addAll(values, duplicateValues);
+        }
+
+        @Override
+        public void clear() {
+            attribute.clear();
+        }
+
+        @Override
+        public boolean contains(final Object value) {
+            return attribute.contains(value);
+        }
+
+        @Override
+        public boolean containsAll(final Collection<?> values) {
+            return attribute.containsAll(values);
+        }
+
+        @Override
+        public boolean equals(final Object object) {
+            return AbstractAttribute.equals(this, object);
+        }
+
+        @Override
+        public ByteString firstValue() {
+            return attribute.firstValue();
+        }
+
+        @Override
+        public String firstValueAsString() {
+            return attribute.firstValueAsString();
+        }
+
+        @Override
+        public AttributeDescription getAttributeDescription() {
+            return attributeDescription;
+        }
+
+        @Override
+        public String getAttributeDescriptionAsString() {
+            return attributeDescription.toString();
+        }
+
+        @Override
+        public int hashCode() {
+            return AbstractAttribute.hashCode(this);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return attribute.isEmpty();
+        }
+
+        @Override
+        public Iterator<ByteString> iterator() {
+            return attribute.iterator();
+        }
+
+        @Override
+        public AttributeParser parse() {
+            return attribute.parse();
+        }
+
+        @Override
+        public boolean remove(final Object value) {
+            return attribute.remove(value);
+        }
+
+        @Override
+        public boolean removeAll(final Collection<?> values) {
+            return attribute.removeAll(values);
+        }
+
+        @Override
+        public <T> boolean removeAll(final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            return attribute.removeAll(values, missingValues);
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> values) {
+            return attribute.retainAll(values);
+        }
+
+        @Override
+        public <T> boolean retainAll(final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            return attribute.retainAll(values, missingValues);
+        }
+
+        @Override
+        public int size() {
+            return attribute.size();
+        }
+
+        @Override
+        public ByteString[] toArray() {
+            return attribute.toArray();
+        }
+
+        @Override
+        public <T> T[] toArray(final T[] array) {
+            return attribute.toArray(array);
+        }
+
+        @Override
+        public String toString() {
+            return AbstractAttribute.toString(this);
+        }
+
+    }
+
+    /**
+     * Singleton attribute.
+     */
+    private static final class SingletonAttribute extends AbstractAttribute {
+
+        private final AttributeDescription attributeDescription;
+        private ByteString normalizedValue;
+        private final ByteString value;
+
+        private SingletonAttribute(final AttributeDescription attributeDescription,
+                final Object value) {
+            this.attributeDescription = attributeDescription;
+            this.value = ByteString.valueOfObject(value);
+        }
+
+        @Override
+        public boolean add(final ByteString value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean contains(final Object value) {
+            final ByteString normalizedValue = normalizeValue(this, ByteString.valueOfObject(value));
+            return normalizedSingleValue().equals(normalizedValue);
+        }
+
+        @Override
+        public AttributeDescription getAttributeDescription() {
+            return attributeDescription;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public Iterator<ByteString> iterator() {
+            return Iterators.singletonIterator(value);
+        }
+
+        @Override
+        public boolean remove(final Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int size() {
+            return 1;
+        }
+
+        /** Lazily computes the normalized single value. */
+        private ByteString normalizedSingleValue() {
+            if (normalizedValue == null) {
+                normalizedValue = normalizeValue(this, value);
+            }
+            return normalizedValue;
+        }
+
+    }
+
+    /**
+     * Unmodifiable attribute.
+     */
+    private static final class UnmodifiableAttribute implements Attribute {
+
+        private final Attribute attribute;
+
+        private UnmodifiableAttribute(final Attribute attribute) {
+            this.attribute = attribute;
+        }
+
+        @Override
+        public boolean add(final ByteString value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean add(final Object... values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean addAll(final Collection<? extends ByteString> values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> boolean addAll(final Collection<T> values,
+                final Collection<? super T> duplicateValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean contains(final Object value) {
+            return attribute.contains(value);
+        }
+
+        @Override
+        public boolean containsAll(final Collection<?> values) {
+            return attribute.containsAll(values);
+        }
+
+        @Override
+        public boolean equals(final Object object) {
+            return object == this || attribute.equals(object);
+        }
+
+        @Override
+        public ByteString firstValue() {
+            return attribute.firstValue();
+        }
+
+        @Override
+        public String firstValueAsString() {
+            return attribute.firstValueAsString();
+        }
+
+        @Override
+        public AttributeDescription getAttributeDescription() {
+            return attribute.getAttributeDescription();
+        }
+
+        @Override
+        public String getAttributeDescriptionAsString() {
+            return attribute.getAttributeDescriptionAsString();
+        }
+
+        @Override
+        public int hashCode() {
+            return attribute.hashCode();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return attribute.isEmpty();
+        }
+
+        @Override
+        public Iterator<ByteString> iterator() {
+            return Iterators.unmodifiableIterator(attribute.iterator());
+        }
+
+        @Override
+        public AttributeParser parse() {
+            return attribute.parse();
+        }
+
+        @Override
+        public boolean remove(final Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean removeAll(final Collection<?> values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> boolean removeAll(final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T> boolean retainAll(final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int size() {
+            return attribute.size();
+        }
+
+        @Override
+        public ByteString[] toArray() {
+            return attribute.toArray();
+        }
+
+        @Override
+        public <T> T[] toArray(final T[] array) {
+            return attribute.toArray(array);
+        }
+
+        @Override
+        public String toString() {
+            return attribute.toString();
+        }
+    }
+
+    /**
+     * Returns a read-only empty attribute having the specified attribute
+     * description. Attempts to modify the returned attribute either directly,
+     * or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @return The empty attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public static Attribute emptyAttribute(final AttributeDescription attributeDescription) {
+        return new EmptyAttribute(attributeDescription);
+    }
+
+    /**
+     * Returns a read-only empty attribute having the specified attribute
+     * description. The attribute description will be decoded using the default
+     * schema. Attempts to modify the returned attribute either directly, or
+     * indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @return The empty attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public static Attribute emptyAttribute(final String attributeDescription) {
+        return emptyAttribute(AttributeDescription.valueOf(attributeDescription));
+    }
+
+    /**
+     * Returns a view of {@code attribute} having a different attribute
+     * description. All operations on the returned attribute "pass-through" to
+     * the underlying attribute.
+     *
+     * @param attribute
+     *            The attribute to be renamed.
+     * @param attributeDescription
+     *            The new attribute description for {@code attribute}.
+     * @return A renamed view of {@code attribute}.
+     * @throws NullPointerException
+     *             If {@code attribute} or {@code attributeDescription} was
+     *             {@code null}.
+     */
+    public static Attribute renameAttribute(final Attribute attribute,
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(attribute, attributeDescription);
+
+        // Optimize for the case where no renaming is required.
+        if (attribute.getAttributeDescription() == attributeDescription) {
+            return attribute;
+        } else {
+            return new RenamedAttribute(attribute, attributeDescription);
+        }
+    }
+
+    /**
+     * Returns a view of {@code attribute} having a different attribute
+     * description. All operations on the returned attribute "pass-through" to
+     * the underlying attribute. The attribute description will be decoded using
+     * the default schema.
+     *
+     * @param attribute
+     *            The attribute to be renamed.
+     * @param attributeDescription
+     *            The new attribute description for {@code attribute}.
+     * @return A renamed view of {@code attribute}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attribute} or {@code attributeDescription} was
+     *             {@code null}.
+     */
+    public static Attribute renameAttribute(final Attribute attribute, final String attributeDescription) {
+        Reject.ifNull(attribute, attributeDescription);
+        return renameAttribute(attribute, AttributeDescription.valueOf(attributeDescription));
+    }
+
+    /**
+     * Returns a read-only single-valued attribute having the specified
+     * attribute description and value. Attempts to modify the returned
+     * attribute either directly, or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @return The single-valued attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null}.
+     */
+    public static Attribute singletonAttribute(final AttributeDescription attributeDescription, final Object value) {
+        return new SingletonAttribute(attributeDescription, value);
+    }
+
+    /**
+     * Returns a read-only single-valued attribute having the specified
+     * attribute description. The attribute description will be decoded using
+     * the default schema. Attempts to modify the returned attribute either
+     * directly, or indirectly via an iterator, result in an
+     * {@code UnsupportedOperationException}.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @return The single-valued attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null}.
+     */
+    public static Attribute singletonAttribute(final String attributeDescription, final Object value) {
+        return singletonAttribute(AttributeDescription.valueOf(attributeDescription), value);
+    }
+
+    /**
+     * Returns a read-only view of {@code attribute}. Query operations on the
+     * returned attribute "read-through" to the underlying attribute, and
+     * attempts to modify the returned attribute either directly or indirectly
+     * via an iterator result in an {@code UnsupportedOperationException}.
+     *
+     * @param attribute
+     *            The attribute for which a read-only view is to be returned.
+     * @return A read-only view of {@code attribute}.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    public static Attribute unmodifiableAttribute(final Attribute attribute) {
+        if (attribute instanceof UnmodifiableAttribute) {
+            return attribute;
+        }
+        return new UnmodifiableAttribute(attribute);
+    }
+
+    /** Prevent instantiation. */
+    private Attributes() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticationException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticationException.java
new file mode 100644
index 0000000..a167208
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticationException.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Bind
+ * Request failed due to an authentication failure. More specifically, this
+ * exception is used for the following error result codes:
+ * <ul>
+ * <li>{@link ResultCode#AUTH_METHOD_NOT_SUPPORTED AUTH_METHOD_NOT_SUPPORTED} -
+ * the Bind request failed because it referenced an invalid SASL mechanism.
+ * <li>{@link ResultCode#CLIENT_SIDE_AUTH_UNKNOWN CLIENT_SIDE_AUTH_UNKNOWN} -
+ * the Bind request failed because the user requested an authentication
+ * mechanism which is unknown or unsupported by the OpenDJ SDK.
+ * <li>{@link ResultCode#INAPPROPRIATE_AUTHENTICATION
+ * INAPPROPRIATE_AUTHENTICATION} - the Bind request failed because the requested
+ * type of authentication was not appropriate for the targeted entry.
+ * <li>{@link ResultCode#INVALID_CREDENTIALS INVALID_CREDENTIALS} - the Bind
+ * request failed because the user did not provide a valid set of credentials.
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class AuthenticationException extends LdapException {
+    AuthenticationException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthorizationException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthorizationException.java
new file mode 100644
index 0000000..d4cd073
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthorizationException.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * failed due to an authorization failure. More specifically, this exception is
+ * used for the following error result codes:
+ * <ul>
+ * <li>{@link ResultCode#AUTHORIZATION_DENIED AUTHORIZATION_DENIED} - the
+ * Request failed because the server has not allowed the client to use the
+ * requested authorization.
+ * <li>{@link ResultCode#CONFIDENTIALITY_REQUIRED CONFIDENTIALITY_REQUIRED} -
+ * the Request failed because it requires confidentiality for the communication
+ * between the client and the server.
+ * <li>{@link ResultCode#INSUFFICIENT_ACCESS_RIGHTS INSUFFICIENT_ACCESS_RIGHTS}
+ * - the Request failed because the client does not have sufficient permission
+ * to perform the requested operation.
+ * <li>{@link ResultCode#STRONG_AUTH_REQUIRED STRONG_AUTH_REQUIRED} - the
+ * Request failed because it requires that the client has completed a strong
+ * form of authentication.
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class AuthorizationException extends LdapException {
+    AuthorizationException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Base64.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Base64.java
new file mode 100644
index 0000000..93f5e5a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Base64.java
@@ -0,0 +1,356 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.Reject;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_BASE64_DECODE_INVALID_CHARACTER;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_BASE64_DECODE_INVALID_LENGTH;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+/**
+ * This class provides methods for performing base64 encoding and decoding.
+ * Base64 is a mechanism for encoding binary data in ASCII form by converting
+ * sets of three bytes with eight significant bits each to sets of four bytes
+ * with six significant bits each.
+ * <p>
+ * <b>NOTE:</b> the JDK class {@link javax.xml.bind.DatatypeConverter} provides
+ * similar functionality, however the methods are better suited to the LDAP SDK.
+ * For example, the JDK encoder does not handle array/offset/len parameters, and
+ * the decoder ignores invalid Base64 data.
+ */
+final class Base64 {
+    /**
+     * The set of characters that may be used in base64-encoded values.
+     */
+    private static final char[] BASE64_ALPHABET = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/").toCharArray();
+
+    /**
+     * Decodes the provided base64 encoded data.
+     *
+     * @param base64
+     *            The base64 encoded data.
+     * @return The decoded data.
+     * @throws LocalizedIllegalArgumentException
+     *             If a problem occurs while attempting to decode {@code base64}
+     *             .
+     * @throws NullPointerException
+     *             If {@code base64} was {@code null}.
+     */
+    static ByteString decode(final String base64) {
+        Reject.ifNull(base64);
+
+        // The encoded value must have length that is a multiple of four
+        // bytes.
+        final int length = base64.length();
+        if (length % 4 != 0) {
+            final LocalizableMessage message = ERR_BASE64_DECODE_INVALID_LENGTH.get(base64);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        final ByteStringBuilder builder = new ByteStringBuilder(length);
+        for (int i = 0; i < length; i += 4) {
+            boolean append = true;
+            int value = 0;
+
+            for (int j = 0; j < 4; j++) {
+                switch (base64.charAt(i + j)) {
+                case 'A':
+                    value <<= 6;
+                    break;
+                case 'B':
+                    value = (value << 6) | 0x01;
+                    break;
+                case 'C':
+                    value = (value << 6) | 0x02;
+                    break;
+                case 'D':
+                    value = (value << 6) | 0x03;
+                    break;
+                case 'E':
+                    value = (value << 6) | 0x04;
+                    break;
+                case 'F':
+                    value = (value << 6) | 0x05;
+                    break;
+                case 'G':
+                    value = (value << 6) | 0x06;
+                    break;
+                case 'H':
+                    value = (value << 6) | 0x07;
+                    break;
+                case 'I':
+                    value = (value << 6) | 0x08;
+                    break;
+                case 'J':
+                    value = (value << 6) | 0x09;
+                    break;
+                case 'K':
+                    value = (value << 6) | 0x0A;
+                    break;
+                case 'L':
+                    value = (value << 6) | 0x0B;
+                    break;
+                case 'M':
+                    value = (value << 6) | 0x0C;
+                    break;
+                case 'N':
+                    value = (value << 6) | 0x0D;
+                    break;
+                case 'O':
+                    value = (value << 6) | 0x0E;
+                    break;
+                case 'P':
+                    value = (value << 6) | 0x0F;
+                    break;
+                case 'Q':
+                    value = (value << 6) | 0x10;
+                    break;
+                case 'R':
+                    value = (value << 6) | 0x11;
+                    break;
+                case 'S':
+                    value = (value << 6) | 0x12;
+                    break;
+                case 'T':
+                    value = (value << 6) | 0x13;
+                    break;
+                case 'U':
+                    value = (value << 6) | 0x14;
+                    break;
+                case 'V':
+                    value = (value << 6) | 0x15;
+                    break;
+                case 'W':
+                    value = (value << 6) | 0x16;
+                    break;
+                case 'X':
+                    value = (value << 6) | 0x17;
+                    break;
+                case 'Y':
+                    value = (value << 6) | 0x18;
+                    break;
+                case 'Z':
+                    value = (value << 6) | 0x19;
+                    break;
+                case 'a':
+                    value = (value << 6) | 0x1A;
+                    break;
+                case 'b':
+                    value = (value << 6) | 0x1B;
+                    break;
+                case 'c':
+                    value = (value << 6) | 0x1C;
+                    break;
+                case 'd':
+                    value = (value << 6) | 0x1D;
+                    break;
+                case 'e':
+                    value = (value << 6) | 0x1E;
+                    break;
+                case 'f':
+                    value = (value << 6) | 0x1F;
+                    break;
+                case 'g':
+                    value = (value << 6) | 0x20;
+                    break;
+                case 'h':
+                    value = (value << 6) | 0x21;
+                    break;
+                case 'i':
+                    value = (value << 6) | 0x22;
+                    break;
+                case 'j':
+                    value = (value << 6) | 0x23;
+                    break;
+                case 'k':
+                    value = (value << 6) | 0x24;
+                    break;
+                case 'l':
+                    value = (value << 6) | 0x25;
+                    break;
+                case 'm':
+                    value = (value << 6) | 0x26;
+                    break;
+                case 'n':
+                    value = (value << 6) | 0x27;
+                    break;
+                case 'o':
+                    value = (value << 6) | 0x28;
+                    break;
+                case 'p':
+                    value = (value << 6) | 0x29;
+                    break;
+                case 'q':
+                    value = (value << 6) | 0x2A;
+                    break;
+                case 'r':
+                    value = (value << 6) | 0x2B;
+                    break;
+                case 's':
+                    value = (value << 6) | 0x2C;
+                    break;
+                case 't':
+                    value = (value << 6) | 0x2D;
+                    break;
+                case 'u':
+                    value = (value << 6) | 0x2E;
+                    break;
+                case 'v':
+                    value = (value << 6) | 0x2F;
+                    break;
+                case 'w':
+                    value = (value << 6) | 0x30;
+                    break;
+                case 'x':
+                    value = (value << 6) | 0x31;
+                    break;
+                case 'y':
+                    value = (value << 6) | 0x32;
+                    break;
+                case 'z':
+                    value = (value << 6) | 0x33;
+                    break;
+                case '0':
+                    value = (value << 6) | 0x34;
+                    break;
+                case '1':
+                    value = (value << 6) | 0x35;
+                    break;
+                case '2':
+                    value = (value << 6) | 0x36;
+                    break;
+                case '3':
+                    value = (value << 6) | 0x37;
+                    break;
+                case '4':
+                    value = (value << 6) | 0x38;
+                    break;
+                case '5':
+                    value = (value << 6) | 0x39;
+                    break;
+                case '6':
+                    value = (value << 6) | 0x3A;
+                    break;
+                case '7':
+                    value = (value << 6) | 0x3B;
+                    break;
+                case '8':
+                    value = (value << 6) | 0x3C;
+                    break;
+                case '9':
+                    value = (value << 6) | 0x3D;
+                    break;
+                case '+':
+                    value = (value << 6) | 0x3E;
+                    break;
+                case '/':
+                    value = (value << 6) | 0x3F;
+                    break;
+                case '=':
+                    append = false;
+                    switch (j) {
+                    case 2:
+                        builder.appendByte(value >>> 4);
+                        break;
+                    case 3:
+                        builder.appendByte(value >>> 10);
+                        builder.appendByte(value >>> 2);
+                        break;
+                    }
+                    break;
+                default:
+                    final LocalizableMessage message =
+                            ERR_BASE64_DECODE_INVALID_CHARACTER.get(base64, base64.charAt(i + j));
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                if (!append) {
+                    break;
+                }
+            }
+
+            if (append) {
+                builder.appendByte(value >>> 16);
+                builder.appendByte(value >>> 8);
+                builder.appendByte(value);
+            } else {
+                break;
+            }
+        }
+
+        return builder.toByteString();
+    }
+
+    /**
+     * Encodes the provided data as a base64 string.
+     *
+     * @param bytes
+     *            The data to be encoded.
+     * @return The base64 encoded representation of {@code bytes}.
+     * @throws NullPointerException
+     *             If {@code bytes} was {@code null}.
+     */
+    static String encode(final ByteSequence bytes) {
+        Reject.ifNull(bytes);
+
+        if (bytes.isEmpty()) {
+            return "";
+        }
+
+        final StringBuilder buffer = new StringBuilder(4 * bytes.length() / 3);
+
+        int pos = 0;
+        final int iterations = bytes.length() / 3;
+        for (int i = 0; i < iterations; i++) {
+            final int value =
+                    ((bytes.byteAt(pos++) & 0xFF) << 16) | ((bytes.byteAt(pos++) & 0xFF) << 8)
+                            | (bytes.byteAt(pos++) & 0xFF);
+
+            buffer.append(BASE64_ALPHABET[(value >>> 18) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[(value >>> 12) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[(value >>> 6) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[value & 0x3F]);
+        }
+
+        switch (bytes.length() % 3) {
+        case 1:
+            buffer.append(BASE64_ALPHABET[(bytes.byteAt(pos) >>> 2) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[(bytes.byteAt(pos) << 4) & 0x3F]);
+            buffer.append("==");
+            break;
+        case 2:
+            final int value = ((bytes.byteAt(pos++) & 0xFF) << 8) | (bytes.byteAt(pos) & 0xFF);
+            buffer.append(BASE64_ALPHABET[(value >>> 10) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[(value >>> 4) & 0x3F]);
+            buffer.append(BASE64_ALPHABET[(value << 2) & 0x3F]);
+            buffer.append("=");
+            break;
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Prevent instance creation.
+     */
+    private Base64() {
+        // No implementation required.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java
new file mode 100644
index 0000000..a8fa3a9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequence.java
@@ -0,0 +1,366 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.util.Comparator;
+
+/**
+ * A {@code ByteSequence} is a readable sequence of byte values. This interface
+ * provides uniform, read-only access to many different kinds of byte sequences.
+ */
+public interface ByteSequence extends Comparable<ByteSequence> {
+
+    /** A byte array comparator. */
+    Comparator<byte[]> BYTE_ARRAY_COMPARATOR = new Comparator<byte[]>() {
+        @Override
+        public int compare(final byte[] b1, final byte[] b2) {
+            return ByteString.compareTo(b1, 0, b1.length, b2, 0, b2.length);
+        }
+    };
+
+    /** A ByteSequence comparator. */
+    Comparator<ByteSequence> COMPARATOR = new Comparator<ByteSequence>() {
+        @Override
+        public int compare(final ByteSequence o1, final ByteSequence o2) {
+            return o1.compareTo(o2);
+        }
+    };
+
+    /**
+     * Returns a {@link ByteSequenceReader} which can be used to incrementally
+     * read and decode data from this byte sequence.
+     * <p>
+     * <b>NOTE:</b> any concurrent changes to the underlying byte sequence (if
+     * mutable) may cause subsequent reads to overrun and fail.
+     *
+     * @return The {@link ByteSequenceReader} which can be used to incrementally
+     *         read and decode data from this byte sequence.
+     */
+    ByteSequenceReader asReader();
+
+    /**
+     * Returns the byte value at the specified index.
+     * <p>
+     * An index ranges from zero to {@code length() - 1}. The first byte value
+     * of the sequence is at index zero, the next at index one, and so on, as
+     * for array indexing.
+     *
+     * @param index
+     *            The index of the byte to be returned.
+     * @return The byte value at the specified index.
+     * @throws IndexOutOfBoundsException
+     *             If the index argument is negative or not less than length().
+     */
+    byte byteAt(int index);
+
+    /**
+     * Compares this byte sequence with the specified byte array sub-sequence
+     * for order. Returns a negative integer, zero, or a positive integer
+     * depending on whether this byte sequence is less than, equal to, or
+     * greater than the specified byte array sub-sequence.
+     *
+     * @param bytes
+     *            The byte array to compare.
+     * @param offset
+     *            The offset of the sub-sequence in the byte array to be
+     *            compared; must be non-negative and no larger than
+     *            {@code bytes.length} .
+     * @param length
+     *            The length of the sub-sequence in the byte array to be
+     *            compared; must be non-negative and no larger than
+     *            {@code bytes.length - offset}.
+     * @return A negative integer, zero, or a positive integer depending on
+     *         whether this byte sequence is less than, equal to, or greater
+     *         than the specified byte array sub-sequence.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative or if {@code length} is
+     *             negative or if {@code offset + length} is greater than
+     *             {@code bytes.length}.
+     */
+    int compareTo(byte[] bytes, int offset, int length);
+
+    /**
+     * Compares this byte sequence with the specified byte sequence for order.
+     * Returns a negative integer, zero, or a positive integer depending on
+     * whether this byte sequence is less than, equal to, or greater than the
+     * specified object.
+     *
+     * @param o
+     *            The byte sequence to be compared.
+     * @return A negative integer, zero, or a positive integer depending on
+     *         whether this byte sequence is less than, equal to, or greater
+     *         than the specified object.
+     */
+    @Override
+    int compareTo(ByteSequence o);
+
+    /**
+     * Copies the contents of this byte sequence to the provided byte array.
+     * <p>
+     * Copying will stop when either the entire content of this sequence has
+     * been copied or if the end of the provided byte array has been reached.
+     * <p>
+     * An invocation of the form:
+     *
+     * <pre>
+     * src.copyTo(bytes)
+     * </pre>
+     *
+     * Behaves in exactly the same way as the invocation:
+     *
+     * <pre>
+     * src.copyTo(bytes, 0);
+     * </pre>
+     *
+     * @param bytes
+     *            The byte array to which bytes are to be copied.
+     * @return The byte array.
+     */
+    byte[] copyTo(byte[] bytes);
+
+    /**
+     * Copies the contents of this byte sequence to the specified location in
+     * the provided byte array.
+     * <p>
+     * Copying will stop when either the entire content of this sequence has
+     * been copied or if the end of the provided byte array has been reached.
+     * <p>
+     * An invocation of the form:
+     *
+     * <pre>
+     * src.copyTo(bytes, offset)
+     * </pre>
+     *
+     * Behaves in exactly the same way as the invocation:
+     *
+     * <pre>
+     * int len = Math.min(src.length(), bytes.length - offset);
+     * for (int i = 0; i &lt; len; i++)
+     *     bytes[offset + i] = src.get(i);
+     * </pre>
+     *
+     * Except that it is potentially much more efficient.
+     *
+     * @param bytes
+     *            The byte array to which bytes are to be copied.
+     * @param offset
+     *            The offset within the array of the first byte to be written;
+     *            must be non-negative and no larger than bytes.length.
+     * @return The byte array.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative.
+     */
+    byte[] copyTo(byte[] bytes, int offset);
+
+    /**
+     * Appends the entire contents of this byte sequence to the provided
+     * {@link ByteStringBuilder}.
+     *
+     * @param builder
+     *            The builder to copy to.
+     * @return The builder.
+     */
+    ByteStringBuilder copyTo(ByteStringBuilder builder);
+
+    /**
+     * Appends the content of this byte sequence to the provided {@link ByteBuffer} starting at it's current position.
+     * The position of the buffer is then incremented by the length of this sequence.
+     *
+     * @param buffer
+     *            The buffer to copy to.
+     *            It must be large enough to receive all bytes.
+     * @return The buffer.
+     * @throws BufferOverflowException
+     *            If there is insufficient space in the provided buffer
+     */
+    ByteBuffer copyTo(ByteBuffer buffer);
+
+    /**
+     * Appends the content of this byte sequence decoded using provided charset decoder to the provided
+     * {@link CharBuffer} starting at it's current position. The position of charBuffer is then incremented by the
+     * length of this sequence.
+     *
+     * @param charBuffer
+     *            The buffer to copy to, if decoding is successful.
+     *            It must be large enough to receive all decoded characters.
+     * @param decoder
+     *            The charset decoder to use for decoding.
+     * @return {@code true} if byte string was successfully decoded and charBuffer is
+     *         large enough to receive the resulting string, {@code false} otherwise
+     */
+    boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder);
+
+    /**
+     * Copies the entire contents of this byte sequence to the provided
+     * {@code OutputStream}.
+     *
+     * @param stream
+     *            The {@code OutputStream} to copy to.
+     * @return The {@code OutputStream}.
+     * @throws IOException
+     *             If an error occurs while writing to the {@code OutputStream}.
+     */
+    OutputStream copyTo(OutputStream stream) throws IOException;
+
+    /**
+     * Indicates whether the provided byte array sub-sequence is equal to this
+     * byte sequence. In order for it to be considered equal, the provided byte
+     * array sub-sequence must contain the same bytes in the same order.
+     *
+     * @param bytes
+     *            The byte array for which to make the determination.
+     * @param offset
+     *            The offset of the sub-sequence in the byte array to be
+     *            compared; must be non-negative and no larger than
+     *            {@code bytes.length} .
+     * @param length
+     *            The length of the sub-sequence in the byte array to be
+     *            compared; must be non-negative and no larger than
+     *            {@code bytes.length - offset}.
+     * @return {@code true} if the content of the provided byte array
+     *         sub-sequence is equal to that of this byte sequence, or
+     *         {@code false} if not.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative or if {@code length} is
+     *             negative or if {@code offset + length} is greater than
+     *             {@code bytes.length}.
+     */
+    boolean equals(byte[] bytes, int offset, int length);
+
+    /**
+     * Indicates whether the provided object is equal to this byte sequence. In
+     * order for it to be considered equal, the provided object must be a byte
+     * sequence containing the same bytes in the same order.
+     *
+     * @param o
+     *            The object for which to make the determination.
+     * @return {@code true} if the provided object is a byte sequence whose
+     *         content is equal to that of this byte sequence, or {@code false}
+     *         if not.
+     */
+    @Override
+    boolean equals(Object o);
+
+    /**
+     * Returns a hash code for this byte sequence. It will be the sum of all of
+     * the bytes contained in the byte sequence.
+     *
+     * @return A hash code for this byte sequence.
+     */
+    @Override
+    int hashCode();
+
+    /**
+     * Returns {@code true} if this byte sequence has a length of zero.
+     *
+     * @return {@code true} if this byte sequence has a length of zero.
+     */
+    boolean isEmpty();
+
+    /**
+     * Returns the length of this byte sequence.
+     *
+     * @return The length of this byte sequence.
+     */
+    int length();
+
+    /**
+     * Returns a new byte sequence that is a subsequence of this byte sequence.
+     * <p>
+     * The subsequence starts with the byte value at the specified {@code start}
+     * index and ends with the byte value at index {@code end - 1}. The length
+     * (in bytes) of the returned sequence is {@code end - start}, so if
+     * {@code start
+     * == end} then an empty sequence is returned.
+     * <p>
+     * <b>NOTE:</b> changes to the underlying byte sequence (if mutable) may
+     * render the returned sub-sequence invalid.
+     *
+     * @param start
+     *            The start index, inclusive.
+     * @param end
+     *            The end index, exclusive.
+     * @return The newly created byte subsequence.
+     * @throws IndexOutOfBoundsException
+     *             If {@code start} or {@code end} are negative, if {@code end}
+     *             is greater than {@code length()}, or if {@code start} is
+     *             greater than {@code end}.
+     */
+    ByteSequence subSequence(int start, int end);
+
+    /**
+     * Tests if this ByteSequence starts with the specified prefix.
+     *
+     * @param prefix
+     *            The prefix.
+     * @return true if the byte sequence represented by the argument is a prefix of the byte sequence represented by
+     *         this ByteSequence; false otherwise. Note also that true will be returned if the argument is an empty
+     *         sequence or is equal to this ByteSequence object as determined by the equals(Object) method.
+     */
+    boolean startsWith(ByteSequence prefix);
+
+    /**
+     * Returns the Base64 encoded string representation of this byte string.
+     *
+     * @return The Base64 encoded string representation of this byte string.
+     * @see ByteString#valueOfBase64(String)
+     */
+    String toBase64String();
+
+    /**
+     * Returns a byte array containing the bytes in this sequence in the same
+     * order as this sequence. The length of the byte array will be the length
+     * of this sequence.
+     * <p>
+     * An invocation of the form:
+     *
+     * <pre>
+     * src.toByteArray()
+     * </pre>
+     *
+     * Behaves in exactly the same way as the invocation:
+     *
+     * <pre>
+     * src.copyTo(new byte[src.length()]);
+     * </pre>
+     *
+     * @return A byte array consisting of exactly this sequence of bytes.
+     */
+    byte[] toByteArray();
+
+    /**
+     * Returns the {@link ByteString} representation of this byte sequence.
+     *
+     * @return The {@link ByteString} representation of this byte sequence.
+     */
+    ByteString toByteString();
+
+    /**
+     * Returns the UTF-8 decoded string representation of this byte sequence. If
+     * UTF-8 decoding fails, the platform's default encoding will be used.
+     *
+     * @return The string representation of this byte sequence.
+     */
+    @Override
+    String toString();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequenceReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequenceReader.java
new file mode 100644
index 0000000..3cb9d89
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteSequenceReader.java
@@ -0,0 +1,523 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.forgerock.opendj.util.PackedLong;
+
+/**
+ * An interface for iteratively reading data from a {@link ByteSequence} .
+ * {@code ByteSequenceReader} must be created using the associated
+ * {@code ByteSequence}'s {@code asReader()} method.
+ */
+public final class ByteSequenceReader {
+
+    /** The current position in the byte sequence. */
+    private int pos;
+
+    /** The underlying byte sequence. */
+    private final ByteSequence sequence;
+
+    /**
+     * The lazily allocated input stream view of this reader. Synchronization is not necessary because the stream is
+     * stateless and race conditions can be tolerated.
+     */
+    private InputStream inputStream;
+
+    /**
+     * Creates a new byte sequence reader whose source is the provided byte
+     * sequence.
+     * <p>
+     * <b>NOTE:</b> any concurrent changes to the underlying byte sequence (if
+     * mutable) may cause subsequent reads to overrun and fail.
+     * <p>
+     * This constructor is package private: construction must be performed using
+     * {@link ByteSequence#asReader()}.
+     *
+     * @param sequence
+     *            The byte sequence to be read.
+     */
+    ByteSequenceReader(final ByteSequence sequence) {
+        this.sequence = sequence;
+    }
+
+    /**
+     * Relative read method. Reads the byte at the current position.
+     *
+     * @return The byte at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < 1}.
+     */
+    public byte readByte() {
+        final byte b = sequence.byteAt(pos);
+        pos++;
+        return b;
+    }
+
+    /**
+     * Relative bulk read method. This method transfers bytes from this reader
+     * into the given destination array. An invocation of this method of the
+     * form:
+     *
+     * <pre>
+     * src.readBytes(b);
+     * </pre>
+     *
+     * Behaves in exactly the same way as the invocation:
+     *
+     * <pre>
+     * src.readBytes(b, 0, b.length);
+     * </pre>
+     *
+     * @param b
+     *            The byte array into which bytes are to be written.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < b.length}.
+     */
+    public void readBytes(final byte[] b) {
+        readBytes(b, 0, b.length);
+    }
+
+    /**
+     * Relative bulk read method. Copies {@code length} bytes from this reader
+     * into the given array, starting at the current position of this reader and
+     * at the given {@code offset} in the array. The position of this reader is
+     * then incremented by {@code length}. In other words, an invocation of this
+     * method of the form:
+     *
+     * <pre>
+     * src.read(b, offset, length);
+     * </pre>
+     *
+     * Has exactly the same effect as the loop:
+     *
+     * <pre>
+     * for (int i = offset; i &lt; offset + length; i++)
+     *     b[i] = src.readByte();
+     * </pre>
+     *
+     * Except that it first checks that there are sufficient bytes in this
+     * buffer and it is potentially much more efficient.
+     *
+     * @param b
+     *            The byte array into which bytes are to be written.
+     * @param offset
+     *            The offset within the array of the first byte to be written;
+     *            must be non-negative and no larger than {@code b.length}.
+     * @param length
+     *            The number of bytes to be written to the given array; must be
+     *            non-negative and no larger than {@code b.length} .
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < length}.
+     */
+    public void readBytes(final byte[] b, final int offset, final int length) {
+        if (offset < 0 || length < 0 || offset + length > b.length || length > remaining()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        sequence.subSequence(pos, pos + length).copyTo(b, offset);
+        pos += length;
+    }
+
+    /**
+     * Relative read method for reading a multi-byte BER length. Reads the next
+     * one to five bytes at this reader's current position, composing them into
+     * a integer value and then increments the position by the number of bytes
+     * read.
+     *
+     * @return The integer value representing the length at this reader's
+     *         current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request.
+     */
+    public int readBERLength() {
+        // Make sure we have at least one byte to read.
+        int newPos = pos + 1;
+        if (newPos > sequence.length()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        int length = sequence.byteAt(pos) & 0x7F;
+        if (length != sequence.byteAt(pos)) {
+            // Its a multi-byte length
+            final int numLengthBytes = length;
+            newPos = pos + 1 + numLengthBytes;
+            // Make sure we have the bytes needed
+            if (numLengthBytes > 4 || newPos > sequence.length()) {
+                // Shouldn't have more than 4 bytes
+                throw new IndexOutOfBoundsException();
+            }
+
+            length = 0x00;
+            for (int i = pos + 1; i < newPos; i++) {
+                length = length << 8 | sequence.byteAt(i) & 0xFF;
+            }
+        }
+
+        pos = newPos;
+        return length;
+    }
+
+    /**
+     * Relative bulk read method. Returns a {@link ByteSequence} whose content is
+     * the next {@code length} bytes from this reader, starting at the current
+     * position of this reader. The position of this reader is then incremented
+     * by {@code length}.
+     * <p>
+     * <b>NOTE:</b> The value returned from this method should NEVER be cached
+     * as it prevents the contents of the underlying byte stream from being
+     * garbage collected.
+     *
+     * @param length
+     *            The length of the byte sequence to be returned.
+     * @return The byte sequence whose content is the next {@code length} bytes
+     *         from this reader.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < length}.
+     */
+    public ByteSequence readByteSequence(final int length) {
+        final int newPos = pos + length;
+        final ByteSequence subSequence = sequence.subSequence(pos, newPos);
+        pos = newPos;
+        return subSequence;
+    }
+
+    /**
+     * Relative bulk read method. Returns a {@link ByteString} whose content is
+     * the next {@code length} bytes from this reader, starting at the current
+     * position of this reader. The position of this reader is then incremented
+     * by {@code length}.
+     * <p>
+     * An invocation of this method of the form:
+     *
+     * <pre>
+     * src.readByteString(length);
+     * </pre>
+     *
+     * Has exactly the same effect as:
+     *
+     * <pre>
+     * src.readByteSequence(length).toByteString();
+     * </pre>
+     *
+     * <b>NOTE:</b> The value returned from this method should NEVER be cached
+     * as it prevents the contents of the underlying byte stream from being
+     * garbage collected.
+     *
+     * @param length
+     *            The length of the byte string to be returned.
+     * @return The byte string whose content is the next {@code length} bytes
+     *         from this reader.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < length}.
+     */
+    public ByteString readByteString(final int length) {
+        return readByteSequence(length).toByteString();
+    }
+
+    /**
+     * Relative read method for reading an integer value. Reads the next four
+     * bytes at this reader's current position, composing them into an integer
+     * value according to big-endian byte order, and then increments the
+     * position by four.
+     *
+     * @return The integer value at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < 4}.
+     */
+    public int readInt() {
+        if (remaining() < 4) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        int v = 0;
+        for (int i = 0; i < 4; i++) {
+            v <<= 8;
+            v |= sequence.byteAt(pos++) & 0xFF;
+        }
+
+        return v;
+    }
+
+    /**
+     * Relative read method for reading a long value. Reads the next eight bytes
+     * at this reader's current position, composing them into a long value
+     * according to big-endian byte order, and then increments the position by
+     * eight.
+     *
+     * @return The long value at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < 8}.
+     */
+    public long readLong() {
+        if (remaining() < 8) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        long v = 0;
+        for (int i = 0; i < 8; i++) {
+            v <<= 8;
+            v |= sequence.byteAt(pos++) & 0xFF;
+        }
+
+        return v;
+    }
+
+    /**
+     * Relative read method for reading a compacted long value.
+     * Compaction allows to reduce number of bytes needed to hold long types
+     * depending on its value (i.e: if value < 128, value will be encoded using one byte only).
+     * Reads the next bytes at this reader's current position, composing them into a long value
+     * according to big-endian byte order, and then increments the position by the size of the
+     * encoded long.
+     * Note that the maximum value of a compact long is 2^56.
+     *
+     * @return The long value at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request.
+     */
+    public long readCompactUnsignedLong() {
+        try {
+            return PackedLong.readCompactUnsignedLong(asInputStream());
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Relative read method for reading a compacted int value.
+     * Compaction allows to reduce number of bytes needed to hold int types
+     * depending on its value (i.e: if value < 128, value will be encoded using one byte only).
+     * Reads the next bytes at this reader's current position, composing them into an int value
+     * according to big-endian byte order, and then increments the position by the size of the
+     * encoded int.
+     *
+     * @return The int value at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request.
+     */
+    public int readCompactUnsignedInt() {
+        long l = readCompactUnsignedLong();
+        if (l > Integer.MAX_VALUE) {
+            throw new IllegalStateException(ERR_INVALID_COMPACTED_UNSIGNED_INT.get(Integer.MAX_VALUE, l).toString());
+        }
+        return (int) l;
+    }
+
+    /**
+     * Relative read method for reading an short value. Reads the next 2 bytes at
+     * this reader's current position, composing them into an short value
+     * according to big-endian byte order, and then increments the position by
+     * two.
+     *
+     * @return The integer value at this reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < 2}.
+     */
+    public short readShort() {
+        if (remaining() < 2) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        short v = 0;
+        for (int i = 0; i < 2; i++) {
+            v <<= 8;
+            v |= sequence.byteAt(pos++) & 0xFF;
+        }
+
+        return v;
+    }
+
+    /**
+     * Relative read method for reading a UTF-8 encoded string. Reads the next
+     * number of specified bytes at this reader's current position, decoding
+     * them into a string using UTF-8 and then increments the position by the
+     * number of bytes read. If UTF-8 decoding fails, the platform's default
+     * encoding will be used.
+     *
+     * @param length
+     *            The number of bytes to read and decode.
+     * @return The string value at the reader's current position.
+     * @throws IndexOutOfBoundsException
+     *             If there are fewer bytes remaining in this reader than are
+     *             required to satisfy the request, that is, if
+     *             {@code remaining() < length}.
+     */
+    public String readStringUtf8(final int length) {
+        if (remaining() < length) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        final int newPos = pos + length;
+        final String str = sequence.subSequence(pos, pos + length).toString();
+        pos = newPos;
+        return str;
+    }
+
+    /**
+     * Returns this reader's position.
+     *
+     * @return The position of this reader.
+     */
+    public int position() {
+        return pos;
+    }
+
+    /**
+     * Sets this reader's position.
+     *
+     * @param pos
+     *            The new position value; must be non-negative and no larger
+     *            than the length of the underlying byte sequence.
+     * @throws IndexOutOfBoundsException
+     *             If the position is negative or larger than the length of the
+     *             underlying byte sequence.
+     */
+    public void position(final int pos) {
+        if (pos > sequence.length() || pos < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        this.pos = pos;
+    }
+
+    /**
+     * Returns the number of bytes between the current position and the end of
+     * the underlying byte sequence.
+     *
+     * @return The number of bytes between the current position and the end of
+     *         the underlying byte sequence.
+     */
+    public int remaining() {
+        return sequence.length() - pos;
+    }
+
+    /**
+     * Rewinds this reader's position to zero.
+     * <p>
+     * An invocation of this method of the form:
+     *
+     * <pre>
+     * src.rewind();
+     * </pre>
+     *
+     * Has exactly the same effect as:
+     *
+     * <pre>
+     * src.position(0);
+     * </pre>
+     */
+    public void rewind() {
+        position(0);
+    }
+
+    /**
+     * Returns the byte situated at the current position. The byte is not
+     * consumed.
+     *
+     * @return the byte situated at the current position
+     * @throws IndexOutOfBoundsException
+     *           If the position is negative or larger than the length of the
+     *           underlying byte sequence.
+     */
+    public byte peek() {
+        return sequence.byteAt(pos);
+    }
+
+    /**
+     * Returns the byte situated at the given offset from current position. The
+     * byte is not consumed.
+     *
+     * @param offset
+     *          The offset where to look at from current position.
+     * @return the byte situated at the given offset from current position
+     * @throws IndexOutOfBoundsException
+     *           If the position is negative or larger than the length of the
+     *           underlying byte sequence.
+     */
+    public byte peek(int offset) {
+        return sequence.byteAt(pos + offset);
+    }
+
+    /**
+     * Skips the given number of bytes. Negative values are allowed.
+     * <p>
+     * An invocation of this method of the form:
+     *
+     * <pre>
+     * src.skip(length);
+     * </pre>
+     *
+     * Has exactly the same effect as:
+     *
+     * <pre>
+     * src.position(position() + length);
+     * </pre>
+     *
+     * @param length
+     *            The number of bytes to skip.
+     * @throws IndexOutOfBoundsException
+     *             If the new position is less than 0 or greater than the length
+     *             of the underlying byte sequence.
+     */
+    public void skip(final int length) {
+        position(pos + length);
+    }
+
+    @Override
+    public String toString() {
+        return sequence.toString();
+    }
+
+    private InputStream asInputStream() {
+        if (inputStream == null) {
+            inputStream = new InputStream() {
+                @Override
+                public int read() throws IOException {
+                    if (pos >= sequence.length()) {
+                        return -1;
+                    }
+                    return sequence.byteAt(pos++) & 0xFF;
+                }
+            };
+        }
+        return inputStream;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java
new file mode 100644
index 0000000..de0a469
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteString.java
@@ -0,0 +1,958 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.util.Arrays;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/** An immutable sequence of bytes backed by a byte array. */
+public final class ByteString implements ByteSequence {
+    /** Singleton empty byte string. */
+    private static final ByteString EMPTY = wrap(new byte[0]);
+
+    /**
+     * Returns an empty byte string.
+     *
+     * @return An empty byte string.
+     */
+    public static ByteString empty() {
+        return EMPTY;
+    }
+
+    /**
+     * Returns a byte string containing the big-endian encoded bytes of the
+     * provided integer.
+     *
+     * @param i
+     *            The integer to encode.
+     * @return The byte string containing the big-endian encoded bytes of the
+     *         provided integer.
+     */
+    public static ByteString valueOfInt(int i) {
+        final byte[] bytes = new byte[4];
+        for (int j = 3; j >= 0; j--) {
+            bytes[j] = (byte) i;
+            i >>>= 8;
+        }
+        return wrap(bytes);
+    }
+
+    /**
+     * Returns a byte string containing the big-endian encoded bytes of the
+     * provided long.
+     *
+     * @param l
+     *            The long to encode.
+     * @return The byte string containing the big-endian encoded bytes of the
+     *         provided long.
+     */
+    public static ByteString valueOfLong(long l) {
+        final byte[] bytes = new byte[8];
+        for (int i = 7; i >= 0; i--) {
+            bytes[i] = (byte) l;
+            l >>>= 8;
+        }
+        return wrap(bytes);
+    }
+
+    /**
+     * Returns a byte string representation of the provided object. The object
+     * is converted to a byte string as follows:
+     * <ul>
+     * <li>if the object is an instance of {@code ByteSequence} then this method
+     * is equivalent to calling {@code o.toByteString()}
+     * <li>if the object is a {@code byte[]} then this method is equivalent to
+     * calling {@link #valueOfBytes(byte[])}
+     * <li>if the object is a {@code char[]} then this method is equivalent to
+     * calling {@link #valueOfUtf8(char[])}
+     * <li>for all other types of object this method is equivalent to calling
+     * {@link #valueOfUtf8(CharSequence)} with the {@code toString()} representation of
+     * the provided object.
+     * </ul>
+     * <b>Note:</b> this method treats {@code Long} and {@code Integer} objects
+     * like any other type of {@code Object}. More specifically, the following
+     * invocations are not equivalent:
+     * <ul>
+     * <li>{@code valueOf(0)} is not equivalent to {@code valueOf((Object) 0)}
+     * <li>{@code valueOf(0L)} is not equivalent to {@code valueOf((Object) 0L)}
+     * </ul>
+     *
+     * @param o
+     *            The object to use.
+     * @return The byte string containing the provided object.
+     */
+    public static ByteString valueOfObject(final Object o) {
+        if (o instanceof ByteSequence) {
+            return ((ByteSequence) o).toByteString();
+        } else if (o instanceof byte[]) {
+            return valueOfBytes((byte[]) o);
+        } else if (o instanceof char[]) {
+            return valueOfUtf8((char[]) o);
+        } else {
+            return valueOfUtf8(o.toString());
+        }
+    }
+
+    /**
+     * Returns a byte string containing the UTF-8 encoded bytes of the provided
+     * char sequence.
+     *
+     * @param s
+     *            The char sequence to use.
+     * @return The byte string with the encoded bytes of the provided string.
+     */
+    public static ByteString valueOfUtf8(final CharSequence s) {
+        if (s.length() == 0) {
+            return EMPTY;
+        }
+        return wrap(StaticUtils.getBytes(s));
+    }
+
+    /**
+     * Returns a byte string containing the Base64 decoded bytes of the provided
+     * string.
+     *
+     * @param s
+     *            The string to use.
+     * @return The byte string containing the Base64 decoded bytes of the
+     *         provided string.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided string does not contain valid Base64 encoded
+     *             content.
+     * @see #toBase64String()
+     */
+    public static ByteString valueOfBase64(final String s) {
+        if (s.length() == 0) {
+            return EMPTY;
+        }
+        return Base64.decode(s);
+    }
+
+    /**
+     * Returns a byte string containing the bytes of the provided hexadecimal string.
+     *
+     * @param hexString
+     *            The hexadecimal string to convert to a byte array.
+     * @return The byte string containing the binary representation of the
+     *         provided hex string.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided string contains invalid hexadecimal digits or
+     *             does not contain an even number of digits.
+     */
+    public static ByteString valueOfHex(final String hexString) {
+        if (hexString == null || hexString.length() == 0) {
+            return EMPTY;
+        }
+
+        final int length = hexString.length();
+        if (length % 2 != 0) {
+            throw new LocalizedIllegalArgumentException(ERR_HEX_DECODE_INVALID_LENGTH.get(hexString));
+        }
+        final int arrayLength = length / 2;
+        final byte[] bytes = new byte[arrayLength];
+        for (int i = 0; i < arrayLength; i++) {
+            bytes[i] = hexToByte(hexString, hexString.charAt(i * 2), hexString.charAt(i * 2 + 1));
+        }
+        return valueOfBytes(bytes);
+    }
+
+    /**
+     * Returns a byte string containing the contents of the provided byte array.
+     * <p>
+     * This method differs from {@link #wrap(byte[])} in that it defensively
+     * copies the provided byte array.
+     *
+     * @param bytes
+     *            The byte array to use.
+     * @return A byte string containing a copy of the provided byte array.
+     */
+    public static ByteString valueOfBytes(final byte[] bytes) {
+        if (bytes.length == 0) {
+            return EMPTY;
+        }
+        return wrap(Arrays.copyOf(bytes, bytes.length));
+    }
+
+    /**
+     * Returns a byte string containing a subsequence of the contents of the
+     * provided byte array.
+     * <p>
+     * This method differs from {@link #wrap(byte[], int, int)} in that it
+     * defensively copies the provided byte array.
+     *
+     * @param bytes
+     *            The byte array to use.
+     * @param offset
+     *            The offset of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length} .
+     * @param length
+     *            The length of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length - offset}.
+     * @return A byte string containing a copy of the subsequence of the
+     *         provided byte array.
+     */
+    public static ByteString valueOfBytes(final byte[] bytes, final int offset, final int length) {
+        checkArrayBounds(bytes, offset, length);
+        if (offset == length) {
+            return EMPTY;
+        }
+        return wrap(Arrays.copyOfRange(bytes, offset, offset + length));
+    }
+
+    /**
+     * Returns a byte string containing the UTF-8 encoded bytes of the provided
+     * char array.
+     *
+     * @param chars
+     *            The char array to use.
+     * @return A byte string containing the UTF-8 encoded bytes of the provided
+     *         char array.
+     */
+    public static ByteString valueOfUtf8(final char[] chars) {
+        if (chars.length == 0) {
+            return EMPTY;
+        }
+        return wrap(StaticUtils.getBytes(chars));
+    }
+
+    /**
+     * Returns a byte string that wraps the provided byte array.
+     * <p>
+     * <b>NOTE:</b> this method takes ownership of the provided byte array and,
+     * therefore, the byte array MUST NOT be altered directly after this method
+     * returns.
+     *
+     * @param bytes
+     *            The byte array to wrap.
+     * @return The byte string that wraps the given byte array.
+     */
+    public static ByteString wrap(final byte[] bytes) {
+        return new ByteString(bytes, 0, bytes.length);
+    }
+
+    /**
+     * Returns a byte string that wraps a subsequence of the provided byte
+     * array.
+     * <p>
+     * <b>NOTE:</b> this method takes ownership of the provided byte array and,
+     * therefore, the byte array MUST NOT be altered directly after this method
+     * returns.
+     *
+     * @param bytes
+     *            The byte array to wrap.
+     * @param offset
+     *            The offset of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length} .
+     * @param length
+     *            The length of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length - offset}.
+     * @return The byte string that wraps the given byte array.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative or if {@code length} is
+     *             negative or if {@code offset + length} is greater than
+     *             {@code bytes.length}.
+     */
+    public static ByteString wrap(final byte[] bytes, final int offset, final int length) {
+        checkArrayBounds(bytes, offset, length);
+        return new ByteString(bytes, offset, length);
+    }
+
+    /**
+     * Checks the array bounds of the provided byte array sub-sequence, throwing
+     * an {@code IndexOutOfBoundsException} if they are illegal.
+     *
+     * @param b
+     *            The byte array.
+     * @param offset
+     *            The offset of the byte array to be checked; must be
+     *            non-negative and no larger than {@code b.length}.
+     * @param length
+     *            The length of the byte array to be checked; must be
+     *            non-negative and no larger than {@code b.length - offset}.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative or if {@code length} is
+     *             negative or if {@code offset + length} is greater than
+     *             {@code b.length}.
+     */
+    static void checkArrayBounds(final byte[] b, final int offset, final int length) {
+        if (offset < 0 || offset > b.length || length < 0 || offset + length > b.length
+                || offset + length < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Compares two byte array sub-sequences and returns a value that indicates
+     * their relative order.
+     *
+     * @param b1
+     *            The byte array containing the first sub-sequence.
+     * @param offset1
+     *            The offset of the first byte array sub-sequence.
+     * @param length1
+     *            The length of the first byte array sub-sequence.
+     * @param b2
+     *            The byte array containing the second sub-sequence.
+     * @param offset2
+     *            The offset of the second byte array sub-sequence.
+     * @param length2
+     *            The length of the second byte array sub-sequence.
+     * @return A negative integer if first byte array sub-sequence should come
+     *         before the second byte array sub-sequence in ascending order, a
+     *         positive integer if the first byte array sub-sequence should come
+     *         after the byte array sub-sequence in ascending order, or zero if
+     *         there is no difference between the two byte array sub-sequences
+     *         with regard to ordering.
+     */
+    static int compareTo(final byte[] b1, final int offset1, final int length1, final byte[] b2,
+            final int offset2, final int length2) {
+        int count = Math.min(length1, length2);
+        int i = offset1;
+        int j = offset2;
+        while (count-- != 0) {
+            final int firstByte = 0xFF & b1[i++];
+            final int secondByte = 0xFF & b2[j++];
+            if (firstByte != secondByte) {
+                return firstByte - secondByte;
+            }
+        }
+        return length1 - length2;
+    }
+
+    /**
+     * Indicates whether two byte array sub-sequences are equal. In order for
+     * them to be considered equal, they must contain the same bytes in the same
+     * order.
+     *
+     * @param b1
+     *            The byte array containing the first sub-sequence.
+     * @param offset1
+     *            The offset of the first byte array sub-sequence.
+     * @param length1
+     *            The length of the first byte array sub-sequence.
+     * @param b2
+     *            The byte array containing the second sub-sequence.
+     * @param offset2
+     *            The offset of the second byte array sub-sequence.
+     * @param length2
+     *            The length of the second byte array sub-sequence.
+     * @return {@code true} if the two byte array sub-sequences have the same
+     *         content, or {@code false} if not.
+     */
+    static boolean equals(final byte[] b1, final int offset1, final int length1, final byte[] b2,
+            final int offset2, final int length2) {
+        if (length1 != length2) {
+            return false;
+        }
+
+        int i = offset1;
+        int j = offset2;
+        int count = length1;
+        while (count-- != 0) {
+            if (b1[i++] != b2[j++]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns a hash code for the provided byte array sub-sequence.
+     *
+     * @param b
+     *            The byte array.
+     * @param offset
+     *            The offset of the byte array sub-sequence.
+     * @param length
+     *            The length of the byte array sub-sequence.
+     * @return A hash code for the provided byte array sub-sequence.
+     */
+    static int hashCode(final byte[] b, final int offset, final int length) {
+        int hashCode = 1;
+        int i = offset;
+        int count = length;
+        while (count-- != 0) {
+            hashCode = 31 * hashCode + b[i++];
+        }
+        return hashCode;
+    }
+
+    /**
+     * Returns the UTF-8 decoded string representation of the provided byte
+     * array sub-sequence. If UTF-8 decoding fails, the platform's default
+     * encoding will be used.
+     *
+     * @param b
+     *            The byte array.
+     * @param offset
+     *            The offset of the byte array sub-sequence.
+     * @param length
+     *            The length of the byte array sub-sequence.
+     * @return The string representation of the byte array sub-sequence.
+     */
+    static String toString(final byte[] b, final int offset, final int length) {
+        if (length == 0) {
+            return "";
+        }
+        try {
+            return new String(b, offset, length, "UTF-8");
+        } catch (final UnsupportedEncodingException e) {
+            // TODO: I18N
+            throw new RuntimeException("Unable to decode bytes as UTF-8 string", e);
+        }
+    }
+
+    /**
+     * Returns a 7-bit ASCII string representation.
+     * Non-ASCII characters will be expanded to percent (%) hexadecimal value.
+     * @return a 7-bit ASCII string representation
+     */
+    public String toASCIIString() {
+        if (length == 0) {
+            return "";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            byte b = buffer[offset + i];
+            if (StaticUtils.isPrintable(b)) {
+                sb.append((char) b);
+            } else {
+                sb.append('%');
+                sb.append(StaticUtils.byteToHex(b));
+            }
+        }
+        return sb.toString();
+    }
+
+    private static byte hexToByte(final String value, final char c1, final char c2) {
+        byte b;
+        switch (c1) {
+        case '0':
+            b = 0x00;
+            break;
+        case '1':
+            b = 0x10;
+            break;
+        case '2':
+            b = 0x20;
+            break;
+        case '3':
+            b = 0x30;
+            break;
+        case '4':
+            b = 0x40;
+            break;
+        case '5':
+            b = 0x50;
+            break;
+        case '6':
+            b = 0x60;
+            break;
+        case '7':
+            b = 0x70;
+            break;
+        case '8':
+            b = (byte) 0x80;
+            break;
+        case '9':
+            b = (byte) 0x90;
+            break;
+        case 'A':
+        case 'a':
+            b = (byte) 0xA0;
+            break;
+        case 'B':
+        case 'b':
+            b = (byte) 0xB0;
+            break;
+        case 'C':
+        case 'c':
+            b = (byte) 0xC0;
+            break;
+        case 'D':
+        case 'd':
+            b = (byte) 0xD0;
+            break;
+        case 'E':
+        case 'e':
+            b = (byte) 0xE0;
+            break;
+        case 'F':
+        case 'f':
+            b = (byte) 0xF0;
+            break;
+        default:
+            throw new LocalizedIllegalArgumentException(ERR_HEX_DECODE_INVALID_CHARACTER.get(value, c1));
+        }
+
+        switch (c2) {
+        case '0':
+            // No action required.
+            break;
+        case '1':
+            b |= 0x01;
+            break;
+        case '2':
+            b |= 0x02;
+            break;
+        case '3':
+            b |= 0x03;
+            break;
+        case '4':
+            b |= 0x04;
+            break;
+        case '5':
+            b |= 0x05;
+            break;
+        case '6':
+            b |= 0x06;
+            break;
+        case '7':
+            b |= 0x07;
+            break;
+        case '8':
+            b |= 0x08;
+            break;
+        case '9':
+            b |= 0x09;
+            break;
+        case 'A':
+        case 'a':
+            b |= 0x0A;
+            break;
+        case 'B':
+        case 'b':
+            b |= 0x0B;
+            break;
+        case 'C':
+        case 'c':
+            b |= 0x0C;
+            break;
+        case 'D':
+        case 'd':
+            b |= 0x0D;
+            break;
+        case 'E':
+        case 'e':
+            b |= 0x0E;
+            break;
+        case 'F':
+        case 'f':
+            b |= 0x0F;
+            break;
+        default:
+            throw new LocalizedIllegalArgumentException(ERR_HEX_DECODE_INVALID_CHARACTER.get(value, c2));
+        }
+
+        return b;
+    }
+
+    // These are package private so that compression and crypto
+    // functionality may directly access the fields.
+
+    /** The buffer where data is stored. */
+    final byte[] buffer;
+
+    /** The number of bytes to expose from the buffer. */
+    final int length;
+
+    /** The start index of the range of bytes to expose through this byte string. */
+    final int offset;
+
+    /**
+     * Creates a new byte string that wraps a subsequence of the provided byte
+     * array.
+     * <p>
+     * <b>NOTE:</b> this method takes ownership of the provided byte array and,
+     * therefore, the byte array MUST NOT be altered directly after this method
+     * returns.
+     *
+     * @param b
+     *            The byte array to wrap.
+     * @param offset
+     *            The offset of the byte array to be used; must be non-negative
+     *            and no larger than {@code b.length} .
+     * @param length
+     *            The length of the byte array to be used; must be non-negative
+     *            and no larger than {@code b.length - offset}.
+     */
+    private ByteString(final byte[] b, final int offset, final int length) {
+        this.buffer = b;
+        this.offset = offset;
+        this.length = length;
+    }
+
+    /**
+     * Returns a {@link ByteSequenceReader} which can be used to incrementally
+     * read and decode data from this byte string.
+     *
+     * @return The {@link ByteSequenceReader} which can be used to incrementally
+     *         read and decode data from this byte string.
+     */
+    @Override
+    public ByteSequenceReader asReader() {
+        return new ByteSequenceReader(this);
+    }
+
+    @Override
+    public byte byteAt(final int index) {
+        if (index >= length || index < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        return buffer[offset + index];
+    }
+
+    @Override
+    public int compareTo(final byte[] bytes, final int offset, final int length) {
+        checkArrayBounds(bytes, offset, length);
+        return compareTo(this.buffer, this.offset, this.length, bytes, offset, length);
+    }
+
+    @Override
+    public int compareTo(final ByteSequence o) {
+        if (this == o) {
+            return 0;
+        }
+        return -o.compareTo(buffer, offset, length);
+    }
+
+    @Override
+    public byte[] copyTo(final byte[] bytes) {
+        copyTo(bytes, 0);
+        return bytes;
+    }
+
+    @Override
+    public byte[] copyTo(final byte[] bytes, final int offset) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        System.arraycopy(buffer, this.offset, bytes, offset, Math.min(length, bytes.length - offset));
+        return bytes;
+    }
+
+    @Override
+    public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+        byteBuffer.put(buffer, offset, length);
+        return byteBuffer;
+    }
+
+    @Override
+    public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
+        builder.appendBytes(buffer, offset, length);
+        return builder;
+    }
+
+    @Override
+    public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+        return copyTo(ByteBuffer.wrap(buffer, offset, length), charBuffer, decoder);
+    }
+
+    /**
+     * Convenience method to copy from a byte buffer to a char buffer using provided decoder to decode
+     * bytes into characters.
+     * <p>
+     * It should not be used directly, prefer instance method of ByteString or ByteStringBuilder instead.
+     */
+    static boolean copyTo(ByteBuffer inBuffer, CharBuffer outBuffer, CharsetDecoder decoder) {
+        final CoderResult result = decoder.decode(inBuffer, outBuffer, true);
+        decoder.flush(outBuffer);
+        return !result.isError() && !result.isOverflow();
+    }
+
+    @Override
+    public OutputStream copyTo(final OutputStream stream) throws IOException {
+        stream.write(buffer, offset, length);
+        return stream;
+    }
+
+    @Override
+    public boolean equals(final byte[] bytes, final int offset, final int length) {
+        checkArrayBounds(bytes, offset, length);
+        return equals(this.buffer, this.offset, this.length, bytes, offset, length);
+    }
+
+    /**
+     * Indicates whether the provided object is equal to this byte string. In
+     * order for it to be considered equal, the provided object must be a byte
+     * sequence containing the same bytes in the same order.
+     *
+     * @param o
+     *            The object for which to make the determination.
+     * @return {@code true} if the provided object is a byte sequence whose
+     *         content is equal to that of this byte string, or {@code false} if
+     *         not.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof ByteSequence) {
+            final ByteSequence other = (ByteSequence) o;
+            return other.equals(buffer, offset, length);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns a hash code for this byte string. It will be the sum of all of
+     * the bytes contained in the byte string.
+     *
+     * @return A hash code for this byte string.
+     */
+    @Override
+    public int hashCode() {
+        return hashCode(buffer, offset, length);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return length == 0;
+    }
+
+    @Override
+    public int length() {
+        return length;
+    }
+
+    @Override
+    public ByteString subSequence(final int start, final int end) {
+        if (start < 0 || start > end || end > length) {
+            throw new IndexOutOfBoundsException();
+        }
+        return new ByteString(buffer, offset + start, end - start);
+    }
+
+    @Override
+    public boolean startsWith(ByteSequence prefix) {
+        return prefix != null && prefix.length() <= length && prefix.equals(buffer, 0, prefix.length());
+    }
+
+    @Override
+    public String toBase64String() {
+        return Base64.encode(this);
+    }
+
+    /**
+     * Returns a string representation of the contents of this byte sequence
+     * using hexadecimal characters and a space between each byte.
+     *
+     * @return A string representation of the contents of this byte sequence
+     *         using hexadecimal characters.
+     */
+    public String toHexString() {
+        if (isEmpty()) {
+            return "";
+        }
+        StringBuilder builder = new StringBuilder(length * 2);
+        builder.append(StaticUtils.byteToHex(buffer[offset]));
+        for (int i = 1; i < length; i++) {
+            builder.append(StaticUtils.byteToHex(buffer[offset + i]));
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Returns a string representation of the contents of this byte sequence
+     * using hexadecimal characters and a percent prefix (%) before each char.
+     *
+     * @return A string representation of the contents of this byte sequence
+     *         using percent + hexadecimal characters.
+     */
+    public String toPercentHexString() {
+        if (isEmpty()) {
+            return "";
+        }
+        StringBuilder builder = new StringBuilder(length * 3);
+        for (int i = 0; i < length; i++) {
+            builder.append('%');
+            builder.append(StaticUtils.byteToHex(buffer[offset + i]));
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Returns a string representation of the data in this byte sequence using
+     * the specified indent.
+     * <p>
+     * The data will be formatted with sixteen hex bytes in a row followed by
+     * the ASCII representation, then wrapping to a new line as necessary. The
+     * state of the byte buffer is not changed.
+     *
+     * @param indent
+     *            The number of spaces to indent the output.
+     * @return the string representation of this byte string
+     */
+    public String toHexPlusAsciiString(int indent) {
+        StringBuilder builder = new StringBuilder();
+        StringBuilder indentBuf = new StringBuilder(indent);
+        for (int i = 0; i < indent; i++) {
+            indentBuf.append(' ');
+        }
+        int pos = 0;
+        while (length - pos >= 16) {
+            StringBuilder asciiBuf = new StringBuilder(17);
+            byte currentByte = buffer[offset + pos];
+            builder.append(indentBuf);
+            builder.append(byteToHex(currentByte));
+            asciiBuf.append(byteToASCII(currentByte));
+            pos++;
+
+            for (int i = 1; i < 16; i++, pos++) {
+                currentByte = buffer[offset + pos];
+                builder.append(' ');
+                builder.append(byteToHex(currentByte));
+                asciiBuf.append(byteToASCII(currentByte));
+
+                if (i == 7) {
+                    builder.append("  ");
+                    asciiBuf.append(' ');
+                }
+            }
+
+            builder.append("  ");
+            builder.append(asciiBuf);
+            builder.append(EOL);
+        }
+
+        int remaining = length - pos;
+        if (remaining > 0) {
+            StringBuilder asciiBuf = new StringBuilder(remaining + 1);
+
+            byte currentByte = buffer[offset + pos];
+            builder.append(indentBuf);
+            builder.append(byteToHex(currentByte));
+            asciiBuf.append(byteToASCII(currentByte));
+            pos++;
+
+            for (int i = 1; i < 16; i++, pos++) {
+                builder.append(' ');
+
+                if (i < remaining) {
+                    currentByte = buffer[offset + pos];
+                    builder.append(byteToHex(currentByte));
+                    asciiBuf.append(byteToASCII(currentByte));
+                } else {
+                    builder.append("  ");
+                }
+
+                if (i == 7) {
+                    builder.append("  ");
+
+                    if (i < remaining) {
+                        asciiBuf.append(' ');
+                    }
+                }
+            }
+
+            builder.append("  ");
+            builder.append(asciiBuf);
+            builder.append(EOL);
+        }
+        return builder.toString();
+    }
+
+    @Override
+    public byte[] toByteArray() {
+        return copyTo(new byte[length]);
+    }
+
+    @Override
+    public ByteString toByteString() {
+        return this;
+    }
+
+    /**
+     * Returns the UTF-8 decoded char array representation of this byte
+     * sequence.
+     *
+     * @return The UTF-8 decoded char array representation of this byte
+     *         sequence.
+     */
+    public char[] toCharArray() {
+        Charset utf8 = Charset.forName("UTF-8");
+        CharBuffer charBuffer = utf8.decode(ByteBuffer.wrap(buffer, offset, length));
+        char[] chars = new char[charBuffer.remaining()];
+        charBuffer.get(chars);
+        return chars;
+    }
+
+    /**
+     * Returns the integer value represented by the first four bytes of this
+     * byte string in big-endian order.
+     *
+     * @return The integer value represented by the first four bytes of this
+     *         byte string in big-endian order.
+     * @throws IndexOutOfBoundsException
+     *             If this byte string has less than four bytes.
+     */
+    public int toInt() {
+        if (length < 4) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        int v = 0;
+        for (int i = 0; i < 4; i++) {
+            v <<= 8;
+            v |= buffer[offset + i] & 0xFF;
+        }
+        return v;
+    }
+
+    /**
+     * Returns the long value represented by the first eight bytes of this byte
+     * string in big-endian order.
+     *
+     * @return The long value represented by the first eight bytes of this byte
+     *         string in big-endian order.
+     * @throws IndexOutOfBoundsException
+     *             If this byte string has less than eight bytes.
+     */
+    public long toLong() {
+        if (length < 8) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        long v = 0;
+        for (int i = 0; i < 8; i++) {
+            v <<= 8;
+            v |= buffer[offset + i] & 0xFF;
+        }
+        return v;
+    }
+
+    @Override
+    public String toString() {
+        return toString(buffer, offset, length);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java
new file mode 100644
index 0000000..cb67029
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ByteStringBuilder.java
@@ -0,0 +1,1152 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.DataInput;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.PackedLong;
+
+/** A mutable sequence of bytes backed by a byte array. */
+public final class ByteStringBuilder implements ByteSequence {
+
+    /** Maximum size in bytes of a compact encoded value. */
+    public static final int MAX_COMPACT_SIZE = PackedLong.MAX_COMPACT_SIZE;
+
+    /** Output stream implementation. */
+    private final class OutputStreamImpl extends OutputStream {
+        @Override
+        public void close() {
+            // Do nothing.
+        }
+
+        @Override
+        public void write(final byte[] bytes) {
+            appendBytes(bytes);
+        }
+
+        @Override
+        public void write(final byte[] bytes, final int i, final int i1) {
+            appendBytes(bytes, i, i1);
+        }
+
+        @Override
+        public void write(final int i) {
+            appendByte(i);
+        }
+    }
+
+    /**
+     * A sub-sequence of the parent byte string builder. The sub-sequence will
+     * be robust against all updates to the byte string builder except for
+     * invocations of the method {@code clear()}.
+     */
+    private final class SubSequence implements ByteSequence {
+
+        /** The length of the sub-sequence. */
+        private final int subLength;
+
+        /** The offset of the sub-sequence. */
+        private final int subOffset;
+
+        /**
+         * Creates a new sub-sequence.
+         *
+         * @param offset
+         *            The offset of the sub-sequence.
+         * @param length
+         *            The length of the sub-sequence.
+         */
+        private SubSequence(final int offset, final int length) {
+            this.subOffset = offset;
+            this.subLength = length;
+        }
+
+        @Override
+        public ByteSequenceReader asReader() {
+            return new ByteSequenceReader(this);
+        }
+
+        @Override
+        public byte byteAt(final int index) {
+            if (index >= subLength || index < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            // Protect against reallocation: use builder's buffer.
+            return buffer[subOffset + index];
+        }
+
+        @Override
+        public int compareTo(final byte[] b, final int offset, final int length) {
+            ByteString.checkArrayBounds(b, offset, length);
+
+            // Protect against reallocation: use builder's buffer.
+            return ByteString.compareTo(buffer, subOffset, subLength, b, offset, length);
+        }
+
+        @Override
+        public int compareTo(final ByteSequence o) {
+            if (this == o) {
+                return 0;
+            }
+
+            // Protect against reallocation: use builder's buffer.
+            return -o.compareTo(buffer, subOffset, subLength);
+        }
+
+        @Override
+        public byte[] copyTo(final byte[] b) {
+            copyTo(b, 0);
+            return b;
+        }
+
+        @Override
+        public byte[] copyTo(final byte[] b, final int offset) {
+            if (offset < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            // Protect against reallocation: use builder's buffer.
+            System.arraycopy(buffer, subOffset, b, offset, Math.min(subLength, b.length - offset));
+            return b;
+        }
+
+        @Override
+        public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+            byteBuffer.put(buffer, subOffset, subLength);
+            return byteBuffer;
+        }
+
+        @Override
+        public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
+            // Protect against reallocation: use builder's buffer.
+            return builder.appendBytes(buffer, subOffset, subLength);
+        }
+
+        @Override
+        public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+            return ByteString.copyTo(ByteBuffer.wrap(buffer, subOffset, subLength), charBuffer, decoder);
+        }
+
+        @Override
+        public OutputStream copyTo(final OutputStream stream) throws IOException {
+            // Protect against reallocation: use builder's buffer.
+            stream.write(buffer, subOffset, subLength);
+            return stream;
+        }
+
+        @Override
+        public boolean equals(final byte[] b, final int offset, final int length) {
+            ByteString.checkArrayBounds(b, offset, length);
+
+            // Protect against reallocation: use builder's buffer.
+            return ByteString.equals(buffer, subOffset, subLength, b, offset, length);
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            } else if (o instanceof ByteSequence) {
+                final ByteSequence other = (ByteSequence) o;
+
+                // Protect against reallocation: use builder's buffer.
+                return other.equals(buffer, subOffset, subLength);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            // Protect against reallocation: use builder's buffer.
+            return ByteString.hashCode(buffer, subOffset, subLength);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return length == 0;
+        }
+
+        @Override
+        public int length() {
+            return subLength;
+        }
+
+        @Override
+        public ByteSequence subSequence(final int start, final int end) {
+            if (start < 0 || start > end || end > subLength) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            return new SubSequence(subOffset + start, end - start);
+        }
+
+        @Override
+        public boolean startsWith(ByteSequence prefix) {
+            if (prefix == null || prefix.length() > length) {
+                return false;
+            }
+            return prefix.equals(buffer, 0, prefix.length());
+        }
+
+        @Override
+        public String toBase64String() {
+            return Base64.encode(this);
+        }
+
+        @Override
+        public byte[] toByteArray() {
+            return copyTo(new byte[subLength]);
+        }
+
+        @Override
+        public ByteString toByteString() {
+            // Protect against reallocation: use builder's buffer.
+            final byte[] b = new byte[subLength];
+            System.arraycopy(buffer, subOffset, b, 0, subLength);
+            return ByteString.wrap(b);
+        }
+
+        @Override
+        public String toString() {
+            // Protect against reallocation: use builder's buffer.
+            return ByteString.toString(buffer, subOffset, subLength);
+        }
+    }
+
+    // These are package private so that compression and crypto
+    // functionality may directly access the fields.
+
+    /** The buffer where data is stored. */
+    byte[] buffer;
+
+    /** The number of bytes to expose from the buffer. */
+    int length;
+
+    /**
+     * The lazily allocated output stream view of this builder. Synchronization
+     * is not necessary because the stream is stateless and race conditions can
+     * be tolerated.
+     */
+    private OutputStreamImpl os;
+
+    /** Creates a new byte string builder with an initial capacity of 32 bytes. */
+    public ByteStringBuilder() {
+        // Initially create a 32 byte buffer.
+        this(32);
+    }
+
+    /**
+     * Creates a new byte string builder with the specified initial capacity.
+     *
+     * @param capacity
+     *            The initial capacity.
+     * @throws IllegalArgumentException
+     *             If the {@code capacity} is negative.
+     */
+    public ByteStringBuilder(final int capacity) {
+        Reject.ifFalse(capacity >= 0, "capacity must be >= 0");
+        this.buffer = new byte[capacity];
+        this.length = 0;
+    }
+
+    /**
+     * Creates a new byte string builder with the content of the provided
+     * ByteSequence. Its capacity is set to the length of the provided
+     * ByteSequence.
+     *
+     * @param bs
+     *            The ByteSequence to copy
+     */
+    public ByteStringBuilder(final ByteSequence bs) {
+        this(bs.length());
+        bs.copyTo(this);
+    }
+
+    /**
+     * Appends the provided byte to this byte string builder.
+     * <p>
+     * Note: this method accepts an {@code int} for ease of reading and writing.
+     * <p>
+     * This method only keeps the lowest 8-bits of the provided {@code int}.
+     * Higher bits will be truncated. This method performs the equivalent of:
+     *
+     * <pre>
+     * int i = ...;
+     * int i8bits = i & 0xFF;
+     * // only use "i8bits"
+     * </pre>
+     * OR
+     * <pre>
+     * int i = ...;
+     * byte b = (byte) i;
+     * // only use "b"
+     * </pre>
+     *
+     * @param b
+     *            The byte to be appended to this byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendByte(final int b) {
+        ensureAdditionalCapacity(1);
+        buffer[length++] = (byte) b;
+        return this;
+    }
+
+    /**
+     * Appends the provided byte array to this byte string builder.
+     * <p>
+     * An invocation of the form:
+     *
+     * <pre>
+     * src.append(bytes)
+     * </pre>
+     *
+     * Behaves in exactly the same way as the invocation:
+     *
+     * <pre>
+     * src.append(bytes, 0, bytes.length);
+     * </pre>
+     *
+     * @param bytes
+     *            The byte array to be appended to this byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendBytes(final byte[] bytes) {
+        return appendBytes(bytes, 0, bytes.length);
+    }
+
+    /**
+     * Appends the provided byte array to this byte string builder.
+     *
+     * @param bytes
+     *            The byte array to be appended to this byte string builder.
+     * @param offset
+     *            The offset of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length} .
+     * @param length
+     *            The length of the byte array to be used; must be non-negative
+     *            and no larger than {@code bytes.length - offset}.
+     * @return This byte string builder.
+     * @throws IndexOutOfBoundsException
+     *             If {@code offset} is negative or if {@code length} is
+     *             negative or if {@code offset + length} is greater than
+     *             {@code bytes.length}.
+     */
+    public ByteStringBuilder appendBytes(final byte[] bytes, final int offset, final int length) {
+        ByteString.checkArrayBounds(bytes, offset, length);
+
+        if (length != 0) {
+            ensureAdditionalCapacity(length);
+            System.arraycopy(bytes, offset, buffer, this.length, length);
+            this.length += length;
+        }
+
+        return this;
+    }
+
+    /**
+     * Appends the provided {@code ByteBuffer} to this byte string builder.
+     *
+     * @param buffer
+     *            The byte buffer to be appended to this byte string builder.
+     * @param length
+     *            The number of bytes to be appended from {@code buffer}.
+     * @return This byte string builder.
+     * @throws IndexOutOfBoundsException
+     *             If {@code length} is less than zero or greater than
+     *             {@code buffer.remaining()}.
+     */
+    public ByteStringBuilder appendBytes(final ByteBuffer buffer, final int length) {
+        if (length < 0 || length > buffer.remaining()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        if (length != 0) {
+            ensureAdditionalCapacity(length);
+            buffer.get(this.buffer, this.length, length);
+            this.length += length;
+        }
+
+        return this;
+    }
+
+    /**
+     * Appends the provided {@link ByteSequence} to this byte string builder.
+     *
+     * @param bytes
+     *            The byte sequence to be appended to this byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendBytes(final ByteSequence bytes) {
+        return bytes.copyTo(this);
+    }
+
+    /**
+     * Appends the provided {@link ByteSequenceReader} to this byte string builder.
+     *
+     * @param reader
+     *            The byte sequence reader to be appended to this byte string
+     *            builder.
+     * @param length
+     *            The number of bytes to be appended from {@code reader}.
+     * @return This byte string builder.
+     * @throws IndexOutOfBoundsException
+     *             If {@code length} is less than zero or greater than
+     *             {@code reader.remaining()}.
+     */
+    public ByteStringBuilder appendBytes(final ByteSequenceReader reader, final int length) {
+        if (length < 0 || length > reader.remaining()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        if (length != 0) {
+            ensureAdditionalCapacity(length);
+            reader.readBytes(buffer, this.length, length);
+            this.length += length;
+        }
+
+        return this;
+    }
+
+    /**
+     * Appends the UTF-8 encoded bytes of the provided char array to this byte
+     * string builder.
+     *
+     * @param chars
+     *            The char array whose UTF-8 encoding is to be appended to this
+     *            byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendUtf8(final char[] chars) {
+        if (chars == null) {
+            return this;
+        }
+
+        // Assume that each char is 1 byte
+        final int len = chars.length;
+        ensureAdditionalCapacity(len);
+
+        for (int i = 0; i < len; i++) {
+            final char c = chars[i];
+            final byte b = (byte) (c & 0x0000007F);
+
+            if (c == b) {
+                buffer[this.length + i] = b;
+            } else {
+                // There is a multi-byte char. Defer to JDK.
+                final Charset utf8 = Charset.forName("UTF-8");
+                final ByteBuffer byteBuffer = utf8.encode(CharBuffer.wrap(chars));
+                final int remaining = byteBuffer.remaining();
+                ensureAdditionalCapacity(remaining - len);
+                byteBuffer.get(buffer, this.length, remaining);
+                this.length += remaining;
+                return this;
+            }
+        }
+
+        // The 1 byte char assumption was correct
+        this.length += len;
+        return this;
+    }
+
+    /**
+     * Appends the provided {@code DataInput} to this byte string
+     * builder.
+     *
+     * @param stream
+     *          The data input stream to be appended to this byte string
+     *          builder.
+     * @param length
+     *          The maximum number of bytes to be appended from {@code
+     *          input}.
+     * @throws IndexOutOfBoundsException
+     *           If {@code length} is less than zero.
+     * @throws EOFException
+     *           If this stream reaches the end before reading all the bytes.
+     * @throws IOException
+     *           If an I/O error occurs.
+     */
+    public void appendBytes(DataInput stream, int length) throws EOFException, IOException {
+        if (length < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        ensureAdditionalCapacity(length);
+        stream.readFully(buffer, this.length, length);
+        this.length += length;
+    }
+
+    /**
+     * Appends the provided {@code InputStream} to this byte string builder.
+     *
+     * @param stream
+     *            The input stream to be appended to this byte string builder.
+     * @param length
+     *            The maximum number of bytes to be appended from {@code buffer}
+     *            .
+     * @return The number of bytes read from the input stream, or {@code -1} if
+     *         the end of the input stream has been reached.
+     * @throws IndexOutOfBoundsException
+     *             If {@code length} is less than zero.
+     * @throws IOException
+     *             If an I/O error occurs.
+     */
+    public int appendBytes(final InputStream stream, final int length) throws IOException {
+        if (length < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        ensureAdditionalCapacity(length);
+        final int bytesRead = stream.read(buffer, this.length, length);
+        if (bytesRead > 0) {
+            this.length += bytesRead;
+        }
+
+        return bytesRead;
+    }
+
+    /**
+     * Appends the big-endian encoded bytes of the provided integer to this byte
+     * string builder.
+     *
+     * @param i
+     *            The integer whose big-endian encoding is to be appended to
+     *            this byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendInt(int i) {
+        ensureAdditionalCapacity(4);
+        for (int j = length + 3; j >= length; j--) {
+            buffer[j] = (byte) i;
+            i >>>= 8;
+        }
+        length += 4;
+        return this;
+    }
+
+    /**
+     * Appends the big-endian encoded bytes of the provided long to this byte
+     * string builder.
+     *
+     * @param l
+     *            The long whose big-endian encoding is to be appended to this
+     *            byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendLong(long l) {
+        ensureAdditionalCapacity(8);
+        for (int i = length + 7; i >= length; i--) {
+            buffer[i] = (byte) l;
+            l >>>= 8;
+        }
+        length += 8;
+        return this;
+    }
+
+    /**
+     * Appends the compact encoded bytes of the provided unsigned long to this byte
+     * string builder. This method allows to encode unsigned long up to 56 bits using
+     * fewer bytes (from 1 to 8) than append(long). The encoding has the important
+     * property that it preserves ordering, so it can be used for keys.
+     *
+     * @param value
+     *            The long whose compact encoding is to be appended to this
+     *            byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendCompactUnsigned(long value) {
+        Reject.ifFalse(value >= 0, "value must be >= 0");
+        try {
+            PackedLong.writeCompactUnsigned(asOutputStream(), value);
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+        return this;
+    }
+
+    /**
+     * Appends the byte string representation of the provided object to this
+     * byte string builder. The object is converted to a byte string as follows:
+     * <ul>
+     * <li>if the object is an instance of {@code ByteSequence} then this method
+     * is equivalent to calling {@link #appendBytes(ByteSequence)}
+     * <li>if the object is a {@code byte[]} then this method is equivalent to
+     * calling {@link #appendBytes(byte[])}
+     * <li>if the object is a {@code char[]} then this method is equivalent to
+     * calling {@link #appendUtf8(char[])}
+     * <li>for all other types of object this method is equivalent to calling
+     * {@link #appendUtf8(String)} with the {@code toString()} representation of the
+     * provided object.
+     * </ul>
+     * <b>Note:</b> this method treats {@code Long} and {@code Integer} objects
+     * like any other type of {@code Object}. More specifically, the following
+     * invocations are not equivalent:
+     * <ul>
+     * <li>{@code append(0)} is not equivalent to {@code append((Object) 0)}
+     * <li>{@code append(0L)} is not equivalent to {@code append((Object) 0L)}
+     * </ul>
+     *
+     * @param o
+     *            The object to be appended to this byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendObject(final Object o) {
+        if (o == null) {
+            return this;
+        } else if (o instanceof ByteSequence) {
+            return appendBytes((ByteSequence) o);
+        } else if (o instanceof byte[]) {
+            return appendBytes((byte[]) o);
+        } else if (o instanceof char[]) {
+            return appendUtf8((char[]) o);
+        } else {
+            return appendUtf8(o.toString());
+        }
+    }
+
+    /**
+     * Appends the big-endian encoded bytes of the provided short to this byte
+     * string builder.
+     * <p>
+     * Note: this method accepts an {@code int} for ease of reading and writing.
+     * <p>
+     * This method only keeps the lowest 16-bits of the provided {@code int}.
+     * Higher bits will be truncated. This method performs the equivalent of:
+     *
+     * <pre>
+     * int i = ...;
+     * int i16bits = i & 0xFFFF;
+     * // only use "i16bits"
+     * </pre>
+     * OR
+     * <pre>
+     * int i = ...;
+     * short s = (short) i;
+     * // only use "s"
+     * </pre>
+     *
+     * @param i
+     *            The short whose big-endian encoding is to be appended to this
+     *            byte string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendShort(int i) {
+        ensureAdditionalCapacity(2);
+        for (int j = length + 1; j >= length; j--) {
+            buffer[j] = (byte) i;
+            i >>>= 8;
+        }
+        length += 2;
+        return this;
+    }
+
+    /**
+     * Appends the UTF-8 encoded bytes of the provided string to this byte
+     * string builder.
+     *
+     * @param s
+     *            The string whose UTF-8 encoding is to be appended to this byte
+     *            string builder.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendUtf8(final String s) {
+        if (s == null) {
+            return this;
+        }
+
+        // Assume that each char is 1 byte
+        final int len = s.length();
+        ensureAdditionalCapacity(len);
+
+        for (int i = 0; i < len; i++) {
+            final char c = s.charAt(i);
+            final byte b = (byte) (c & 0x0000007F);
+
+            if (c == b) {
+                buffer[this.length + i] = b;
+            } else {
+                // There is a multi-byte char. Defer to JDK
+                try {
+                    return appendBytes(s.getBytes("UTF-8"));
+                } catch (final UnsupportedEncodingException e) {
+                    // TODO: I18N
+                    throw new RuntimeException("Unable to encode String '" + s + "' to UTF-8 bytes", e);
+                }
+            }
+        }
+
+        // The 1 byte char assumption was correct
+        this.length += len;
+        return this;
+    }
+
+    /**
+     * Appends the ASN.1 BER length encoding representation of the provided
+     * integer to this byte string builder.
+     *
+     * @param length
+     *            The value to encode using the BER length encoding rules.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder appendBERLength(final int length) {
+        if ((length & 0x0000007F) == length) {
+            ensureAdditionalCapacity(1);
+
+            buffer[this.length++] = (byte) length;
+        } else if ((length & 0x000000FF) == length) {
+            ensureAdditionalCapacity(2);
+
+            buffer[this.length++] = (byte) 0x81;
+            buffer[this.length++] = (byte) length;
+        } else if ((length & 0x0000FFFF) == length) {
+            ensureAdditionalCapacity(3);
+
+            buffer[this.length++] = (byte) 0x82;
+            buffer[this.length++] = (byte) (length >> 8);
+            buffer[this.length++] = (byte) length;
+        } else if ((length & 0x00FFFFFF) == length) {
+            ensureAdditionalCapacity(4);
+
+            buffer[this.length++] = (byte) 0x83;
+            buffer[this.length++] = (byte) (length >> 16);
+            buffer[this.length++] = (byte) (length >> 8);
+            buffer[this.length++] = (byte) length;
+        } else {
+            ensureAdditionalCapacity(5);
+
+            buffer[this.length++] = (byte) 0x84;
+            buffer[this.length++] = (byte) (length >> 24);
+            buffer[this.length++] = (byte) (length >> 16);
+            buffer[this.length++] = (byte) (length >> 8);
+            buffer[this.length++] = (byte) length;
+        }
+        return this;
+    }
+
+    /**
+     * Returns an {@link OutputStream} whose write operations append data to
+     * this byte string builder. The returned output stream will never throw an
+     * {@link IOException} and its {@link OutputStream#close() close} method
+     * does not do anything.
+     *
+     * @return An {@link OutputStream} whose write operations append data to
+     *         this byte string builder.
+     */
+    public OutputStream asOutputStream() {
+        if (os == null) {
+            os = new OutputStreamImpl();
+        }
+        return os;
+    }
+
+    /**
+     * Returns a {@link ByteSequenceReader} which can be used to incrementally
+     * read and decode data from this byte string builder.
+     * <p>
+     * <b>NOTE:</b> all concurrent updates to this byte string builder are
+     * supported with the exception of {@link #clear()}. Any invocations of
+     * {@link #clear()} must be accompanied by a subsequent call to
+     * {@code ByteSequenceReader.rewind()}.
+     *
+     * @return The {@link ByteSequenceReader} which can be used to incrementally
+     *         read and decode data from this byte string builder.
+     * @see #clear()
+     */
+    @Override
+    public ByteSequenceReader asReader() {
+        return new ByteSequenceReader(this);
+    }
+
+    @Override
+    public byte byteAt(final int index) {
+        if (index >= length || index < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        return buffer[index];
+    }
+
+    /**
+     * Returns the current capacity of this byte string builder. The capacity
+     * may increase as more data is appended.
+     *
+     * @return The current capacity of this byte string builder.
+     */
+    public int capacity() {
+        return buffer.length;
+    }
+
+    /**
+     * Sets the length of this byte string builder to zero.
+     * <p>
+     * <b>NOTE:</b> if this method is called, then
+     * {@code ByteSequenceReader.rewind()} must also be called on any associated
+     * byte sequence readers in order for them to remain valid.
+     *
+     * @return This byte string builder.
+     * @see #asReader()
+     */
+    public ByteStringBuilder clear() {
+        length = 0;
+        return this;
+    }
+
+    /**
+     * Sets the length of this byte string builder to zero, and resets the
+     * capacity to the specified size if above provided threshold.
+     * <p>
+     * <b>NOTE:</b> if this method is called, then
+     * {@code ByteSequenceReader.rewind()} must also be called on any associated
+     * byte sequence readers in order for them to remain valid.
+     *
+     * @param thresholdCapacity
+     *             The threshold capacity triggering a truncate
+     * @param newCapacity
+     *            The new capacity.
+     * @return This byte string builder.
+     * @throws IllegalArgumentException
+     *             If the {@code newCapacity} is negative or the {@code newCapacity}
+     *             is bigger than the {@code thresholdCapacity}.
+     * @see #asReader()
+     */
+    public ByteStringBuilder clearAndTruncate(int thresholdCapacity, int newCapacity) {
+        if (newCapacity > thresholdCapacity) {
+            throw new IllegalArgumentException("new capacity '" + newCapacity
+                    + "' cannot be bigger than threshold capacity '" + thresholdCapacity + "'");
+        }
+        if (newCapacity < 0) {
+            throw new IllegalArgumentException("new capacity '" + newCapacity + "' cannot be negative.");
+        }
+        if (buffer.length > thresholdCapacity) {
+            // garbage collect excessively large buffers
+            buffer = new byte[newCapacity];
+        }
+        length = 0;
+        return this;
+    }
+
+    @Override
+    public int compareTo(final byte[] bytes, final int offset, final int length) {
+        ByteString.checkArrayBounds(bytes, offset, length);
+        return ByteString.compareTo(this.buffer, 0, this.length, bytes, offset, length);
+    }
+
+    @Override
+    public int compareTo(final ByteSequence o) {
+        if (this == o) {
+            return 0;
+        }
+        return -o.compareTo(buffer, 0, length);
+    }
+
+    @Override
+    public byte[] copyTo(final byte[] bytes) {
+        copyTo(bytes, 0);
+        return bytes;
+    }
+
+    @Override
+    public byte[] copyTo(final byte[] bytes, final int offset) {
+        if (offset < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        System.arraycopy(buffer, 0, bytes, offset, Math.min(length, bytes.length - offset));
+        return bytes;
+    }
+
+    @Override
+    public ByteBuffer copyTo(final ByteBuffer byteBuffer) {
+        byteBuffer.put(buffer, 0, length);
+        return byteBuffer;
+    }
+
+    @Override
+    public ByteStringBuilder copyTo(final ByteStringBuilder builder) {
+        builder.appendBytes(buffer, 0, length);
+        return builder;
+    }
+
+    @Override
+    public boolean copyTo(CharBuffer charBuffer, CharsetDecoder decoder) {
+        return ByteString.copyTo(ByteBuffer.wrap(buffer, 0, length), charBuffer, decoder);
+    }
+
+    @Override
+    public OutputStream copyTo(final OutputStream stream) throws IOException {
+        stream.write(buffer, 0, length);
+        return stream;
+    }
+
+    /**
+     * Copies the entire contents of this byte string to the provided
+     * {@code WritableByteChannel}.
+     *
+     * @param channel
+     *            The {@code WritableByteChannel} to copy to.
+     * @return The number of bytes written, possibly zero
+     * @throws IOException
+     *             If some other I/O error occurs
+     * @see WritableByteChannel#write(java.nio.ByteBuffer)
+     */
+    public int copyTo(WritableByteChannel channel) throws IOException {
+        return channel.write(ByteBuffer.wrap(buffer, 0, length));
+    }
+
+    /**
+     * Ensures that the specified number of additional bytes will fit in this
+     * byte string builder and resizes it if necessary.
+     *
+     * @param size
+     *            The number of additional bytes.
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder ensureAdditionalCapacity(final int size) {
+        final int newCount = this.length + size;
+        if (newCount > buffer.length) {
+            final byte[] newbuffer = new byte[Math.max(buffer.length << 1, newCount)];
+            System.arraycopy(buffer, 0, newbuffer, 0, buffer.length);
+            buffer = newbuffer;
+        }
+        return this;
+    }
+
+    @Override
+    public boolean equals(final byte[] bytes, final int offset, final int length) {
+        ByteString.checkArrayBounds(bytes, offset, length);
+        return ByteString.equals(this.buffer, 0, this.length, bytes, offset, length);
+    }
+
+    /**
+     * Indicates whether the provided object is equal to this byte string
+     * builder. In order for it to be considered equal, the provided object must
+     * be a byte sequence containing the same bytes in the same order.
+     *
+     * @param o
+     *            The object for which to make the determination.
+     * @return {@code true} if the provided object is a byte sequence whose
+     *         content is equal to that of this byte string builder, or
+     *         {@code false} if not.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof ByteSequence) {
+            final ByteSequence other = (ByteSequence) o;
+            return other.equals(buffer, 0, length);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the byte array that backs this byte string builder. Modifications
+     * to this byte string builder's content may cause the returned array's
+     * content to be modified, and vice versa.
+     * <p>
+     * Note that the length of the returned array is only guaranteed to be the
+     * same as the length of this byte string builder immediately after a call
+     * to {@link #trimToSize()}.
+     * <p>
+     * In addition, subsequent modifications to this byte string builder may
+     * cause the backing byte array to be reallocated thus decoupling the
+     * returned byte array from this byte string builder.
+     *
+     * @return The byte array that backs this byte string builder.
+     */
+    public byte[] getBackingArray() {
+        return buffer;
+    }
+
+    /**
+     * Returns a hash code for this byte string builder. It will be the sum of
+     * all of the bytes contained in the byte string builder.
+     * <p>
+     * <b>NOTE:</b> subsequent changes to this byte string builder will
+     * invalidate the returned hash code.
+     *
+     * @return A hash code for this byte string builder.
+     */
+    @Override
+    public int hashCode() {
+        return ByteString.hashCode(buffer, 0, length);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return length == 0;
+    }
+
+    @Override
+    public int length() {
+        return length;
+    }
+
+    /**
+     * Sets the byte value at the specified index.
+     * <p>
+     * An index ranges from zero to {@code length() - 1}. The first byte value
+     * of the sequence is at index zero, the next at index one, and so on, as
+     * for array indexing.
+     *
+     * @param index
+     *            The index of the byte to be set.
+     * @param b
+     *            The byte to set on this byte string builder.
+     * @throws IndexOutOfBoundsException
+     *             If the index argument is negative or not less than length().
+     */
+    public void setByte(final int index, final byte b) {
+        if (index >= length || index < 0) {
+            throw new IndexOutOfBoundsException();
+        }
+        buffer[index] = b;
+    }
+
+    /**
+     * Sets the length of this byte string builder.
+     * <p>
+     * If the <code>newLength</code> argument is less than the current length,
+     * the length is changed to the specified length.
+     * <p>
+     * If the <code>newLength</code> argument is greater than or equal to the
+     * current length, then the capacity is increased and sufficient null bytes
+     * are appended so that length becomes the <code>newLength</code> argument.
+     * <p>
+     * The <code>newLength</code> argument must be greater than or equal to
+     * <code>0</code>.
+     *
+     * @param newLength
+     *            The new length.
+     * @return This byte string builder.
+     * @throws IndexOutOfBoundsException
+     *             If the <code>newLength</code> argument is negative.
+     */
+    public ByteStringBuilder setLength(final int newLength) {
+        if (newLength < 0) {
+            throw new IndexOutOfBoundsException("Negative newLength: " + newLength);
+        }
+
+        if (newLength > length) {
+            ensureAdditionalCapacity(newLength - length);
+
+            // Pad with zeros.
+            for (int i = length; i < newLength; i++) {
+                buffer[i] = 0;
+            }
+        }
+        length = newLength;
+
+        return this;
+    }
+
+    /**
+     * Returns a new byte sequence that is a subsequence of this byte sequence.
+     * <p>
+     * The subsequence starts with the byte value at the specified {@code start}
+     * index and ends with the byte value at index {@code end - 1}. The length
+     * (in bytes) of the returned sequence is {@code end - start}, so if
+     * {@code start
+     * == end} then an empty sequence is returned.
+     * <p>
+     * <b>NOTE:</b> the returned sub-sequence will be robust against all updates
+     * to the byte string builder except for invocations of the method
+     * {@link #clear()}. If a permanent immutable byte sequence is required then
+     * callers should invoke {@code toByteString()} on the returned byte
+     * sequence.
+     *
+     * @param start
+     *            The start index, inclusive.
+     * @param end
+     *            The end index, exclusive.
+     * @return The newly created byte subsequence.
+     */
+    @Override
+    public ByteSequence subSequence(final int start, final int end) {
+        if (start < 0 || start > end || end > length) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        return new SubSequence(start, end - start);
+    }
+
+    @Override
+    public boolean startsWith(ByteSequence prefix) {
+        if (prefix == null || prefix.length() > length) {
+            return false;
+        }
+        return prefix.equals(buffer, 0, prefix.length());
+    }
+
+    @Override
+    public String toBase64String() {
+        return Base64.encode(this);
+    }
+
+    @Override
+    public byte[] toByteArray() {
+        return copyTo(new byte[length]);
+    }
+
+    /**
+     * Returns the {@link ByteString} representation of this byte string
+     * builder. Subsequent changes to this byte string builder will not modify
+     * the returned {@link ByteString}.
+     *
+     * @return The {@link ByteString} representation of this byte sequence.
+     */
+    @Override
+    public ByteString toByteString() {
+        final byte[] b = new byte[length];
+        System.arraycopy(buffer, 0, b, 0, length);
+        return ByteString.wrap(b);
+    }
+
+    @Override
+    public String toString() {
+        return ByteString.toString(buffer, 0, length);
+    }
+
+    /**
+     * Attempts to reduce storage used for this byte string builder. If the
+     * buffer is larger than necessary to hold its current sequence of bytes,
+     * then it may be resized to become more space efficient.
+     *
+     * @return This byte string builder.
+     */
+    public ByteStringBuilder trimToSize() {
+        if (buffer.length > length) {
+            final byte[] newBuffer = new byte[length];
+            System.arraycopy(buffer, 0, newBuffer, 0, length);
+            buffer = newBuffer;
+        }
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CachedConnectionPool.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CachedConnectionPool.java
new file mode 100644
index 0000000..728123e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CachedConnectionPool.java
@@ -0,0 +1,925 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.util.promise.Promises.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.time.TimeService;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * A connection pool implementation which maintains a cache of pooled
+ * connections with a configurable core pool size, maximum size, and expiration
+ * policy.
+ */
+final class CachedConnectionPool implements ConnectionPool {
+
+    /**
+     * This success handler is invoked when an attempt to add a new connection
+     * to the pool completes.
+     */
+    private final class ConnectionResultHandler implements ResultHandler<Connection> {
+        @Override
+        public void handleResult(final Connection connection) {
+            logger.debug(LocalizableMessage.raw(
+                    "Connection attempt succeeded:  availableConnections=%d, maxPoolSize=%d",
+                     currentPoolSize(), maxPoolSize));
+            pendingConnectionAttempts.decrementAndGet();
+            publishConnection(connection);
+        }
+    }
+
+    /**
+     * This failure handler is invoked when an attempt to add a new connection
+     * to the pool ended in error.
+     */
+    private final class ConnectionFailureHandler implements ExceptionHandler<LdapException> {
+        @Override
+        public void handleException(final LdapException exception) {
+            // Connection attempt failed, so decrease the pool size.
+            pendingConnectionAttempts.decrementAndGet();
+            availableConnections.release();
+
+            logger.debug(LocalizableMessage.raw(
+                    "Connection attempt failed: availableConnections=%d, maxPoolSize=%d",
+                    currentPoolSize(), maxPoolSize, exception));
+
+            /*
+             * There may be many pending promises waiting for a connection
+             * attempt to succeed. In some situations the number of pending
+             * promises may exceed the pool size and the number of outstanding
+             * connection attempts. If only one pending promises is resolved per
+             * failed connection attempt then some pending promises will be left
+             * unresolved. Therefore, a failed connection attempt must fail all
+             * pending promises, even if some of the subsequent connection
+             * attempts succeed, which is unlikely (if one fails, then they are
+             * all likely to fail).
+             */
+            final List<QueueElement> waitingPromises = new LinkedList<>();
+            synchronized (queue) {
+                while (hasWaitingPromises()) {
+                    waitingPromises.add(queue.removeFirst());
+                }
+            }
+            for (QueueElement waitingPromise : waitingPromises) {
+                waitingPromise.getWaitingPromise().handleException(exception);
+            }
+        }
+    }
+
+    /**
+     * A pooled connection is passed to the client. It wraps an underlying
+     * "pooled" connection obtained from the underlying factory and lasts until
+     * the client application closes this connection. More specifically, pooled
+     * connections are not actually stored in the internal queue.
+     */
+    class PooledConnection implements Connection, ConnectionEventListener {
+        private final Connection connection;
+        private LdapException error;
+        private final AtomicBoolean isClosed = new AtomicBoolean(false);
+        private boolean isDisconnectNotification;
+        private List<ConnectionEventListener> listeners;
+        private final Object stateLock = new Object();
+
+        PooledConnection(final Connection connection) {
+            this.connection = connection;
+        }
+
+        @Override
+        public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+            return checkState().abandonAsync(request);
+        }
+
+        @Override
+        public Result add(final AddRequest request) throws LdapException {
+            return checkState().add(request);
+        }
+
+        @Override
+        public Result add(final Entry entry) throws LdapException {
+            return checkState().add(entry);
+        }
+
+        @Override
+        public Result add(final String... ldifLines) throws LdapException {
+            return checkState().add(ldifLines);
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(AddRequest request) {
+            return addAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().addAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public void addConnectionEventListener(final ConnectionEventListener listener) {
+            Reject.ifNull(listener);
+            final boolean notifyClose;
+            final boolean notifyErrorOccurred;
+            synchronized (stateLock) {
+                notifyClose = isClosed.get();
+                notifyErrorOccurred = error != null;
+                if (!notifyClose) {
+                    if (listeners == null) {
+                        /*
+                         * Create and register first listener. If an error has
+                         * already occurred on the underlying connection, then
+                         * the listener may be immediately invoked so ensure
+                         * that it is already in the list.
+                         */
+                        listeners = new CopyOnWriteArrayList<>();
+                        listeners.add(listener);
+                        connection.addConnectionEventListener(this);
+                    } else {
+                        listeners.add(listener);
+                    }
+                }
+            }
+            if (notifyErrorOccurred) {
+                listener.handleConnectionError(isDisconnectNotification, error);
+            }
+            if (notifyClose) {
+                listener.handleConnectionClosed();
+            }
+        }
+
+        @Override
+        public Result applyChange(final ChangeRecord request) throws LdapException {
+            return checkState().applyChange(request);
+        }
+
+        @Override
+        public LdapPromise<Result> applyChangeAsync(final ChangeRecord request) {
+            return checkState().applyChangeAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<Result> applyChangeAsync(final ChangeRecord request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().applyChangeAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public BindResult bind(final BindRequest request) throws LdapException {
+            return checkState().bind(request);
+        }
+
+        @Override
+        public BindResult bind(final String name, final char[] password) throws LdapException {
+            return checkState().bind(name, password);
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(BindRequest request) {
+            return bindAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().bindAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public void close() {
+            final List<ConnectionEventListener> tmpListeners;
+            synchronized (stateLock) {
+                if (!isClosed.compareAndSet(false, true)) {
+                    // Already closed.
+                    return;
+                }
+                tmpListeners = listeners;
+            }
+
+            /*
+             * Remove underlying listener if needed and do this before
+             * subsequent connection events may occur.
+             */
+            if (tmpListeners != null) {
+                connection.removeConnectionEventListener(this);
+            }
+
+            // Don't put invalid connections back in the pool.
+            if (connection.isValid()) {
+                publishConnection(connection);
+            } else {
+                /*
+                 * The connection may have been disconnected by the remote
+                 * server, but the server may still be available. In order to
+                 * avoid leaving pending promises hanging indefinitely, we should
+                 * try to reconnect immediately. No need to release/acquire
+                 * availableConnections.
+                 */
+                connection.close();
+                pendingConnectionAttempts.incrementAndGet();
+                factory.getConnectionAsync().thenOnResult(connectionResultHandler)
+                                            .thenOnException(connectionFailureHandler);
+
+                logger.debug(LocalizableMessage.raw(
+                        "Connection no longer valid: availableConnections=%d, maxPoolSize=%d",
+                        currentPoolSize(), maxPoolSize));
+            }
+
+            // Invoke listeners.
+            if (tmpListeners != null) {
+                for (final ConnectionEventListener listener : tmpListeners) {
+                    listener.handleConnectionClosed();
+                }
+            }
+        }
+
+        @Override
+        public void close(final UnbindRequest request, final String reason) {
+            close();
+        }
+
+        @Override
+        public CompareResult compare(final CompareRequest request) throws LdapException {
+            return checkState().compare(request);
+        }
+
+        @Override
+        public CompareResult compare(final String name, final String attributeDescription,
+                final String assertionValue) throws LdapException {
+            return checkState().compare(name, attributeDescription, assertionValue);
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(CompareRequest request) {
+            return compareAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().compareAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public Result delete(final DeleteRequest request) throws LdapException {
+            return checkState().delete(request);
+        }
+
+        @Override
+        public Result delete(final String name) throws LdapException {
+            return checkState().delete(name);
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(DeleteRequest request) {
+            return deleteAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().deleteAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public Result deleteSubtree(final String name) throws LdapException {
+            return checkState().deleteSubtree(name);
+        }
+
+        @Override
+        public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request) throws LdapException {
+            return checkState().extendedRequest(request);
+        }
+
+        @Override
+        public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
+                final IntermediateResponseHandler handler) throws LdapException {
+            return checkState().extendedRequest(request, handler);
+        }
+
+        @Override
+        public GenericExtendedResult extendedRequest(final String requestName,
+                final ByteString requestValue) throws LdapException {
+            return checkState().extendedRequest(requestName, requestValue);
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request) {
+            return extendedRequestAsync(request, null);
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().extendedRequestAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public void handleConnectionClosed() {
+            /*
+             * The underlying connection was closed by the client. This can only
+             * occur when the pool is being shut down and the underlying
+             * connection is not in use.
+             */
+            throw new IllegalStateException(
+                    "Pooled connection received unexpected close notification");
+        }
+
+        @Override
+        public void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) {
+            final List<ConnectionEventListener> tmpListeners;
+            synchronized (stateLock) {
+                tmpListeners = listeners;
+                this.isDisconnectNotification = isDisconnectNotification;
+                this.error = error;
+            }
+            if (tmpListeners != null) {
+                for (final ConnectionEventListener listener : tmpListeners) {
+                    listener.handleConnectionError(isDisconnectNotification, error);
+                }
+            }
+        }
+
+        @Override
+        public void handleUnsolicitedNotification(final ExtendedResult notification) {
+            final List<ConnectionEventListener> tmpListeners;
+            synchronized (stateLock) {
+                tmpListeners = listeners;
+            }
+            if (tmpListeners != null) {
+                for (final ConnectionEventListener listener : tmpListeners) {
+                    listener.handleUnsolicitedNotification(notification);
+                }
+            }
+        }
+
+        @Override
+        public boolean isClosed() {
+            return isClosed.get();
+        }
+
+        @Override
+        public boolean isValid() {
+            return connection.isValid() && !isClosed();
+        }
+
+        @Override
+        public Result modify(final ModifyRequest request) throws LdapException {
+            return checkState().modify(request);
+        }
+
+        @Override
+        public Result modify(final String... ldifLines) throws LdapException {
+            return checkState().modify(ldifLines);
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(ModifyRequest request) {
+            return modifyAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().modifyAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public Result modifyDN(final ModifyDNRequest request) throws LdapException {
+            return checkState().modifyDN(request);
+        }
+
+        @Override
+        public Result modifyDN(final String name, final String newRDN) throws LdapException {
+            return checkState().modifyDN(name, newRDN);
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request) {
+            return modifyDNAsync(request, null);
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+            return checkState().modifyDNAsync(request, intermediateResponseHandler);
+        }
+
+        @Override
+        public SearchResultEntry readEntry(final DN name, final String... attributeDescriptions)
+                throws LdapException {
+            return checkState().readEntry(name, attributeDescriptions);
+        }
+
+        @Override
+        public SearchResultEntry readEntry(final String name, final String... attributeDescriptions)
+                throws LdapException {
+            return checkState().readEntry(name, attributeDescriptions);
+        }
+
+        @Override
+        public LdapPromise<SearchResultEntry> readEntryAsync(final DN name,
+                final Collection<String> attributeDescriptions) {
+            return checkState().readEntryAsync(name, attributeDescriptions);
+        }
+
+        @Override
+        public void removeConnectionEventListener(final ConnectionEventListener listener) {
+            Reject.ifNull(listener);
+            synchronized (stateLock) {
+                if (listeners != null) {
+                    listeners.remove(listener);
+                }
+            }
+        }
+
+        @Override
+        public ConnectionEntryReader search(final SearchRequest request) {
+            return checkState().search(request);
+        }
+
+        @Override
+        public Result search(final SearchRequest request, final Collection<? super SearchResultEntry> entries)
+                throws LdapException {
+            return checkState().search(request, entries);
+        }
+
+        @Override
+        public Result search(final SearchRequest request, final Collection<? super SearchResultEntry> entries,
+                final Collection<? super SearchResultReference> references) throws LdapException {
+            return checkState().search(request, entries, references);
+        }
+
+        @Override
+        public Result search(final SearchRequest request, final SearchResultHandler handler)
+                throws LdapException {
+            return checkState().search(request, handler);
+        }
+
+        @Override
+        public ConnectionEntryReader search(final String baseObject, final SearchScope scope, final String filter,
+                final String... attributeDescriptions) {
+            return checkState().search(baseObject, scope, filter, attributeDescriptions);
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(SearchRequest request, SearchResultHandler resultHandler) {
+            return searchAsync(request, null, resultHandler);
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(final SearchRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
+            return checkState().searchAsync(request, intermediateResponseHandler, entryHandler);
+        }
+
+        @Override
+        public SearchResultEntry searchSingleEntry(final SearchRequest request) throws LdapException {
+            return checkState().searchSingleEntry(request);
+        }
+
+        @Override
+        public SearchResultEntry searchSingleEntry(final String baseObject, final SearchScope scope,
+                final String filter, final String... attributeDescriptions) throws LdapException {
+            return checkState().searchSingleEntry(baseObject, scope, filter, attributeDescriptions);
+        }
+
+        @Override
+        public LdapPromise<SearchResultEntry> searchSingleEntryAsync(final SearchRequest request) {
+            return checkState().searchSingleEntryAsync(request);
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append("PooledConnection(");
+            builder.append(connection);
+            builder.append(')');
+            return builder.toString();
+        }
+
+        /** Checks that this pooled connection has not been closed. */
+        private Connection checkState() {
+            if (isClosed()) {
+                throw new IllegalStateException();
+            }
+            return connection;
+        }
+    }
+
+    /**
+     * Scheduled task responsible for purging non-core pooled connections which
+     * have been idle for longer than the idle timeout limit.
+     */
+    private final class PurgeIdleConnectionsTask implements Runnable {
+        @Override
+        public void run() {
+            final List<Connection> idleConnections;
+            synchronized (queue) {
+                if (isClosed) {
+                    return;
+                }
+
+                /*
+                 * Obtain a list of expired connections but don't close them yet
+                 * since we don't want to hold the lock too long.
+                 */
+                idleConnections = new LinkedList<>();
+                final long timeoutMillis = timeService.now() - idleTimeoutMillis;
+                int nonCoreConnectionCount = currentPoolSize() - corePoolSize;
+                for (QueueElement holder = queue.peek(); nonCoreConnectionCount > 0
+                        && isTimedOutQueuedConnection(holder, timeoutMillis); holder = queue.peek()) {
+                    idleConnections.add(holder.getWaitingConnection());
+                    queue.poll();
+                    availableConnections.release();
+                    nonCoreConnectionCount--;
+                }
+            }
+
+            // Close the idle connections.
+            if (!idleConnections.isEmpty()) {
+                logger.debug(LocalizableMessage.raw(
+                        "Closing %d idle pooled connections: availableConnections=%d, maxPoolSize=%d",
+                        idleConnections.size(), currentPoolSize(), maxPoolSize));
+                for (final Connection connection : idleConnections) {
+                    connection.close();
+                }
+            }
+        }
+
+        private boolean isTimedOutQueuedConnection(final QueueElement holder, final long timeoutMillis) {
+            return holder != null && !holder.isWaitingPromise() && holder.hasTimedOut(timeoutMillis);
+        }
+    }
+
+    private final class DebugEnabledPooledConnection extends PooledConnection {
+        private final StackTraceElement[] stackTrace;
+
+        private DebugEnabledPooledConnection(final Connection connection,
+                final StackTraceElement[] stackTrace) {
+            super(connection);
+            this.stackTrace = stackTrace;
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            if (!isClosed()) {
+                logIfDebugEnabled("CONNECTION POOL: connection leaked! It was allocated here: ", stackTrace);
+            }
+        }
+    }
+
+    /**
+     * A queue element is either a pending connection request promise awaiting an
+     * {@code Connection} or it is an unused {@code Connection} awaiting a
+     * connection request.
+     */
+    private static final class QueueElement {
+        private final long timestampMillis;
+        private final Object value;
+        private final StackTraceElement[] stack;
+
+        QueueElement(final Connection connection, final long timestampMillis) {
+            this.value = connection;
+            this.timestampMillis = timestampMillis;
+            this.stack = null;
+        }
+
+        QueueElement(final long timestampMillis, final StackTraceElement[] stack) {
+            this.value = PromiseImpl.create();
+            this.timestampMillis = timestampMillis;
+            this.stack = stack;
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(value);
+        }
+
+        StackTraceElement[] getStackTrace() {
+            return stack;
+        }
+
+        Connection getWaitingConnection() {
+            if (value instanceof Connection) {
+                return (Connection) value;
+            } else {
+                throw new IllegalStateException();
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        PromiseImpl<Connection, LdapException> getWaitingPromise() {
+            return (PromiseImpl<Connection, LdapException>) value;
+        }
+
+        boolean hasTimedOut(final long timeLimitMillis) {
+            return timestampMillis < timeLimitMillis;
+        }
+
+        boolean isWaitingPromise() {
+            return value instanceof PromiseImpl;
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * This is package private in order to allow unit tests to inject fake time
+     * stamps.
+     */
+    TimeService timeService = TimeService.SYSTEM;
+
+    private final Semaphore availableConnections;
+    private final ResultHandler<Connection> connectionResultHandler = new ConnectionResultHandler();
+    private final ExceptionHandler<LdapException> connectionFailureHandler = new ConnectionFailureHandler();
+    private final int corePoolSize;
+    private final ConnectionFactory factory;
+    private boolean isClosed;
+    private final ScheduledFuture<?> idleTimeoutFuture;
+    private final long idleTimeoutMillis;
+    private final int maxPoolSize;
+    private final LinkedList<QueueElement> queue = new LinkedList<>();
+    private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler;
+
+    /**
+     * The number of new connections which are in the process of being
+     * established.
+     */
+    private final AtomicInteger pendingConnectionAttempts = new AtomicInteger();
+
+    CachedConnectionPool(final ConnectionFactory factory, final int corePoolSize,
+            final int maximumPoolSize, final long idleTimeout, final TimeUnit unit,
+            final ScheduledExecutorService scheduler) {
+        Reject.ifNull(factory);
+        Reject.ifFalse(corePoolSize >= 0, "corePoolSize < 0");
+        Reject.ifFalse(maximumPoolSize > 0, "maxPoolSize <= 0");
+        Reject.ifFalse(corePoolSize <= maximumPoolSize, "corePoolSize > maxPoolSize");
+        Reject.ifFalse(idleTimeout >= 0, "idleTimeout < 0");
+        Reject.ifFalse(idleTimeout == 0 || unit != null, "time unit is null");
+
+        this.factory = factory;
+        this.corePoolSize = corePoolSize;
+        this.maxPoolSize = maximumPoolSize;
+        this.availableConnections = new Semaphore(maximumPoolSize);
+
+        if (corePoolSize < maximumPoolSize && idleTimeout > 0) {
+            // Dynamic pool.
+            this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(scheduler);
+            this.idleTimeoutMillis = unit.toMillis(idleTimeout);
+            this.idleTimeoutFuture =
+                    this.scheduler.get().scheduleWithFixedDelay(new PurgeIdleConnectionsTask(),
+                            idleTimeout, idleTimeout, unit);
+        } else {
+            // Fixed pool.
+            this.scheduler = null;
+            this.idleTimeoutMillis = 0;
+            this.idleTimeoutFuture = null;
+        }
+    }
+
+    @Override
+    public void close() {
+        final LinkedList<Connection> idleConnections;
+        synchronized (queue) {
+            if (isClosed) {
+                return;
+            }
+            isClosed = true;
+
+            /*
+             * Remove any connections which are waiting in the queue as these
+             * can be closed immediately.
+             */
+            idleConnections = new LinkedList<>();
+            while (hasWaitingConnections()) {
+                final QueueElement holder = queue.removeFirst();
+                idleConnections.add(holder.getWaitingConnection());
+                availableConnections.release();
+            }
+        }
+
+        logger.debug(LocalizableMessage.raw(
+                "Connection pool is closing: availableConnections=%d, maxPoolSize=%d",
+                currentPoolSize(), maxPoolSize));
+
+        if (idleTimeoutFuture != null) {
+            idleTimeoutFuture.cancel(false);
+            scheduler.release();
+        }
+
+        // Close all idle connections.
+        for (final Connection connection : idleConnections) {
+            connection.close();
+        }
+
+        // Close the underlying factory.
+        factory.close();
+    }
+
+    @Override
+    public Connection getConnection() throws LdapException {
+        try {
+            return getConnectionAsync().getOrThrow();
+        } catch (final InterruptedException e) {
+            throw newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
+        }
+    }
+
+    @Override
+    public Promise<Connection, LdapException> getConnectionAsync() {
+        // Loop while iterating through stale connections (see OPENDJ-590).
+        for (;;) {
+            final QueueElement holder;
+            synchronized (queue) {
+                if (isClosed) {
+                    throw new IllegalStateException("CachedConnectionPool is already closed");
+                } else if (hasWaitingConnections()) {
+                    holder = queue.removeFirst();
+                } else {
+                    holder = new QueueElement(timeService.now(), getStackTraceIfDebugEnabled());
+                    queue.add(holder);
+                }
+            }
+
+            if (holder.isWaitingPromise()) {
+                // Grow the pool if needed.
+                final Promise<Connection, LdapException> promise = holder.getWaitingPromise();
+                if (!promise.isDone() && availableConnections.tryAcquire()) {
+                    pendingConnectionAttempts.incrementAndGet();
+                    factory.getConnectionAsync().thenOnResult(connectionResultHandler)
+                                                .thenOnException(connectionFailureHandler);
+                }
+                return promise;
+            }
+
+            // There was a completed connection attempt.
+            final Connection connection = holder.getWaitingConnection();
+            if (connection.isValid()) {
+                final Connection pooledConnection = newPooledConnection(connection, getStackTraceIfDebugEnabled());
+                return newResultPromise(pooledConnection);
+            } else {
+                // Close the stale connection and try again.
+                connection.close();
+                availableConnections.release();
+
+                logger.debug(LocalizableMessage.raw("Connection no longer valid: availableConnections=%d, poolSize=%d",
+                        currentPoolSize(), maxPoolSize));
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        final int size = currentPoolSize();
+        final int pending = pendingConnectionAttempts.get();
+        int in = 0;
+        int blocked = 0;
+        synchronized (queue) {
+            for (QueueElement qe : queue) {
+                if (qe.isWaitingPromise()) {
+                    blocked++;
+                } else {
+                    in++;
+                }
+            }
+        }
+        final int out = size - in - pending;
+        return String.format("CachedConnectionPool(size=%d[in:%d + out:%d + "
+                + "pending:%d], maxSize=%d, blocked=%d, factory=%s)", size, in, out, pending,
+                maxPoolSize, blocked, String.valueOf(factory));
+    }
+
+    /**
+     * Provide a finalizer because connection pools are expensive resources to
+     * accidentally leave around. Also, since they won't be created all that
+     * frequently, there's little risk of overloading the finalizer.
+     */
+    @Override
+    protected void finalize() throws Throwable {
+        close();
+    }
+
+    /** Package private for unit testing. */
+    int currentPoolSize() {
+        return maxPoolSize - availableConnections.availablePermits();
+    }
+
+    private boolean hasWaitingConnections() {
+        return !queue.isEmpty() && !queue.getFirst().isWaitingPromise();
+    }
+
+    private boolean hasWaitingPromises() {
+        return !queue.isEmpty() && queue.getFirst().isWaitingPromise();
+    }
+
+    private void publishConnection(final Connection connection) {
+        final QueueElement holder;
+        boolean connectionPoolIsClosing = false;
+
+        synchronized (queue) {
+            if (hasWaitingPromises()) {
+                connectionPoolIsClosing = isClosed;
+                holder = queue.removeFirst();
+            } else if (isClosed) {
+                connectionPoolIsClosing = true;
+                holder = null;
+            } else {
+                holder = new QueueElement(connection, timeService.now());
+                queue.add(holder);
+                return;
+            }
+        }
+
+        // There was waiting promise, so complete it.
+        if (connectionPoolIsClosing) {
+            // The connection will be closed, so decrease the pool size.
+            availableConnections.release();
+            connection.close();
+
+            logger.debug(LocalizableMessage.raw(
+                    "Closing connection because connection pool is closing: availableConnections=%d, maxPoolSize=%d",
+                    currentPoolSize(), maxPoolSize));
+
+            if (holder != null) {
+                final LdapException e =
+                        newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED,
+                                ERR_CONNECTION_POOL_CLOSING.get(toString()).toString());
+                holder.getWaitingPromise().handleException(e);
+
+                logger.debug(LocalizableMessage.raw(
+                        "Connection attempt failed: availableConnections=%d, maxPoolSize=%d",
+                        currentPoolSize(), maxPoolSize, e));
+            }
+        } else {
+            holder.getWaitingPromise().handleResult(
+                    newPooledConnection(connection, holder.getStackTrace()));
+        }
+    }
+
+    private PooledConnection newPooledConnection(final Connection connection,
+            final StackTraceElement[] stack) {
+        if (!DEBUG_ENABLED) {
+            return new PooledConnection(connection);
+        } else {
+            return new DebugEnabledPooledConnection(connection, stack);
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelRequestListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelRequestListener.java
new file mode 100644
index 0000000..1ccd502
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelRequestListener.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.EventListener;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * An object that registers to be notified when a cancellation request has been
+ * received and processing of the request should be aborted if possible.
+ * <p>
+ * Requests may be cancelled as a result of an abandon request or a cancel
+ * extended request sent from the client, or by the server itself (e.g. during
+ * server shutdown).
+ */
+public interface CancelRequestListener extends EventListener {
+    /**
+     * Invoked when a cancellation request has been received and processing of
+     * the request should be aborted if possible.
+     * <p>
+     * Requests may be cancelled as a result of an abandon request or a cancel
+     * extended request sent from the client, or by the server itself (e.g.
+     * during server shutdown).
+     * <p>
+     * Implementations should, if possible, abort further processing of the
+     * request and return an appropriate cancellation result.
+     *
+     * @param cancellationReason
+     *            A message describing the reason why the request is being
+     *            cancelled.
+     */
+    void handleCancelRequest(LocalizableMessage cancellationReason);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelledResultException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelledResultException.java
new file mode 100644
index 0000000..88d4a48
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CancelledResultException.java
@@ -0,0 +1,38 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * was cancelled. More specifically, this exception is used for the following
+ * error result codes:
+ * <ul>
+ * <li>{@link ResultCode#CANCELLED CANCELLED} - the requested operation was
+ * cancelled.
+ * <li>{@link ResultCode#CLIENT_SIDE_USER_CANCELLED CLIENT_SIDE_USER_CANCELLED}
+ * - the requested operation was cancelled by the user.
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class CancelledResultException extends LdapException {
+    CancelledResultException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
new file mode 100644
index 0000000..2354990
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
@@ -0,0 +1,127 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.util.StaticUtils.getProvider;
+
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+
+/**
+ * Common options for LDAP clients and listeners.
+ */
+abstract class CommonLDAPOptions {
+    /**
+     * Specifies the class loader which will be used to load the
+     * {@link org.forgerock.opendj.ldap.spi.TransportProvider TransportProvider}.
+     * <p>
+     * By default the default class loader will be used.
+     * <p>
+     * The transport provider is loaded using {@code java.util.ServiceLoader},
+     * the JDK service-provider loading facility. The provider must be
+     * accessible from the same class loader that was initially queried to
+     * locate the configuration file; note that this is not necessarily the
+     * class loader from which the file was actually loaded. This method allows
+     * to provide a class loader to be used for loading the provider.
+     *
+     */
+    public static final Option<ClassLoader> TRANSPORT_PROVIDER_CLASS_LOADER = Option.of(ClassLoader.class, null);
+
+    /**
+     * Specifies the name of the provider to use for transport.
+     * <p>
+     * Transport providers implement {@link org.forgerock.opendj.ldap.spi.TransportProvider TransportProvider}
+     * interface.
+     * <p>
+     * The name should correspond to the name of an existing provider, as
+     * returned by {@code TransportProvider#getName()} method.
+     */
+    public static final Option<String> TRANSPORT_PROVIDER = Option.of(String.class, null);
+
+    /**
+     * Specifies the transport provider to use. This option is internal and only intended for testing.
+     */
+    static final Option<TransportProvider> TRANSPORT_PROVIDER_INSTANCE = Option.of(TransportProvider.class, null);
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#TCP_NODELAY
+     * TCP_NODELAY} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.tcpNoDelay} property.
+     */
+    public static final Option<Boolean> TCP_NO_DELAY = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.tcpNoDelay", true));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_REUSEADDR
+     * SO_REUSEADDR} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.reuseAddress} property.
+     *
+     */
+    public static final Option<Boolean> SO_REUSE_ADDRESS = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.reuseAddress", true));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_LINGER
+     * SO_LINGER} socket option for new connections.
+     * <p>
+     * The default setting is {@code -1} (disabled) and may be configured using
+     * the {@code org.forgerock.opendj.io.linger} property.
+     */
+    public static final Option<Integer> SO_LINGER_IN_SECONDS = Option.withDefault(
+        getIntProperty("org.forgerock.opendj.io.linger", -1));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_KEEPALIVE
+     * SO_KEEPALIVE} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.keepAlive} property.
+     */
+    public static final Option<Boolean> SO_KEEPALIVE = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.keepAlive", true));
+
+    /** Sets the decoding options which will be used to control how requests and responses are decoded. */
+    public static final Option<DecodeOptions> LDAP_DECODE_OPTIONS = Option.withDefault(new DecodeOptions());
+
+    static TransportProvider getTransportProvider(final Options options) {
+        final TransportProvider transportProvider = options.get(TRANSPORT_PROVIDER_INSTANCE);
+        if (transportProvider != null) {
+            return transportProvider;
+        }
+        return getProvider(TransportProvider.class,
+                           options.get(TRANSPORT_PROVIDER),
+                           options.get(TRANSPORT_PROVIDER_CLASS_LOADER));
+    }
+
+    static boolean getBooleanProperty(final String name, final boolean defaultValue) {
+        final String value = System.getProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    static int getIntProperty(final String name, final int defaultValue) {
+        final String value = System.getProperty(name);
+        try {
+            return value != null ? Integer.parseInt(value) : defaultValue;
+        } catch (final NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConditionResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConditionResult.java
new file mode 100644
index 0000000..aa5f38f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConditionResult.java
@@ -0,0 +1,235 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * The result of a tri-state logical expression. Condition results are used to
+ * represent the result of a conditional evaluation that can yield three
+ * possible values: {@code FALSE} (i.e. "no"), {@code TRUE} (i.e. "yes"), or
+ * {@code UNDEFINED} (i.e. "maybe"). A result of {@code UNDEFINED} indicates
+ * that further investigation may be required.
+ */
+public enum ConditionResult {
+    /**
+     * Indicates that the condition evaluated to {@code false}.
+     */
+    FALSE("false"),
+
+    /**
+     * Indicates that the condition could not be evaluated and its result is
+     * undefined.
+     */
+    UNDEFINED("undefined"),
+
+    /**
+     * Indicates that the condition evaluated to {@code true}.
+     */
+    TRUE("true");
+
+    /** Boolean -> ConditionResult map. */
+    private static final boolean[] BOOLEAN_MAP = { false, false, true };
+
+    /** AND truth table. */
+    private static final ConditionResult[][] LOGICAL_AND = { { FALSE, FALSE, FALSE },
+        { FALSE, UNDEFINED, UNDEFINED }, { FALSE, UNDEFINED, TRUE }, };
+
+    /** NOT truth table. */
+    private static final ConditionResult[] LOGICAL_NOT = { TRUE, UNDEFINED, FALSE };
+
+    /** OR truth table. */
+    private static final ConditionResult[][] LOGICAL_OR = { { FALSE, UNDEFINED, TRUE },
+        { UNDEFINED, UNDEFINED, TRUE }, { TRUE, TRUE, TRUE }, };
+
+    /**
+     * Returns the logical AND of zero condition results, which is always
+     * {@code TRUE}.
+     *
+     * @return The logical OR of zero condition results, which is always
+     *         {@code TRUE}.
+     */
+    public static ConditionResult and() {
+        return TRUE;
+    }
+
+    /**
+     * Returns the logical AND of the provided condition result, which is always
+     * {@code r}.
+     *
+     * @param r
+     *            The condition result.
+     * @return The logical AND of the provided condition result, which is always
+     *         {@code r}.
+     */
+    public static ConditionResult and(final ConditionResult r) {
+        return r;
+    }
+
+    /**
+     * Returns the logical AND of the provided condition results, which is
+     * {@code TRUE} if all of the provided condition results are {@code TRUE},
+     * {@code FALSE} if at least one of them is {@code FALSE}, and
+     * {@code UNDEFINED} otherwise. Note that {@code TRUE} is returned if the
+     * provided list of results is empty.
+     *
+     * @param results
+     *            The condition results to be compared.
+     * @return The logical AND of the provided condition results.
+     */
+    public static ConditionResult and(final ConditionResult... results) {
+        ConditionResult finalResult = TRUE;
+        for (final ConditionResult result : results) {
+            finalResult = and(finalResult, result);
+            if (finalResult == FALSE) {
+                break;
+            }
+        }
+        return finalResult;
+    }
+
+    /**
+     * Returns the logical AND of the provided condition results, which is
+     * {@code TRUE} if both of the provided condition results are {@code TRUE},
+     * {@code FALSE} if at least one of them is {@code FALSE} , and
+     * {@code UNDEFINED} otherwise.
+     *
+     * @param r1
+     *            The first condition result to be compared.
+     * @param r2
+     *            The second condition result to be compared.
+     * @return The logical AND of the provided condition results.
+     */
+    public static ConditionResult and(final ConditionResult r1, final ConditionResult r2) {
+        return LOGICAL_AND[r1.ordinal()][r2.ordinal()];
+    }
+
+    /**
+     * Returns the logical NOT of the provided condition result, which is
+     * {@code TRUE} if the provided condition result is {@code FALSE},
+     * {@code TRUE} if it is {@code FALSE}, and {@code UNDEFINED} otherwise.
+     *
+     * @param r
+     *            The condition result to invert.
+     * @return The logical NOT of the provided condition result.
+     */
+    public static ConditionResult not(final ConditionResult r) {
+        return LOGICAL_NOT[r.ordinal()];
+    }
+
+    /**
+     * Returns the logical OR of zero condition results, which is always
+     * {@code FALSE}.
+     *
+     * @return The logical OR of zero condition results, which is always
+     *         {@code FALSE}.
+     */
+    public static ConditionResult or() {
+        return FALSE;
+    }
+
+    /**
+     * Returns the logical OR of the provided condition result, which is always
+     * {@code r}.
+     *
+     * @param r
+     *            The condition result.
+     * @return The logical OR of the provided condition result, which is always
+     *         {@code r}.
+     */
+    public static ConditionResult or(final ConditionResult r) {
+        return r;
+    }
+
+    /**
+     * Returns the logical OR of the provided condition results, which is
+     * {@code FALSE} if all of the provided condition results are {@code FALSE},
+     * {@code TRUE} if at least one of them is {@code TRUE}, and
+     * {@code UNDEFINED} otherwise. Note that {@code FALSE} is returned if the
+     * provided list of results is empty.
+     *
+     * @param results
+     *            The condition results to be compared.
+     * @return The logical OR of the provided condition results.
+     */
+    public static ConditionResult or(final ConditionResult... results) {
+        ConditionResult finalResult = FALSE;
+        for (final ConditionResult result : results) {
+            finalResult = or(finalResult, result);
+            if (finalResult == TRUE) {
+                break;
+            }
+        }
+        return finalResult;
+    }
+
+    /**
+     * Returns the logical OR of the provided condition results, which is
+     * {@code FALSE} if both of the provided condition results are {@code FALSE}
+     * , {@code TRUE} if at least one of them is {@code TRUE} , and
+     * {@code UNDEFINED} otherwise.
+     *
+     * @param r1
+     *            The first condition result to be compared.
+     * @param r2
+     *            The second condition result to be compared.
+     * @return The logical OR of the provided condition results.
+     */
+    public static ConditionResult or(final ConditionResult r1, final ConditionResult r2) {
+        return LOGICAL_OR[r1.ordinal()][r2.ordinal()];
+    }
+
+    /**
+     * Returns the condition result which is equivalent to the provided boolean
+     * value.
+     *
+     * @param b
+     *            The boolean value.
+     * @return {@code TRUE} if {@code b} was {@code true}, otherwise
+     *         {@code FALSE} .
+     */
+    public static ConditionResult valueOf(final boolean b) {
+        return b ? TRUE : FALSE;
+    }
+
+    /** The human-readable name for this result. */
+    private final String resultName;
+
+    /** Prevent instantiation. */
+    private ConditionResult(final String resultName) {
+        this.resultName = resultName;
+    }
+
+    /**
+     * Converts this condition result to a boolean value. {@code FALSE} and
+     * {@code UNDEFINED} are both converted to {@code false}, and {@code TRUE}
+     * is converted to {@code true}.
+     *
+     * @return The boolean equivalent of this condition result.
+     */
+    public boolean toBoolean() {
+        return BOOLEAN_MAP[ordinal()];
+    }
+
+    /**
+     * Returns the string representation of this condition result.
+     *
+     * @return The string representation of his condition result.
+     */
+    @Override
+    public String toString() {
+        return resultName;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connection.java
new file mode 100644
index 0000000..9336052
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connection.java
@@ -0,0 +1,1453 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.Closeable;
+import java.util.Collection;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+
+/**
+ * A connection with a Directory Server over which read and update operations
+ * may be performed. See RFC 4511 for the LDAPv3 protocol specification and more
+ * information about the types of operations defined in LDAP.
+ * <p>
+ * <h3>Operation processing</h3>
+ * <p>
+ * Operations may be performed synchronously or asynchronously depending on the
+ * method chosen. Asynchronous methods can be identified by their {@code Async}
+ * suffix.
+ * <p>
+ * <h4>Performing operations synchronously</h4>
+ * <p>
+ * Synchronous methods block until a response is received from the Directory
+ * Server, at which point an appropriate {@link Result} object is returned if
+ * the operation succeeded, or thrown as an {@link LdapException} if the
+ * operation failed.
+ * <p>
+ * Since synchronous operations block the calling thread, the only way to
+ * abandon a long running operation is to interrupt the calling thread from
+ * another thread. This will cause the calling thread unblock and throw a
+ * {@link CancelledResultException} whose cause is the underlying
+ * {@link InterruptedException}.
+ * <p>
+ * <h4>Performing operations asynchronously</h4>
+ * <p>
+ * Asynchronous methods, identified by their {@code Async} suffix, are
+ * non-blocking, returning a {@link LdapPromise} or sub-type thereof which can
+ * be used for retrieving the result using the {@link LdapPromise#get} method.
+ * Operation failures, for whatever reason, are signaled by the
+ * {@link LdapPromise#get()} method throwing an {@link LdapException}.
+ * <p>
+ * In addition to returning a {@link LdapPromise}, all asynchronous methods
+ * accept a {@link LdapResultHandler} which will be notified upon completion of the
+ * operation.
+ * <p>
+ * Synchronous operations are easily simulated by immediately getting the
+ * result:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * AddRequest request = ...;
+ * // Will block until operation completes, and
+ * // throws exception on failure.
+ * connection.add(request).get();
+ * </pre>
+ *
+ * Operations can be performed in parallel while taking advantage of the
+ * simplicity of a synchronous application design:
+ *
+ * <pre>
+ * Connection connection1 = ...;
+ * Connection connection2 = ...;
+ * AddRequest request = ...;
+ * // Add the entry to the first server (don't block).
+ * LdapPromise promise1 = connection1.add(request);
+ * // Add the entry to the second server (in parallel).
+ * LdapPromise promise2 = connection2.add(request);
+ * // Total time = is O(1) instead of O(n).
+ * promise1.get();
+ * promise2.get();
+ * </pre>
+ *
+ * More complex client applications can take advantage of a fully asynchronous
+ * event driven design using {@link LdapResultHandler}s:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * SearchRequest request = ...;
+ * // Process results in the search result handler
+ * // in a separate thread.
+ * SearchResponseHandler handle = ...;
+ * connection.search(request, handler);
+ * </pre>
+ * <p>
+ * <h3>Closing connections</h3>
+ * <p>
+ * Applications must ensure that a connection is closed by calling
+ * {@link #close()} even if a fatal error occurs on the connection. Once a
+ * connection has been closed by the client application, any attempts to
+ * continue to use the connection will result in an
+ * {@link IllegalStateException} being thrown. Note that, if a fatal error is
+ * encountered on the connection, then the application can continue to use the
+ * connection. In this case all requests subsequent to the failure will fail
+ * with an appropriate {@link LdapException} when their result is
+ * retrieved.
+ * <p>
+ * <h3>Event notification</h3>
+ * <p>
+ * Applications can choose to be notified when a connection is closed by the
+ * application, receives an unsolicited notification, or experiences a fatal
+ * error by registering a {@link ConnectionEventListener} with the connection
+ * using the {@link #addConnectionEventListener} method.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
+ *      Directory Access Protocol (LDAP): The Protocol </a>
+ */
+public interface Connection extends Closeable {
+
+    /**
+     * Abandons the unfinished operation identified in the provided abandon
+     * request.
+     * <p>
+     * Abandon requests do not have a response, so invoking the method get() on
+     * the returned promise will not block, nor return anything (it is Void), but
+     * may throw an exception if a problem occurred while sending the abandon
+     * request. In addition the returned promise may be used in order to
+     * determine the message ID of the abandon request.
+     * <p>
+     * <b>Note:</b> a more convenient approach to abandoning unfinished
+     * asynchronous operations is provided via the
+     * {@link LdapPromise#cancel(boolean)} method.
+     *
+     * @param request
+     *            The request identifying the operation to be abandoned.
+     * @return A promise whose result is Void.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support abandon operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Void> abandonAsync(AbandonRequest request);
+
+    /**
+     * Adds an entry to the Directory Server using the provided add request.
+     *
+     * @param request
+     *            The add request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support add operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result add(AddRequest request) throws LdapException;
+
+    /**
+     * Adds the provided entry to the Directory Server.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * AddRequest request = new AddRequest(entry);
+     * connection.add(request);
+     * </pre>
+     *
+     * @param entry
+     *            The entry to be added.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support add operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null} .
+     */
+    Result add(Entry entry) throws LdapException;
+
+    /**
+     * Adds an entry to the Directory Server using the provided lines of LDIF.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * AddRequest request = new AddRequest(ldifLines);
+     * connection.add(request);
+     * </pre>
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing the an LDIF add change record or an
+     *            LDIF entry record.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support add operations.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    Result add(String... ldifLines) throws LdapException;
+
+    /**
+     * Asynchronously adds an entry to the Directory Server using the provided
+     * add request.
+     *
+     * @param request
+     *            The add request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support add operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> addAsync(AddRequest request);
+
+    /**
+     * Asynchronously adds an entry to the Directory Server using the provided
+     * add request.
+     *
+     * @param request
+     *            The add request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support add operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Registers the provided connection event listener so that it will be
+     * notified when this connection is closed by the application, receives an
+     * unsolicited notification, or experiences a fatal error.
+     *
+     * @param listener
+     *            The listener which wants to be notified when events occur on
+     *            this connection.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     */
+    void addConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Applies the provided change request to the Directory Server.
+     *
+     * @param request
+     *            The change request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support the provided change
+     *             request.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result applyChange(ChangeRecord request) throws LdapException;
+
+    /**
+     * Asynchronously applies the provided change request to the Directory
+     * Server.
+     *
+     * @param request
+     *            The change request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support the provided change
+     *             request.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> applyChangeAsync(ChangeRecord request);
+
+    /**
+     * Asynchronously applies the provided change request to the Directory
+     * Server.
+     *
+     * @param request
+     *            The change request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support the provided change
+     *             request.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> applyChangeAsync(ChangeRecord request,
+        IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Authenticates to the Directory Server using the provided bind request.
+     *
+     * @param request
+     *            The bind request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    BindResult bind(BindRequest request) throws LdapException;
+
+    /**
+     * Authenticates to the Directory Server using simple authentication and the
+     * provided user name and password.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * BindRequest request = new SimpleBindRequest(name, password);
+     * connection.bind(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the Directory object that the client
+     *            wishes to bind as, which may be empty.
+     * @param password
+     *            The password of the Directory object that the client wishes to
+     *            bind as, which may be empty.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code name} or {@code password} was {@code null}.
+     */
+    BindResult bind(String name, char[] password) throws LdapException;
+
+    /**
+     * Asynchronously authenticates to the Directory Server using the provided
+     * bind request.
+     *
+     * @param request
+     *            The bind request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<BindResult> bindAsync(BindRequest request);
+
+    /**
+     * Asynchronously authenticates to the Directory Server using the provided
+     * bind request.
+     *
+     * @param request
+     *            The bind request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<BindResult> bindAsync(BindRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Releases any resources associated with this connection. For physical
+     * connections to a Directory Server this will mean that an unbind request
+     * is sent and the underlying socket is closed.
+     * <p>
+     * Other connection implementations may behave differently, and may choose
+     * not to send an unbind request if its use is inappropriate (for example a
+     * pooled connection will be released and returned to its connection pool
+     * without ever issuing an unbind request).
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * UnbindRequest request = new UnbindRequest();
+     * connection.close(request);
+     * </pre>
+     *
+     * Calling {@code close} on a connection that is already closed has no
+     * effect.
+     *
+     * @see Connections#uncloseable(Connection)
+     */
+    @Override
+    void close();
+
+    /**
+     * Releases any resources associated with this connection. For physical
+     * connections to a Directory Server this will mean that the provided unbind
+     * request is sent and the underlying socket is closed.
+     * <p>
+     * Other connection implementations may behave differently, and may choose
+     * to ignore the provided unbind request if its use is inappropriate (for
+     * example a pooled connection will be released and returned to its
+     * connection pool without ever issuing an unbind request).
+     * <p>
+     * Calling {@code close} on a connection that is already closed has no
+     * effect.
+     *
+     * @param request
+     *            The unbind request to use in the case where a physical
+     *            connection is closed.
+     * @param reason
+     *            A reason describing why the connection was closed.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    void close(UnbindRequest request, String reason);
+
+    /**
+     * Compares an entry in the Directory Server using the provided compare
+     * request.
+     *
+     * @param request
+     *            The compare request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    CompareResult compare(CompareRequest request) throws LdapException;
+
+    /**
+     * Compares the named entry in the Directory Server against the provided
+     * attribute value assertion.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * CompareRequest request = new CompareRequest(name, attributeDescription, assertionValue);
+     * connection.compare(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be compared.
+     * @param attributeDescription
+     *            The name of the attribute to be compared.
+     * @param assertionValue
+     *            The assertion value to be compared.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} or {@code AttributeDescription} could not be
+     *             decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code name}, {@code attributeDescription}, or
+     *             {@code assertionValue} was {@code null}.
+     */
+    CompareResult compare(String name, String attributeDescription, String assertionValue)
+            throws LdapException;
+
+    /**
+     * Asynchronously compares an entry in the Directory Server using the
+     * provided compare request.
+     *
+     * @param request
+     *            The compare request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<CompareResult> compareAsync(CompareRequest request);
+
+    /**
+     * Asynchronously compares an entry in the Directory Server using the
+     * provided compare request.
+     *
+     * @param request
+     *            The compare request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<CompareResult> compareAsync(CompareRequest request,
+        IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Deletes an entry from the Directory Server using the provided delete
+     * request.
+     *
+     * @param request
+     *            The delete request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result delete(DeleteRequest request) throws LdapException;
+
+    /**
+     * Deletes the named entry from the Directory Server.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * DeleteRequest request = new DeleteRequest(name);
+     * connection.delete(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be deleted.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    Result delete(String name) throws LdapException;
+
+    /**
+     * Deletes the named entry and all of its subordinates from the Directory
+     * Server.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * DeleteRequest request = new DeleteRequest(name).addControl(
+     * connection.delete(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the subtree base entry to be
+     *            deleted.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    Result deleteSubtree(String name) throws LdapException;
+
+    /**
+     * Asynchronously deletes an entry from the Directory Server using the
+     * provided delete request.
+     *
+     * @param request
+     *            The delete request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> deleteAsync(DeleteRequest request);
+
+    /**
+     * Asynchronously deletes an entry from the Directory Server using the
+     * provided delete request.
+     *
+     * @param request
+     *            The delete request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> deleteAsync(DeleteRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Requests that the Directory Server performs the provided extended
+     * request.
+     *
+     * @param <R>
+     *            The type of result returned by the extended request.
+     * @param request
+     *            The extended request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    <R extends ExtendedResult> R extendedRequest(ExtendedRequest<R> request) throws LdapException;
+
+    /**
+     * Requests that the Directory Server performs the provided extended
+     * request, optionally listening for any intermediate responses.
+     *
+     * @param <R>
+     *            The type of result returned by the extended request.
+     * @param request
+     *            The extended request.
+     * @param handler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    <R extends ExtendedResult> R extendedRequest(ExtendedRequest<R> request, IntermediateResponseHandler handler)
+            throws LdapException;
+
+    /**
+     * Requests that the Directory Server performs the provided extended
+     * request.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * GenericExtendedRequest request = new GenericExtendedRequest(requestName, requestValue);
+     * connection.extendedRequest(request);
+     * </pre>
+     *
+     * @param requestName
+     *            The dotted-decimal representation of the unique OID
+     *            corresponding to the extended request.
+     * @param requestValue
+     *            The content of the extended request in a form defined by the
+     *            extended operation, or {@code null} if there is no content.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code requestName} was {@code null}.
+     */
+    GenericExtendedResult extendedRequest(String requestName, ByteString requestValue) throws LdapException;
+
+    /**
+     * Asynchronously performs the provided extended request in the Directory
+     * Server.
+     *
+     * @param <R>
+     *            The type of result returned by the extended request.
+     * @param request
+     *            The extended request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request);
+
+    /**
+     * Asynchronously performs the provided extended request in the Directory
+     * Server.
+     *
+     * @param <R>
+     *            The type of result returned by the extended request.
+     * @param request
+     *            The extended request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
+        IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Indicates whether or not this connection has been explicitly closed by
+     * calling {@code close}. This method will not return {@code true} if a
+     * fatal error has occurred on the connection unless {@code close} has been
+     * called.
+     *
+     * @return {@code true} if this connection has been explicitly closed by
+     *         calling {@code close}, or {@code false} otherwise.
+     */
+    boolean isClosed();
+
+    /**
+     * Returns {@code true} if this connection has not been closed and no fatal
+     * errors have been detected. This method is guaranteed to return
+     * {@code false} only when it is called after the method {@code close} has
+     * been called.
+     *
+     * @return {@code true} if this connection is valid, {@code false}
+     *         otherwise.
+     */
+    boolean isValid();
+
+    /**
+     * Modifies an entry in the Directory Server using the provided modify
+     * request.
+     *
+     * @param request
+     *            The modify request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result modify(ModifyRequest request) throws LdapException;
+
+    /**
+     * Modifies an entry in the Directory Server using the provided lines of
+     * LDIF.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * ModifyRequest request = new ModifyRequest(name, ldifChanges);
+     * connection.modify(request);
+     * </pre>
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing the a single LDIF modify change
+     *            record.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify operations.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    Result modify(String... ldifLines) throws LdapException;
+
+    /**
+     * Asynchronously modifies an entry in the Directory Server using the
+     * provided modify request.
+     *
+     * @param request
+     *            The modify request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> modifyAsync(ModifyRequest request);
+
+    /**
+     * Asynchronously modifies an entry in the Directory Server using the
+     * provided modify request.
+     *
+     * @param request
+     *            The modify request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> modifyAsync(ModifyRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Renames an entry in the Directory Server using the provided modify DN
+     * request.
+     *
+     * @param request
+     *            The modify DN request.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result modifyDN(ModifyDNRequest request) throws LdapException;
+
+    /**
+     * Renames the named entry in the Directory Server using the provided new
+     * RDN.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * ModifyDNRequest request = new ModifyDNRequest(name, newRDN);
+     * connection.modifyDN(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be renamed.
+     * @param newRDN
+     *            The new RDN of the entry.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} or {@code newRDN} could not be decoded using
+     *             the default schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code name} or {@code newRDN} was {@code null}.
+     */
+    Result modifyDN(String name, String newRDN) throws LdapException;
+
+    /**
+     * Asynchronously renames an entry in the Directory Server using the
+     * provided modify DN request.
+     *
+     * @param request
+     *            The modify DN request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> modifyDNAsync(ModifyDNRequest request);
+
+    /**
+     * Asynchronously renames an entry in the Directory Server using the
+     * provided modify DN request.
+     *
+     * @param request
+     *            The modify DN request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
+        IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Reads the named entry from the Directory Server.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, this method will never return {@code null}.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * SearchRequest request = new SearchRequest(name, SearchScope.BASE_OBJECT,
+     * &quot;(objectClass=*)&quot;, attributeDescriptions);
+     * connection.searchSingleEntry(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be read.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with the entry,
+     *            which may be {@code null} or empty indicating that all user
+     *            attributes should be returned.
+     * @return The single search result entry returned from the search.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code name} was {@code null}.
+     */
+    SearchResultEntry readEntry(DN name, String... attributeDescriptions) throws LdapException;
+
+    /**
+     * Reads the named entry from the Directory Server.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, this method will never return {@code null}.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * SearchRequest request =
+     *         new SearchRequest(name, SearchScope.BASE_OBJECT, &quot;(objectClass=*)&quot;, attributeDescriptions);
+     * connection.searchSingleEntry(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be read.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with the entry.
+     * @return The single search result entry returned from the search.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code baseObject} could not be decoded using the default
+     *             schema.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code name} was {@code null}.
+     */
+    SearchResultEntry readEntry(String name, String... attributeDescriptions) throws LdapException;
+
+    /**
+     * Asynchronously reads the named entry from the Directory Server.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, the returned promise will never return {@code null}.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * SearchRequest request =
+     *         new SearchRequest(name, SearchScope.BASE_OBJECT, &quot;(objectClass=*)&quot;, attributeDescriptions);
+     * connection.searchSingleEntryAsync(request, resultHandler, p);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the entry to be read.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with the entry,
+     *            which may be {@code null} or empty indicating that all user
+     *            attributes should be returned.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code name} was {@code null}.
+     */
+    LdapPromise<SearchResultEntry> readEntryAsync(DN name, Collection<String> attributeDescriptions);
+
+    /**
+     * Removes the provided connection event listener from this connection so
+     * that it will no longer be notified when this connection is closed by the
+     * application, receives an unsolicited notification, or experiences a fatal
+     * error.
+     *
+     * @param listener
+     *            The listener which no longer wants to be notified when events
+     *            occur on this connection.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     */
+    void removeConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Searches the Directory Server using the provided search parameters. Any
+     * matching entries returned by the search will be exposed through the
+     * returned {@code ConnectionEntryReader}.
+     * <p>
+     * Unless otherwise specified, calling this method is equivalent to:
+     *
+     * <pre>
+     * ConnectionEntryReader reader = new ConnectionEntryReader(this, request);
+     * </pre>
+     *
+     * @param request
+     *            The search request.
+     * @return The result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} or {@code entries} was {@code null}.
+     */
+    ConnectionEntryReader search(SearchRequest request);
+
+    /**
+     * Searches the Directory Server using the provided search request. Any
+     * matching entries returned by the search will be added to {@code entries},
+     * even if the final search result indicates that the search failed. Search
+     * result references will be discarded.
+     * <p>
+     * <b>Warning:</b> Usage of this method is discouraged if the search request
+     * is expected to yield a large number of search results since the entire
+     * set of results will be stored in memory, potentially causing an
+     * {@code OutOfMemoryError}.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * connection.search(request, entries, null);
+     * </pre>
+     *
+     * @param request
+     *            The search request.
+     * @param entries
+     *            The collection to which matching entries should be added.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} or {@code entries} was {@code null}.
+     */
+    Result search(SearchRequest request, Collection<? super SearchResultEntry> entries) throws LdapException;
+
+    /**
+     * Searches the Directory Server using the provided search request. Any
+     * matching entries returned by the search will be added to {@code entries},
+     * even if the final search result indicates that the search failed.
+     * Similarly, search result references returned by the search will be added
+     * to {@code references}.
+     * <p>
+     * <b>Warning:</b> Usage of this method is discouraged if the search request
+     * is expected to yield a large number of search results since the entire
+     * set of results will be stored in memory, potentially causing an
+     * {@code OutOfMemoryError}.
+     *
+     * @param request
+     *            The search request.
+     * @param entries
+     *            The collection to which matching entries should be added.
+     * @param references
+     *            The collection to which search result references should be
+     *            added, or {@code null} if references are to be discarded.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} or {@code entries} was {@code null}.
+     */
+    Result search(SearchRequest request, Collection<? super SearchResultEntry> entries,
+            Collection<? super SearchResultReference> references) throws LdapException;
+
+    /**
+     * Searches the Directory Server using the provided search request. Any
+     * matching entries returned by the search as well as any search result
+     * references will be passed to the provided search result handler.
+     *
+     * @param request
+     *            The search request.
+     * @param handler
+     *            A search result handler which can be used to process the
+     *            search result entries and references as they are received, may
+     *            be {@code null}.
+     * @return The result of the operation.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    Result search(SearchRequest request, SearchResultHandler handler) throws LdapException;
+
+    /**
+     * Searches the Directory Server using the provided search parameters. Any
+     * matching entries returned by the search will be exposed through the
+     * {@code EntryReader} interface.
+     * <p>
+     * <b>Warning:</b> When using a queue with an optional capacity bound, the
+     * connection will stop reading responses and wait if necessary for space to
+     * become available.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * SearchRequest request = new SearchRequest(baseDN, scope, filter, attributeDescriptions);
+     * connection.search(request, new LinkedBlockingQueue&lt;Response&gt;());
+     * </pre>
+     *
+     * @param baseObject
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return An entry reader exposing the returned entries.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code baseObject}, {@code scope}, or {@code filter}
+     *             were {@code null}.
+     */
+    ConnectionEntryReader search(String baseObject, SearchScope scope, String filter,
+            String... attributeDescriptions);
+
+    /**
+     * Asynchronously searches the Directory Server using the provided search
+     * request.
+     *
+     * @param request
+     *            The search request.
+     * @param entryHandler
+     *            A search result handler which can be used to asynchronously
+     *            process the search result entries and references as they are
+     *            received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> searchAsync(SearchRequest request, SearchResultHandler entryHandler);
+
+    /**
+     * Asynchronously searches the Directory Server using the provided search
+     * request.
+     *
+     * @param request
+     *            The search request.
+     * @param intermediateResponseHandler
+     *            An intermediate response handler which can be used to process
+     *            any intermediate responses as they are received, may be
+     *            {@code null}.
+     * @param entryHandler
+     *            A search result handler which can be used to asynchronously
+     *            process the search result entries and references as they are
+     *            received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    LdapPromise<Result> searchAsync(SearchRequest request, IntermediateResponseHandler intermediateResponseHandler,
+        SearchResultHandler entryHandler);
+
+    /**
+     * Searches the Directory Server for a single entry using the provided
+     * search request.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, this method will never return {@code null}. If multiple
+     * matching entries are returned by the Directory Server then the request
+     * will fail with an {@link MultipleEntriesFoundException}.
+     *
+     * @param request
+     *            The search request.
+     * @return The single search result entry returned from the search.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code request} was {@code null}.
+     */
+    SearchResultEntry searchSingleEntry(SearchRequest request) throws LdapException;
+
+    /**
+     * Searches the Directory Server for a single entry using the provided
+     * search parameters.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, this method will never return {@code null}. If multiple
+     * matching entries are returned by the Directory Server then the request
+     * will fail with an {@link MultipleEntriesFoundException}.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * SearchRequest request = new SearchRequest(baseObject, scope, filter, attributeDescriptions);
+     * connection.searchSingleEntry(request);
+     * </pre>
+     *
+     * @param baseObject
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The single search result entry returned from the search.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code baseObject} could not be decoded using the default
+     *             schema or if {@code filter} is not a valid LDAP string
+     *             representation of a filter.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code baseObject}, {@code scope}, or {@code filter}
+     *             were {@code null}.
+     */
+    SearchResultEntry searchSingleEntry(String baseObject, SearchScope scope, String filter,
+            String... attributeDescriptions) throws LdapException;
+
+    /**
+     * Asynchronously searches the Directory Server for a single entry using the
+     * provided search request.
+     * <p>
+     * If the requested entry is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, the returned promise will never return {@code null}. If
+     * multiple matching entries are returned by the Directory Server then the
+     * request will fail with an {@link MultipleEntriesFoundException}.
+     *
+     * @param request
+     *            The search request.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If this connection does not support search operations.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code request} was {@code null}.
+     */
+    LdapPromise<SearchResultEntry> searchSingleEntryAsync(SearchRequest request);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionEventListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionEventListener.java
new file mode 100644
index 0000000..9cdc6e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionEventListener.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.EventListener;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * An object that registers to be notified when a connection is closed by the
+ * application, receives an unsolicited notification, or experiences a fatal
+ * error.
+ */
+public interface ConnectionEventListener extends EventListener {
+    /**
+     * Notifies this connection event listener that the application has called
+     * {@code close} on the connection. The connection event listener will be
+     * notified immediately after the application calls the {@code close} method
+     * on the associated connection.
+     */
+    void handleConnectionClosed();
+
+    /**
+     * Notifies this connection event listener that a fatal error has occurred
+     * and the connection can no longer be used - the server has crashed, for
+     * example. The connection implementation makes this notification just
+     * before it throws the provided {@link LdapException} to the
+     * application.
+     * <p>
+     * <b>Note:</b> disconnect notifications are treated as fatal connection
+     * errors and are handled by this method. In this case
+     * {@code isDisconnectNotification} will be {@code true} and {@code error}
+     * will contain the result code and any diagnostic information contained in
+     * the notification message.
+     *
+     * @param isDisconnectNotification
+     *            {@code true} if the error was triggered by a disconnect
+     *            notification sent by the server, otherwise {@code false}.
+     * @param error
+     *            The exception that is about to be thrown to the application.
+     */
+    void handleConnectionError(boolean isDisconnectNotification, LdapException error);
+
+    /**
+     * Notifies this connection event listener that the connection has just
+     * received the provided unsolicited notification from the server.
+     * <p>
+     * <b>Note:</b> disconnect notifications are treated as fatal connection
+     * errors and are handled by the {@link #handleConnectionError} method.
+     *
+     * @param notification
+     *            The unsolicited notification.
+     */
+    void handleUnsolicitedNotification(ExtendedResult notification);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionException.java
new file mode 100644
index 0000000..ab219dc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionException.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * was unsuccessful because of a connection failure.
+ */
+@SuppressWarnings("serial")
+public class ConnectionException extends LdapException {
+    ConnectionException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionFactory.java
new file mode 100644
index 0000000..a3befa9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionFactory.java
@@ -0,0 +1,93 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.Closeable;
+
+import org.forgerock.util.promise.Promise;
+
+/**
+ * A connection factory provides an interface for obtaining a connection to a
+ * Directory Server. Connection factories can be used to wrap other connection
+ * factories in order to provide enhanced capabilities in a manner which is
+ * transparent to the application. For example:
+ * <ul>
+ * <li>Connection pooling
+ * <li>Load balancing
+ * <li>Keep alive
+ * <li>Transactional connections
+ * <li>Connections to LDIF files
+ * <li>Data transformations
+ * <li>Logging connections
+ * <li>Read-only connections
+ * <li>Pre-authenticated connections
+ * <li>Recording connections, with primitive roll-back functionality
+ * </ul>
+ * An application typically obtains a connection from a connection factory,
+ * performs one or more operations, and then closes the connection. Applications
+ * should aim to close connections as soon as possible in order to avoid
+ * resource contention.
+ */
+public interface ConnectionFactory extends Closeable {
+
+    /**
+     * Releases any resources associated with this connection factory. Depending
+     * on the implementation a factory may:
+     * <ul>
+     * <li>do nothing
+     * <li>close underlying connection factories (e.g. load-balancers)
+     * <li>close pooled connections (e.g. connection pools)
+     * <li>shutdown IO event service and related thread pools (e.g. Grizzly).
+     * </ul>
+     * Calling {@code close} on a connection factory which is already closed has
+     * no effect.
+     * <p>
+     * Applications should avoid closing connection factories while there are
+     * remaining active connections in use or connection attempts in progress.
+     *
+     * @see Connections#uncloseable(ConnectionFactory)
+     */
+    @Override
+    void close();
+
+    /**
+     * Asynchronously obtains a connection to the Directory Server associated
+     * with this connection factory. The returned {@code Promise} can be used to
+     * retrieve the completed connection.
+     *
+     * @return A promise which can be used to retrieve the connection.
+     */
+    Promise<Connection, LdapException> getConnectionAsync();
+
+    /**
+     * Returns a connection to the Directory Server associated with this
+     * connection factory. The connection returned by this method can be used
+     * immediately.
+     * <p>
+     * If the calling thread is interrupted while waiting for the connection
+     * attempt to complete then the calling thread unblock and throw a
+     * {@link CancelledResultException} whose cause is the underlying
+     * {@link InterruptedException}.
+     *
+     * @return A connection to the Directory Server associated with this
+     *         connection factory.
+     * @throws LdapException
+     *             If the connection request failed for some reason.
+     */
+    Connection getConnection() throws LdapException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionLoadBalancer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionLoadBalancer.java
new file mode 100644
index 0000000..47caa2d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionLoadBalancer.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.util.promise.Promises.newExceptionPromise;
+
+import java.util.Collection;
+
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * An abstract connection based load balancer. Load balancing is performed when the application attempts to obtain a
+ * connection.
+ * <p>
+ * Implementations should override the method {@code getInitialConnectionFactoryIndex()} in order to provide the policy
+ * for selecting the first connection factory to use for each connection request.
+ */
+abstract class ConnectionLoadBalancer extends LoadBalancer {
+    ConnectionLoadBalancer(final String loadBalancerName,
+                           final Collection<? extends ConnectionFactory> factories,
+                           final Options options) {
+        super(loadBalancerName, factories, options);
+    }
+
+    @Override
+    public final Connection getConnection() throws LdapException {
+        return getMonitoredConnectionFactory(getInitialConnectionFactoryIndex()).getConnection();
+    }
+
+    @Override
+    public final Promise<Connection, LdapException> getConnectionAsync() {
+        try {
+            return getMonitoredConnectionFactory(getInitialConnectionFactoryIndex()).getConnectionAsync();
+        } catch (final LdapException e) {
+            return newExceptionPromise(e);
+        }
+    }
+
+    /**
+     * Returns the index of the first connection factory which should be used in order to satisfy the next connection
+     * request.
+     *
+     * @return The index of the first connection factory which should be used in order to satisfy the next connection
+     * request.
+     */
+    abstract int getInitialConnectionFactoryIndex();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionPool.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionPool.java
new file mode 100644
index 0000000..b898010
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionPool.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.promise.Promise;
+
+/**
+ * A connection factory which maintains and re-uses a pool of connections.
+ * Connections obtained from a connection pool are returned to the connection
+ * pool when closed, although connection pool implementations may choose to
+ * physically close the connection if needed (e.g. in order to reduce the size
+ * of the pool).
+ * <p>
+ * When connection pools are no longer needed they must be explicitly closed in
+ * order to close any remaining pooled connections.
+ * <p>
+ * Since pooled connections are re-used, applications must use operations such
+ * as binds and StartTLS with extreme caution.
+ */
+public interface ConnectionPool extends ConnectionFactory {
+    /**
+     * Releases any resources associated with this connection pool. Pooled
+     * connections will be permanently closed and this connection pool will no
+     * longer be available for use.
+     * <p>
+     * Attempts to use this connection pool after it has been closed will result
+     * in an {@code IllegalStateException}.
+     * <p>
+     * Calling {@code close} on a connection pool which is already closed has no
+     * effect.
+     */
+    @Override
+    void close();
+
+    /**
+     * Asynchronously obtains a connection from this connection pool,
+     * potentially opening a new connection if needed.
+     * <p>
+     * The returned {@code Promise} can be used to retrieve the pooled
+     * connection.
+     * <p>
+     * Closing the pooled connection will, depending on the connection pool
+     * implementation, return the connection to this pool without closing it.
+     *
+     * @return A promise which can be used to retrieve the pooled connection.
+     * @throws IllegalStateException
+     *             If this connection pool has already been closed.
+     */
+    @Override
+    Promise<Connection, LdapException> getConnectionAsync();
+
+    /**
+     * Obtains a connection from this connection pool, potentially opening a new
+     * connection if needed.
+     * <p>
+     * Closing the pooled connection will, depending on the connection pool
+     * implementation, return the connection to this pool without closing it.
+     *
+     * @return A pooled connection.
+     * @throws LdapException
+     *             If the connection request failed for some reason.
+     * @throws IllegalStateException
+     *             If this connection pool has already been closed.
+     */
+    @Override
+    Connection getConnection() throws LdapException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionSecurityLayer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionSecurityLayer.java
new file mode 100644
index 0000000..3241840
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConnectionSecurityLayer.java
@@ -0,0 +1,65 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * An interface for providing additional connection security to a connection.
+ */
+public interface ConnectionSecurityLayer {
+
+    /**
+     * Disposes of any system resources or security-sensitive information that
+     * this connection security layer might be using. Invoking this method
+     * invalidates this instance.
+     */
+    void dispose();
+
+    /**
+     * Unwraps a byte array received from the peer.
+     *
+     * @param incoming
+     *            A non-{@code null} byte array containing the encoded bytes
+     *            from the peer.
+     * @param offset
+     *            The starting position in {@code incoming} of the bytes to be
+     *            unwrapped.
+     * @param len
+     *            The number of bytes from {@code incoming} to be unwrapped.
+     * @return A non-{@code null} byte array containing the unwrapped bytes.
+     * @throws LdapException
+     *             If {@code incoming} cannot be successfully unwrapped.
+     */
+    byte[] unwrap(byte[] incoming, int offset, int len) throws LdapException;
+
+    /**
+     * Wraps a byte array to be sent to the peer.
+     *
+     * @param outgoing
+     *            A non-{@code null} byte array containing the unencoded bytes
+     *            to be sent to the peer.
+     * @param offset
+     *            The starting position in {@code outgoing} of the bytes to be
+     *            wrapped.
+     * @param len
+     *            The number of bytes from {@code outgoing} to be wrapped.
+     * @return A non-{@code null} byte array containing the wrapped bytes.
+     * @throws LdapException
+     *             If {@code outgoing} cannot be successfully wrapped.
+     */
+    byte[] wrap(byte[] outgoing, int offset, int len) throws LdapException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java
new file mode 100644
index 0000000..547cfff
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java
@@ -0,0 +1,835 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.RequestHandlerFactoryAdapter.adaptRequestHandler;
+import static org.forgerock.util.time.Duration.duration;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Collection;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.GSSAPISASLBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
+import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
+import org.forgerock.util.Function;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.time.Duration;
+
+/**
+ * This class contains methods for creating and manipulating connection
+ * factories and connections.
+ */
+public final class Connections {
+    /**
+     * Specifies the interval between successive attempts to reconnect to offline load-balanced connection factories.
+     * The default configuration is to attempt to reconnect every second.
+     */
+    public static final Option<Duration> LOAD_BALANCER_MONITORING_INTERVAL = Option.withDefault(duration("1 seconds"));
+
+    /**
+     * Specifies the event listener which should be notified whenever a load-balanced connection factory changes state
+     * from online to offline or vice-versa. By default events will be logged to the {@code LoadBalancingAlgorithm}
+     * logger using the {@link LoadBalancerEventListener#LOG_EVENTS} listener.
+     */
+    public static final Option<LoadBalancerEventListener> LOAD_BALANCER_EVENT_LISTENER =
+            Option.of(LoadBalancerEventListener.class, LoadBalancerEventListener.LOG_EVENTS);
+
+    /**
+     * Specifies the scheduler which will be used for periodically reconnecting to offline connection factories. A
+     * system-wide scheduler will be used by default.
+     */
+    public static final Option<ScheduledExecutorService> LOAD_BALANCER_SCHEDULER =
+            Option.of(ScheduledExecutorService.class, null);
+
+    /**
+     * Creates a new connection pool which creates new connections as needed
+     * using the provided connection factory, but will reuse previously
+     * allocated connections when they are available.
+     * <p>
+     * Connections which have not been used for sixty seconds are closed and
+     * removed from the pool. Thus, a pool that remains idle for long enough
+     * will not contain any cached connections.
+     * <p>
+     * Connections obtained from the connection pool are guaranteed to be valid
+     * immediately before being returned to the calling application. More
+     * specifically, connections which have remained idle in the connection pool
+     * for a long time and which have been remotely closed due to a time out
+     * will never be returned. However, once a pooled connection has been
+     * obtained it is the responsibility of the calling application to handle
+     * subsequent connection failures, these being signaled via a
+     * {@link ConnectionException}.
+     *
+     * @param factory
+     *            The connection factory to use for creating new connections.
+     * @return The new connection pool.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static ConnectionPool newCachedConnectionPool(final ConnectionFactory factory) {
+        return new CachedConnectionPool(factory, 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, null);
+    }
+
+    /**
+     * Creates a new connection pool which creates new connections as needed
+     * using the provided connection factory, but will reuse previously
+     * allocated connections when they are available.
+     * <p>
+     * Attempts to use more than {@code maximumPoolSize} connections at once
+     * will block until a connection is released back to the pool. In other
+     * words, this pool will prevent applications from using more than
+     * {@code maximumPoolSize} connections at the same time.
+     * <p>
+     * Connections which have not been used for the provided {@code idleTimeout}
+     * period are closed and removed from the pool, until there are only
+     * {@code corePoolSize} connections remaining.
+     * <p>
+     * Connections obtained from the connection pool are guaranteed to be valid
+     * immediately before being returned to the calling application. More
+     * specifically, connections which have remained idle in the connection pool
+     * for a long time and which have been remotely closed due to a time out
+     * will never be returned. However, once a pooled connection has been
+     * obtained it is the responsibility of the calling application to handle
+     * subsequent connection failures, these being signaled via a
+     * {@link ConnectionException}.
+     *
+     * @param factory
+     *            The connection factory to use for creating new connections.
+     * @param corePoolSize
+     *            The minimum number of connections to keep in the pool, even if
+     *            they are idle.
+     * @param maximumPoolSize
+     *            The maximum number of connections to allow in the pool.
+     * @param idleTimeout
+     *            The time out period, after which unused non-core connections
+     *            will be closed.
+     * @param unit
+     *            The time unit for the {@code keepAliveTime} argument.
+     * @return The new connection pool.
+     * @throws IllegalArgumentException
+     *             If {@code corePoolSize}, {@code maximumPoolSize} are less
+     *             than or equal to zero, or if {@code idleTimeout} is negative,
+     *             or if {@code corePoolSize} is greater than
+     *             {@code maximumPoolSize}, or if {@code idleTimeout} is
+     *             non-zero and {@code unit} is {@code null}.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static ConnectionPool newCachedConnectionPool(final ConnectionFactory factory,
+            final int corePoolSize, final int maximumPoolSize, final long idleTimeout,
+            final TimeUnit unit) {
+        return new CachedConnectionPool(factory, corePoolSize, maximumPoolSize, idleTimeout, unit,
+                null);
+    }
+
+    /**
+     * Creates a new connection pool which creates new connections as needed
+     * using the provided connection factory, but will reuse previously
+     * allocated connections when they are available.
+     * <p>
+     * Attempts to use more than {@code maximumPoolSize} connections at once
+     * will block until a connection is released back to the pool. In other
+     * words, this pool will prevent applications from using more than
+     * {@code maximumPoolSize} connections at the same time.
+     * <p>
+     * Connections which have not been used for the provided {@code idleTimeout}
+     * period are closed and removed from the pool, until there are only
+     * {@code corePoolSize} connections remaining.
+     * <p>
+     * Connections obtained from the connection pool are guaranteed to be valid
+     * immediately before being returned to the calling application. More
+     * specifically, connections which have remained idle in the connection pool
+     * for a long time and which have been remotely closed due to a time out
+     * will never be returned. However, once a pooled connection has been
+     * obtained it is the responsibility of the calling application to handle
+     * subsequent connection failures, these being signaled via a
+     * {@link ConnectionException}.
+     *
+     * @param factory
+     *            The connection factory to use for creating new connections.
+     * @param corePoolSize
+     *            The minimum number of connections to keep in the pool, even if
+     *            they are idle.
+     * @param maximumPoolSize
+     *            The maximum number of connections to allow in the pool.
+     * @param idleTimeout
+     *            The time out period, after which unused non-core connections
+     *            will be closed.
+     * @param unit
+     *            The time unit for the {@code keepAliveTime} argument.
+     * @param scheduler
+     *            The scheduler which should be used for periodically checking
+     *            for idle connections, or {@code null} if the default scheduler
+     *            should be used.
+     * @return The new connection pool.
+     * @throws IllegalArgumentException
+     *             If {@code corePoolSize}, {@code maximumPoolSize} are less
+     *             than or equal to zero, or if {@code idleTimeout} is negative,
+     *             or if {@code corePoolSize} is greater than
+     *             {@code maximumPoolSize}, or if {@code idleTimeout} is
+     *             non-zero and {@code unit} is {@code null}.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static ConnectionPool newCachedConnectionPool(final ConnectionFactory factory,
+            final int corePoolSize, final int maximumPoolSize, final long idleTimeout,
+            final TimeUnit unit, final ScheduledExecutorService scheduler) {
+        return new CachedConnectionPool(factory, corePoolSize, maximumPoolSize, idleTimeout, unit,
+                scheduler);
+    }
+
+    /**
+     * Creates a new connection pool which will maintain {@code poolSize}
+     * connections created using the provided connection factory.
+     * <p>
+     * Attempts to use more than {@code poolSize} connections at once will block
+     * until a connection is released back to the pool. In other words, this
+     * pool will prevent applications from using more than {@code poolSize}
+     * connections at the same time.
+     * <p>
+     * Connections obtained from the connection pool are guaranteed to be valid
+     * immediately before being returned to the calling application. More
+     * specifically, connections which have remained idle in the connection pool
+     * for a long time and which have been remotely closed due to a time out
+     * will never be returned. However, once a pooled connection has been
+     * obtained it is the responsibility of the calling application to handle
+     * subsequent connection failures, these being signaled via a
+     * {@link ConnectionException}.
+     *
+     * @param factory
+     *            The connection factory to use for creating new connections.
+     * @param poolSize
+     *            The maximum size of the connection pool.
+     * @return The new connection pool.
+     * @throws IllegalArgumentException
+     *             If {@code poolSize} is negative.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static ConnectionPool newFixedConnectionPool(final ConnectionFactory factory,
+            final int poolSize) {
+        return new CachedConnectionPool(factory, poolSize, poolSize, 0L, null, null);
+    }
+
+    /**
+     * Creates a new internal client connection which will route requests to the
+     * provided {@code RequestHandler}.
+     * <p>
+     * When processing requests, {@code RequestHandler} implementations are
+     * passed a {@code RequestContext} having a pseudo {@code requestID} which
+     * is incremented for each successive internal request on a per client
+     * connection basis. The request ID may be useful for logging purposes.
+     * <p>
+     * An internal connection does not require {@code RequestHandler}
+     * implementations to return a result when processing requests. However, it
+     * is recommended that implementations do always return results even for
+     * abandoned requests. This is because application client threads may block
+     * indefinitely waiting for results.
+     *
+     * @param requestHandler
+     *            The request handler which will be used for all client
+     *            connections.
+     * @return The new internal connection.
+     * @throws NullPointerException
+     *             If {@code requestHandler} was {@code null}.
+     */
+    public static Connection newInternalConnection(
+            final RequestHandler<RequestContext> requestHandler) {
+        Reject.ifNull(requestHandler);
+        return newInternalConnection(adaptRequestHandler(requestHandler));
+    }
+
+    /**
+     * Creates a new internal client connection which will route requests to the
+     * provided {@code ServerConnection}.
+     * <p>
+     * When processing requests, {@code ServerConnection} implementations are
+     * passed an integer as the first parameter. This integer represents a
+     * pseudo {@code requestID} which is incremented for each successive
+     * internal request on a per client connection basis. The request ID may be
+     * useful for logging purposes.
+     * <p>
+     * An internal connection does not require {@code ServerConnection}
+     * implementations to return a result when processing requests. However, it
+     * is recommended that implementations do always return results even for
+     * abandoned requests. This is because application client threads may block
+     * indefinitely waiting for results.
+     *
+     * @param serverConnection
+     *            The server connection.
+     * @return The new internal connection.
+     * @throws NullPointerException
+     *             If {@code serverConnection} was {@code null}.
+     */
+    public static Connection newInternalConnection(final ServerConnection<Integer> serverConnection) {
+        Reject.ifNull(serverConnection);
+        return new InternalConnection(serverConnection);
+    }
+
+    /**
+     * Creates a new connection factory which binds internal client connections
+     * to the provided {@link RequestHandler}s.
+     * <p>
+     * When processing requests, {@code RequestHandler} implementations are
+     * passed an integer as the first parameter. This integer represents a
+     * pseudo {@code requestID} which is incremented for each successive
+     * internal request on a per client connection basis. The request ID may be
+     * useful for logging purposes.
+     * <p>
+     * An internal connection factory does not require {@code RequestHandler}
+     * implementations to return a result when processing requests. However, it
+     * is recommended that implementations do always return results even for
+     * abandoned requests. This is because application client threads may block
+     * indefinitely waiting for results.
+     *
+     * @param requestHandler
+     *            The request handler which will be used for all client
+     *            connections.
+     * @return The new internal connection factory.
+     * @throws NullPointerException
+     *             If {@code requestHandler} was {@code null}.
+     */
+    public static ConnectionFactory newInternalConnectionFactory(
+            final RequestHandler<RequestContext> requestHandler) {
+        Reject.ifNull(requestHandler);
+        return new InternalConnectionFactory<>(
+            Connections.<Void> newServerConnectionFactory(requestHandler), null);
+    }
+
+    /**
+     * Creates a new connection factory which binds internal client connections
+     * to {@link RequestHandler}s created using the provided
+     * {@link RequestHandlerFactory}.
+     * <p>
+     * When processing requests, {@code RequestHandler} implementations are
+     * passed an integer as the first parameter. This integer represents a
+     * pseudo {@code requestID} which is incremented for each successive
+     * internal request on a per client connection basis. The request ID may be
+     * useful for logging purposes.
+     * <p>
+     * An internal connection factory does not require {@code RequestHandler}
+     * implementations to return a result when processing requests. However, it
+     * is recommended that implementations do always return results even for
+     * abandoned requests. This is because application client threads may block
+     * indefinitely waiting for results.
+     *
+     * @param <C>
+     *            The type of client context.
+     * @param factory
+     *            The request handler factory to use for creating connections.
+     * @param clientContext
+     *            The client context.
+     * @return The new internal connection factory.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static <C> ConnectionFactory newInternalConnectionFactory(
+            final RequestHandlerFactory<C, RequestContext> factory, final C clientContext) {
+        Reject.ifNull(factory);
+        return new InternalConnectionFactory<>(newServerConnectionFactory(factory), clientContext);
+    }
+
+    /**
+     * Creates a new connection factory which binds internal client connections
+     * to {@link ServerConnection}s created using the provided
+     * {@link ServerConnectionFactory}.
+     * <p>
+     * When processing requests, {@code ServerConnection} implementations are
+     * passed an integer as the first parameter. This integer represents a
+     * pseudo {@code requestID} which is incremented for each successive
+     * internal request on a per client connection basis. The request ID may be
+     * useful for logging purposes.
+     * <p>
+     * An internal connection factory does not require {@code ServerConnection}
+     * implementations to return a result when processing requests. However, it
+     * is recommended that implementations do always return results even for
+     * abandoned requests. This is because application client threads may block
+     * indefinitely waiting for results.
+     *
+     * @param <C>
+     *            The type of client context.
+     * @param factory
+     *            The server connection factory to use for creating connections.
+     * @param clientContext
+     *            The client context.
+     * @return The new internal connection factory.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static <C> ConnectionFactory newInternalConnectionFactory(
+            final ServerConnectionFactory<C, Integer> factory, final C clientContext) {
+        Reject.ifNull(factory);
+        return new InternalConnectionFactory<>(factory, clientContext);
+    }
+
+    /**
+     * Creates a new "round-robin" load-balancer which will load-balance connections across the provided set of
+     * connection factories. A round robin load balancing algorithm distributes connection requests across a list of
+     * connection factories one at a time. When the end of the list is reached, the algorithm starts again from the
+     * beginning.
+     * <p/>
+     * This algorithm is typically used for load-balancing <i>within</i> data centers, where load must be distributed
+     * equally across multiple data sources. This algorithm contrasts with the
+     * {@link #newFailoverLoadBalancer(Collection, Options)} which is used for load-balancing <i>between</i> data
+     * centers.
+     * <p/>
+     * If a problem occurs that temporarily prevents connections from being obtained for one of the connection
+     * factories, then this algorithm automatically "fails over" to the next operational connection factory in the list.
+     * If none of the connection factories are operational then a {@code ConnectionException} is returned to the
+     * client.
+     * <p/>
+     * The implementation periodically attempts to connect to failed connection factories in order to determine if they
+     * have become available again.
+     *
+     * @param factories
+     *         The connection factories.
+     * @param options
+     *         This configuration options for the load-balancer.
+     * @return The new round-robin load balancer.
+     * @see #newShardedRequestLoadBalancer(Collection, Options)
+     * @see #newFailoverLoadBalancer(Collection, Options)
+     * @see #LOAD_BALANCER_EVENT_LISTENER
+     * @see #LOAD_BALANCER_MONITORING_INTERVAL
+     * @see #LOAD_BALANCER_SCHEDULER
+     */
+    public static ConnectionFactory newRoundRobinLoadBalancer(
+            final Collection<? extends ConnectionFactory> factories, final Options options) {
+        return new ConnectionLoadBalancer("RoundRobinLoadBalancer", factories, options) {
+            private final int maxIndex = factories.size();
+            private final AtomicInteger nextIndex = new AtomicInteger(-1);
+
+            @Override
+            int getInitialConnectionFactoryIndex() {
+                // A round robin pool of one connection factories is unlikely in
+                // practice and requires special treatment.
+                if (maxIndex == 1) {
+                    return 0;
+                }
+
+                // Determine the next factory to use: avoid blocking algorithm.
+                int oldNextIndex;
+                int newNextIndex;
+                do {
+                    oldNextIndex = nextIndex.get();
+                    newNextIndex = oldNextIndex + 1;
+                    if (newNextIndex == maxIndex) {
+                        newNextIndex = 0;
+                    }
+                } while (!nextIndex.compareAndSet(oldNextIndex, newNextIndex));
+
+                // There's a potential, but benign, race condition here: other threads
+                // could jump in and rotate through the list before we return the
+                // connection factory.
+                return newNextIndex;
+            }
+        };
+    }
+
+    /**
+     * Creates a new "fail-over" load-balancer which will load-balance connections across the provided set of connection
+     * factories. A fail-over load balancing algorithm provides fault tolerance across multiple underlying connection
+     * factories.
+     * <p/>
+     * This algorithm is typically used for load-balancing <i>between</i> data centers, where there is preference to
+     * always forward connection requests to the <i>closest available</i> data center. This algorithm contrasts with the
+     * {@link #newRoundRobinLoadBalancer(Collection, Options)} which is used for load-balancing <i>within</i> a data
+     * center.
+     * <p/>
+     * This algorithm selects connection factories based on the order in which they were provided during construction.
+     * More specifically, an attempt to obtain a connection factory will always return the <i>first operational</i>
+     * connection factory in the list. Applications should, therefore, organize the connection factories such that the
+     * <i>preferred</i> (usually the closest) connection factories appear before those which are less preferred.
+     * <p/>
+     * If a problem occurs that temporarily prevents connections from being obtained for one of the connection
+     * factories, then this algorithm automatically "fails over" to the next operational connection factory in the list.
+     * If none of the connection factories are operational then a {@code ConnectionException} is returned to the
+     * client.
+     * <p/>
+     * The implementation periodically attempts to connect to failed connection factories in order to determine if they
+     * have become available again.
+     *
+     * @param factories
+     *         The connection factories.
+     * @param options
+     *         This configuration options for the load-balancer.
+     * @return The new fail-over load balancer.
+     * @see #newRoundRobinLoadBalancer(Collection, Options)
+     * @see #newShardedRequestLoadBalancer(Collection, Options)
+     * @see #LOAD_BALANCER_EVENT_LISTENER
+     * @see #LOAD_BALANCER_MONITORING_INTERVAL
+     * @see #LOAD_BALANCER_SCHEDULER
+     */
+    public static ConnectionFactory newFailoverLoadBalancer(
+            final Collection<? extends ConnectionFactory> factories, final Options options) {
+        return new ConnectionLoadBalancer("FailoverLoadBalancer", factories, options) {
+            @Override
+            int getInitialConnectionFactoryIndex() {
+                // Always start with the first connection factory.
+                return 0;
+            }
+        };
+    }
+
+    /**
+     * Creates a new "sharded" load-balancer which will load-balance individual requests across the provided set of
+     * connection factories, each typically representing a single replica, using an algorithm that ensures that requests
+     * targeting a given DN will always be routed to the same replica. In other words, this load-balancer increases
+     * consistency whilst maintaining read-scalability by simulating a "single master" replication topology, where each
+     * replica is responsible for a subset of the entries. When a replica is unavailable the load-balancer "fails over"
+     * by performing a linear probe in order to find the next available replica thus ensuring high-availability when a
+     * network partition occurs while sacrificing consistency, since the unavailable replica may still be visible to
+     * other clients.
+     * <p/>
+     * This load-balancer distributes requests based on the hash of their target DN and handles all core operations, as
+     * well as any password modify extended requests and SASL bind requests which use authentication IDs having the
+     * "dn:" form. Note that subtree operations (searches, subtree deletes, and modify DN) are likely to include entries
+     * which are "mastered" on different replicas, so client applications should be more tolerant of inconsistencies.
+     * Requests that are either unrecognized or that do not have a parameter that may be considered to be a target DN
+     * will be routed randomly.
+     * <p/>
+     * <b>NOTE:</b> this connection factory returns fake connections, since real connections are obtained for each
+     * request. Therefore, the returned fake connections have certain limitations: abandon requests will be ignored
+     * since they cannot be routed; connection event listeners can be registered, but will only be notified when the
+     * fake connection is closed or when all of the connection factories are unavailable.
+     * <p/>
+     * <b>NOTE:</b> in deployments where there are multiple client applications, care should be taken to ensure that
+     * the factories are configured using the same ordering, otherwise requests will not be routed consistently
+     * across the client applications.
+     * <p/>
+     * The implementation periodically attempts to connect to failed connection factories in order to determine if they
+     * have become available again.
+     *
+     * @param factories
+     *         The connection factories.
+     * @param options
+     *         This configuration options for the load-balancer.
+     * @return The new affinity load balancer.
+     * @see #newRoundRobinLoadBalancer(Collection, Options)
+     * @see #newFailoverLoadBalancer(Collection, Options)
+     * @see #LOAD_BALANCER_EVENT_LISTENER
+     * @see #LOAD_BALANCER_MONITORING_INTERVAL
+     * @see #LOAD_BALANCER_SCHEDULER
+     */
+    public static ConnectionFactory newShardedRequestLoadBalancer(
+            final Collection<? extends ConnectionFactory> factories, final Options options) {
+        return new RequestLoadBalancer("ShardedRequestLoadBalancer",
+                                       factories,
+                                       options,
+                                       newShardedRequestLoadBalancerFunction(factories));
+    }
+
+    // Package private for testing.
+    static Function<Request, Integer, NeverThrowsException> newShardedRequestLoadBalancerFunction(
+            final Collection<? extends ConnectionFactory> factories) {
+        return new Function<Request, Integer, NeverThrowsException>() {
+            private final int maxIndex = factories.size();
+
+            @Override
+            public Integer apply(final Request request) {
+                // Normalize the hash to a valid factory index, taking care of negative hash values and especially
+                // Integer.MIN_VALUE (see doc for Math.abs()).
+                final int index = computeIndexBasedOnDnHashCode(request);
+                return index == Integer.MIN_VALUE ? 0 : (Math.abs(index) % maxIndex);
+            }
+
+            private int computeIndexBasedOnDnHashCode(final Request request) {
+                // The following conditions are ordered such that the most common operations appear first in order to
+                // reduce the average number of branches. A better solution would be to use a visitor, but a visitor
+                // would only apply to the core operations, not extended operations or SASL binds.
+                if (request instanceof SearchRequest) {
+                    return ((SearchRequest) request).getName().hashCode();
+                } else if (request instanceof ModifyRequest) {
+                    return ((ModifyRequest) request).getName().hashCode();
+                } else if (request instanceof SimpleBindRequest) {
+                    return hashCodeOfDnString(((SimpleBindRequest) request).getName());
+                } else if (request instanceof AddRequest) {
+                    return ((AddRequest) request).getName().hashCode();
+                } else if (request instanceof DeleteRequest) {
+                    return ((DeleteRequest) request).getName().hashCode();
+                } else if (request instanceof CompareRequest) {
+                    return ((CompareRequest) request).getName().hashCode();
+                } else if (request instanceof ModifyDNRequest) {
+                    return ((ModifyDNRequest) request).getName().hashCode();
+                } else if (request instanceof PasswordModifyExtendedRequest) {
+                    return hashCodeOfAuthzid(((PasswordModifyExtendedRequest) request).getUserIdentityAsString());
+                } else if (request instanceof PlainSASLBindRequest) {
+                    return hashCodeOfAuthzid(((PlainSASLBindRequest) request).getAuthenticationID());
+                } else if (request instanceof DigestMD5SASLBindRequest) {
+                    return hashCodeOfAuthzid(((DigestMD5SASLBindRequest) request).getAuthenticationID());
+                } else if (request instanceof GSSAPISASLBindRequest) {
+                    return hashCodeOfAuthzid(((GSSAPISASLBindRequest) request).getAuthenticationID());
+                } else if (request instanceof CRAMMD5SASLBindRequest) {
+                    return hashCodeOfAuthzid(((CRAMMD5SASLBindRequest) request).getAuthenticationID());
+                } else {
+                    return distributeRequestAtRandom();
+                }
+            }
+
+            private int hashCodeOfAuthzid(final String authzid) {
+                if (authzid != null && authzid.startsWith("dn:")) {
+                    return hashCodeOfDnString(authzid.substring(3));
+                }
+                return distributeRequestAtRandom();
+            }
+
+            private int hashCodeOfDnString(final String dnString) {
+                try {
+                    return DN.valueOf(dnString).hashCode();
+                } catch (final IllegalArgumentException ignored) {
+                    return distributeRequestAtRandom();
+                }
+            }
+
+            private int distributeRequestAtRandom() {
+                return ThreadLocalRandom.current().nextInt(0, maxIndex);
+            }
+        };
+    }
+
+    /**
+     * Creates a new connection factory which forwards connection requests to
+     * the provided factory, but whose {@code toString} method will always
+     * return {@code name}.
+     * <p>
+     * This method may be useful for debugging purposes in order to more easily
+     * identity connection factories.
+     *
+     * @param factory
+     *            The connection factory to be named.
+     * @param name
+     *            The name of the connection factory.
+     * @return The named connection factory.
+     * @throws NullPointerException
+     *             If {@code factory} or {@code name} was {@code null}.
+     */
+    public static ConnectionFactory newNamedConnectionFactory(final ConnectionFactory factory,
+            final String name) {
+        Reject.ifNull(factory, name);
+
+        return new ConnectionFactory() {
+
+            @Override
+            public void close() {
+                factory.close();
+            }
+
+            @Override
+            public Connection getConnection() throws LdapException {
+                return factory.getConnection();
+            }
+
+            @Override
+            public Promise<Connection, LdapException> getConnectionAsync() {
+                return factory.getConnectionAsync();
+            }
+
+            @Override
+            public String toString() {
+                return name;
+            }
+
+        };
+    }
+
+    /**
+     * Creates a new server connection factory using the provided
+     * {@link RequestHandler}. The returned factory will manage connection and
+     * request life-cycle, including request cancellation.
+     * <p>
+     * When processing requests, {@link RequestHandler} implementations are
+     * passed a {@link RequestContext} as the first parameter which may be used
+     * for detecting whether or not the request should be aborted due to
+     * cancellation requests or other events, such as connection failure.
+     * <p>
+     * The returned factory maintains state information which includes a table
+     * of active requests. Therefore, {@code RequestHandler} implementations are
+     * required to always return results in order to avoid potential memory
+     * leaks.
+     *
+     * @param <C>
+     *            The type of client context.
+     * @param requestHandler
+     *            The request handler which will be used for all client
+     *            connections.
+     * @return The new server connection factory.
+     * @throws NullPointerException
+     *             If {@code requestHandler} was {@code null}.
+     */
+    public static <C> ServerConnectionFactory<C, Integer> newServerConnectionFactory(
+            final RequestHandler<RequestContext> requestHandler) {
+        Reject.ifNull(requestHandler);
+        return new RequestHandlerFactoryAdapter<>(new RequestHandlerFactory<C, RequestContext>() {
+            @Override
+            public RequestHandler<RequestContext> handleAccept(final C clientContext) {
+                return requestHandler;
+            }
+        });
+    }
+
+    /**
+     * Creates a new server connection factory using the provided
+     * {@link RequestHandlerFactory}. The returned factory will manage
+     * connection and request life-cycle, including request cancellation.
+     * <p>
+     * When processing requests, {@link RequestHandler} implementations are
+     * passed a {@link RequestContext} as the first parameter which may be used
+     * for detecting whether or not the request should be aborted due to
+     * cancellation requests or other events, such as connection failure.
+     * <p>
+     * The returned factory maintains state information which includes a table
+     * of active requests. Therefore, {@code RequestHandler} implementations are
+     * required to always return results in order to avoid potential memory
+     * leaks.
+     *
+     * @param <C>
+     *            The type of client context.
+     * @param factory
+     *            The request handler factory to use for associating request
+     *            handlers with client connections.
+     * @return The new server connection factory.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public static <C> ServerConnectionFactory<C, Integer> newServerConnectionFactory(
+            final RequestHandlerFactory<C, RequestContext> factory) {
+        Reject.ifNull(factory);
+        return new RequestHandlerFactoryAdapter<>(factory);
+    }
+
+    /**
+     * Returns an uncloseable view of the provided connection. Attempts to call
+     * {@link Connection#close()} or
+     * {@link Connection#close(org.forgerock.opendj.ldap.requests.UnbindRequest, String)}
+     * will be ignored.
+     *
+     * @param connection
+     *            The connection whose {@code close} methods are to be disabled.
+     * @return An uncloseable view of the provided connection.
+     */
+    public static Connection uncloseable(Connection connection) {
+        return new AbstractConnectionWrapper<Connection>(connection) {
+            @Override
+            public void close() {
+                // Do nothing.
+            }
+
+            @Override
+            public void close(org.forgerock.opendj.ldap.requests.UnbindRequest request,
+                    String reason) {
+                // Do nothing.
+            }
+        };
+    }
+
+    /**
+     * Returns an uncloseable view of the provided connection factory. Attempts
+     * to call {@link ConnectionFactory#close()} will be ignored.
+     *
+     * @param factory
+     *            The connection factory whose {@code close} method is to be
+     *            disabled.
+     * @return An uncloseable view of the provided connection factory.
+     */
+    public static ConnectionFactory uncloseable(final ConnectionFactory factory) {
+        return new ConnectionFactory() {
+
+            @Override
+            public Promise<Connection, LdapException> getConnectionAsync() {
+                return factory.getConnectionAsync();
+            }
+
+            @Override
+            public Connection getConnection() throws LdapException {
+                return factory.getConnection();
+            }
+
+            @Override
+            public void close() {
+                // Do nothing.
+            }
+        };
+    }
+
+    /**
+     * Returns the host name associated with the provided
+     * {@code InetSocketAddress}, without performing a DNS lookup. This method
+     * attempts to provide functionality which is compatible with
+     * {@code InetSocketAddress.getHostString()} in JDK7. It can be removed once
+     * we drop support for JDK6.
+     *
+     * @param socketAddress
+     *            The socket address which is expected to be an instance of
+     *            {@code InetSocketAddress}.
+     * @return The host name associated with the provided {@code SocketAddress},
+     *         or {@code null} if it is unknown.
+     */
+    public static String getHostString(final InetSocketAddress socketAddress) {
+        /*
+         * See OPENDJ-1270 for more information about slow DNS queries.
+         *
+         * We must avoid calling getHostName() in the case where it is likely to
+         * perform a blocking DNS query. Ideally we would call getHostString(),
+         * but the method was only added in JDK7.
+         */
+        if (socketAddress.isUnresolved()) {
+            /*
+             * Usually socket addresses are resolved on creation. If the address
+             * is unresolved then there must be a user provided hostname instead
+             * and getHostName will not perform a reverse lookup.
+             */
+            return socketAddress.getHostName();
+        } else {
+            /*
+             * Simulate getHostString() by parsing the toString()
+             * representation. This assumes that the toString() representation
+             * is stable, which I assume it is because it is documented.
+             */
+            final InetAddress address = socketAddress.getAddress();
+            final String hostSlashIp = address.toString();
+            final int slashPos = hostSlashIp.indexOf('/');
+            if (slashPos == 0) {
+                return hostSlashIp.substring(1);
+            } else {
+                return hostSlashIp.substring(0, slashPos);
+            }
+        }
+    }
+
+    /** Prevent instantiation. */
+    private Connections() {
+        // Do nothing.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConstraintViolationException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConstraintViolationException.java
new file mode 100644
index 0000000..ab27ecc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ConstraintViolationException.java
@@ -0,0 +1,67 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the update
+ * Request failed because it would have left the Directory in an inconsistent
+ * state. More specifically, this exception is used for the following error
+ * result codes:
+ * <ul>
+ * <li>{@link ResultCode#ATTRIBUTE_OR_VALUE_EXISTS ATTRIBUTE_OR_VALUE_EXISTS} -
+ * the Request failed because it would have resulted in a conflict with an
+ * existing attribute or attribute value in the target entry.
+ * <li>{@link ResultCode#NO_SUCH_ATTRIBUTE NO_SUCH_ATTRIBUTE} - the Request
+ * failed because it targeted an attribute or attribute value that did not exist
+ * in the specified entry.
+ * <li>{@link ResultCode#CONSTRAINT_VIOLATION CONSTRAINT_VIOLATION} - the
+ * Request failed because it would have violated some constraint defined in the
+ * server.
+ * <li>{@link ResultCode#ENTRY_ALREADY_EXISTS ENTRY_ALREADY_EXISTS} - the
+ * Request failed because it would have resulted in an entry that conflicts with
+ * an entry that already exists.
+ * <li>{@link ResultCode#INVALID_ATTRIBUTE_SYNTAX INVALID_ATTRIBUTE_SYNTAX} -
+ * the Request failed because it violated the syntax for a specified attribute.
+ * <li>{@link ResultCode#INVALID_DN_SYNTAX INVALID_DN_SYNTAX} - the Request
+ * failed because it would have resulted in an entry with an invalid or
+ * malformed DN.
+ * <li>{@link ResultCode#NAMING_VIOLATION NAMING_VIOLATION} - the Request failed
+ * becauseit would have violated the server's naming configuration.
+ * <li>{@link ResultCode#NOT_ALLOWED_ON_NONLEAF NOT_ALLOWED_ON_NONLEAF} - the
+ * Request failed because it is not allowed for non-leaf entries.
+ * <li>{@link ResultCode#NOT_ALLOWED_ON_RDN NOT_ALLOWED_ON_RDN} - the Request
+ * failed because it is not allowed on an RDN attribute.
+ * <li>{@link ResultCode#OBJECTCLASS_MODS_PROHIBITED
+ * OBJECTCLASS_MODS_PROHIBITED} - the Request failed because it would have
+ * modified the objectclasses associated with an entry in an illegal manner.
+ * <li>{@link ResultCode#OBJECTCLASS_VIOLATION OBJECTCLASS_VIOLATION} - the
+ * Request failed because it would have resulted in an entry that violated the
+ * server schema.
+ * <li>{@link ResultCode#UNDEFINED_ATTRIBUTE_TYPE UNDEFINED_ATTRIBUTE_TYPE} -
+ * the Request failed because it referenced an attribute that is not defined in
+ * the server schema.
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class ConstraintViolationException extends LdapException {
+    ConstraintViolationException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
new file mode 100644
index 0000000..3a1a44b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -0,0 +1,1046 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+import java.util.WeakHashMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.CoreSchema;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * A distinguished name (DN) as defined in RFC 4512 section 2.3 is the
+ * concatenation of its relative distinguished name (RDN) and its immediate
+ * superior's DN. A DN unambiguously refers to an entry in the Directory.
+ * <p>
+ * The following are examples of string representations of DNs:
+ *
+ * <pre>
+ * UID=nobody@example.com,DC=example,DC=com CN=John
+ * Smith,OU=Sales,O=ACME Limited,L=Moab,ST=Utah,C=US
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
+ *      Lightweight Directory Access Protocol (LDAP): Directory Information
+ *      Models </a>
+ */
+public final class DN implements Iterable<RDN>, Comparable<DN> {
+    static final byte NORMALIZED_RDN_SEPARATOR = 0x00;
+    static final byte NORMALIZED_AVA_SEPARATOR = 0x01;
+    static final byte NORMALIZED_ESC_BYTE = 0x02;
+
+    static final char RDN_CHAR_SEPARATOR = ',';
+    static final char AVA_CHAR_SEPARATOR = '+';
+
+    private static final DN ROOT_DN = new DN(CoreSchema.getInstance(), null, null);
+
+    /**
+     * This is the size of the per-thread per-schema DN cache. We should
+     * be conservative here in case there are many threads. We will only
+     * cache parent DNs, so there's no need for it to be big.
+     */
+    private static final int DN_CACHE_SIZE = 32;
+
+    private static final ThreadLocal<WeakHashMap<Schema, Map<String, DN>>> CACHE =
+            new ThreadLocal<WeakHashMap<Schema, Map<String, DN>>>() {
+                @Override
+                protected WeakHashMap<Schema, Map<String, DN>> initialValue() {
+                    return new WeakHashMap<>();
+                }
+            };
+
+    /**
+     * Returns the LDAP string representation of the provided DN attribute value
+     * in a form suitable for substitution directly into a DN string. This
+     * method may be useful in cases where a DN is to be constructed from a DN
+     * template using {@code String#format(String, Object...)}. The following
+     * example illustrates two approaches to constructing a DN:
+     *
+     * <pre>
+     * // This may contain user input.
+     * String attributeValue = ...;
+     *
+     * // Using the equality filter constructor:
+     * DN dn = DN.valueOf("ou=people,dc=example,dc=com").child("uid", attributeValue);
+     *
+     * // Using a String template:
+     * String dnTemplate = "uid=%s,ou=people,dc=example,dc=com";
+     * String dnString = String.format(dnTemplate,
+     *                                 DN.escapeAttributeValue(attributeValue));
+     * DN dn = DN.valueOf(dnString);
+     * </pre>
+     *
+     * <b>Note:</b> attribute values do not and should not be escaped before
+     * passing them to constructors like {@link #child(String, Object)}.
+     * Escaping is only required when creating DN strings.
+     *
+     * @param attributeValue
+     *            The attribute value.
+     * @return The LDAP string representation of the provided filter assertion
+     *         value in a form suitable for substitution directly into a filter
+     *         string.
+     */
+    public static String escapeAttributeValue(final Object attributeValue) {
+        Reject.ifNull(attributeValue);
+        final String s = String.valueOf(attributeValue);
+        final StringBuilder builder = new StringBuilder(s.length());
+        AVA.escapeAttributeValue(s, builder);
+        return builder.toString();
+    }
+
+    /**
+     * Creates a new DN using the provided DN template and unescaped attribute
+     * values using the default schema. This method first escapes each of the
+     * attribute values and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP DN using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a DN is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
+     * DN dn = DN.format(template, &quot;bjensen&quot;);
+     * </pre>
+     *
+     * @param template
+     *            The DN template.
+     * @param attributeValues
+     *            The attribute values to be substituted into the template.
+     * @return The formatted template parsed as a {@code DN}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a DN.
+     * @see #escapeAttributeValue(Object)
+     */
+    public static DN format(final String template, final Object... attributeValues) {
+        return format(template, Schema.getDefaultSchema(), attributeValues);
+    }
+
+    /**
+     * Creates a new DN using the provided DN template and unescaped attribute
+     * values using the provided schema. This method first escapes each of the
+     * attribute values and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP DN using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a DN is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
+     * DN dn = DN.format(template, &quot;bjensen&quot;);
+     * </pre>
+     *
+     * @param template
+     *            The DN template.
+     * @param schema
+     *            The schema to use when parsing the DN.
+     * @param attributeValues
+     *            The attribute values to be substituted into the template.
+     * @return The formatted template parsed as a {@code DN}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a DN.
+     * @see #escapeAttributeValue(Object)
+     */
+    public static DN format(final String template, final Schema schema,
+            final Object... attributeValues) {
+        final String[] attributeValueStrings = new String[attributeValues.length];
+        for (int i = 0; i < attributeValues.length; i++) {
+            attributeValueStrings[i] = escapeAttributeValue(attributeValues[i]);
+        }
+        final String dnString = String.format(template, (Object[]) attributeValueStrings);
+        return valueOf(dnString, schema);
+    }
+
+    /**
+     * Returns the Root DN. The Root DN does not contain and RDN components and
+     * is superior to all other DNs.
+     *
+     * @return The Root DN.
+     */
+    public static DN rootDN() {
+        return ROOT_DN;
+    }
+
+    /**
+     * Parses the provided LDAP string representation of a DN using the default schema.
+     *
+     * @param dn
+     *            The LDAP string representation of a DN.
+     * @return The parsed DN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     * @see #format(String, Object...)
+     */
+    public static DN valueOf(final String dn) {
+        return valueOf(dn, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Parses the provided LDAP string representation of a DN using the provided schema.
+     *
+     * @param dn
+     *            The LDAP string representation of a DN.
+     * @param schema
+     *            The schema to use when parsing the DN.
+     * @return The parsed DN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a DN.
+     * @throws NullPointerException
+     *             If {@code dn} or {@code schema} was {@code null}.
+     * @see #format(String, Schema, Object...)
+     */
+    public static DN valueOf(final String dn, final Schema schema) {
+        Reject.ifNull(dn, schema);
+        if (dn.length() == 0) {
+            return ROOT_DN;
+        }
+
+        // First check if DN is already cached.
+        final Map<String, DN> cache = getCache(schema);
+        final DN cachedDN = cache.get(dn);
+        if (cachedDN != null) {
+            return cachedDN;
+        }
+
+        // Not in cache so decode.
+        return decode(new SubstringReader(dn), schema, cache);
+    }
+
+    /**
+     * Parses the provided LDAP string representation of a DN using the default schema.
+     *
+     * @param dn
+     *            The LDAP byte string representation of a DN.
+     * @return The parsed DN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP byte string representation of a DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public static DN valueOf(ByteString dn) {
+        return DN.valueOf(dn.toString());
+    }
+
+    /** Decodes a DN using the provided reader and schema. */
+    private static DN decode(final SubstringReader reader, final Schema schema, final Map<String, DN> cache) {
+        reader.skipWhitespaces();
+        if (reader.remaining() == 0) {
+            return ROOT_DN;
+        }
+
+        final RDN rdn;
+        try {
+            rdn = RDN.decode(reader, schema);
+        } catch (final UnknownSchemaElementException e) {
+            throw new LocalizedIllegalArgumentException(
+                    ERR_DN_TYPE_NOT_FOUND.get(reader.getString(), e.getMessageObject()));
+        }
+
+        if (reader.remaining() > 0 && reader.read() == ',') {
+            reader.skipWhitespaces();
+            if (reader.remaining() == 0) {
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString()));
+            }
+            reader.mark();
+            final String parentString = reader.read(reader.remaining());
+            DN parent = cache.get(parentString);
+            if (parent == null) {
+                reader.reset();
+                parent = decode(reader, schema, cache);
+
+                // Only cache parent DNs since leaf DNs are likely to make the cache to volatile.
+                cache.put(parentString, parent);
+            }
+            return new DN(schema, parent, rdn);
+        } else {
+            return new DN(schema, ROOT_DN, rdn);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    private static Map<String, DN> getCache(final Schema schema) {
+        final WeakHashMap<Schema, Map<String, DN>> threadLocalMap = CACHE.get();
+        Map<String, DN> schemaLocalMap = threadLocalMap.get(schema);
+
+        if (schemaLocalMap == null) {
+            schemaLocalMap = new LinkedHashMap<String, DN>(DN_CACHE_SIZE, 0.75f, true) {
+                @Override
+                protected boolean removeEldestEntry(final Map.Entry<String, DN> e) {
+                    return size() > DN_CACHE_SIZE;
+                }
+            };
+            threadLocalMap.put(schema, schemaLocalMap);
+        }
+        return schemaLocalMap;
+    }
+
+    private final RDN rdn;
+    private DN parent;
+    private final int size;
+
+    /**
+     * The normalized byte string representation of this DN, which is not
+     * a valid DN and is not reversible to a valid DN.
+     */
+    private ByteString normalizedDN;
+
+    /**
+     * The RFC 4514 string representation of this DN. A value of {@code null}
+     * indicates that the value needs to be computed lazily.
+     */
+    private String stringValue;
+
+    /** The schema used to create this DN. */
+    private final Schema schema;
+
+    /** Private constructor. */
+    private DN(final Schema schema, final DN parent, final RDN rdn) {
+        this(schema, parent, rdn, parent != null ? parent.size + 1 : 0);
+    }
+
+    /** Private constructor. */
+    private DN(final Schema schema, final DN parent, final RDN rdn, final int size) {
+        this.schema = schema;
+        this.parent = parent;
+        this.rdn = rdn;
+        this.size = size;
+        this.stringValue = rdn == null ? "" : null;
+    }
+
+    /**
+     * Returns a DN which is subordinate to this DN and having the additional
+     * RDN components contained in the provided DN.
+     *
+     * @param dn
+     *            The DN containing the RDN components to be added to this DN.
+     * @return The subordinate DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public DN child(final DN dn) {
+        Reject.ifNull(dn);
+
+        if (dn.isRootDN()) {
+            return this;
+        } else if (isRootDN()) {
+            return dn;
+        } else {
+            final RDN[] rdns = new RDN[dn.size()];
+            int i = rdns.length;
+            for (DN next = dn; next.rdn != null; next = next.parent) {
+                rdns[--i] = next.rdn;
+            }
+            DN newDN = this;
+            for (i = 0; i < rdns.length; i++) {
+                newDN = new DN(this.schema, newDN, rdns[i]);
+            }
+            return newDN;
+        }
+    }
+
+    /**
+     * Returns a DN which is an immediate child of this DN and having the
+     * specified RDN.
+     * <p>
+     * <b>Note:</b> the child DN whose RDN is {@link RDN#maxValue()} compares
+     * greater than all other possible child DNs, and may be used to construct
+     * range queries against DN keyed sorted collections such as
+     * {@code SortedSet} and {@code SortedMap}.
+     *
+     * @param rdn
+     *            The RDN for the child DN.
+     * @return The child DN.
+     * @throws NullPointerException
+     *             If {@code rdn} was {@code null}.
+     * @see RDN#maxValue()
+     */
+    public DN child(final RDN rdn) {
+        Reject.ifNull(rdn);
+        return new DN(this.schema, this, rdn);
+    }
+
+    /**
+     * Returns a DN which is subordinate to this DN and having the additional
+     * RDN components contained in the provided DN decoded using the default
+     * schema.
+     *
+     * @param dn
+     *            The DN containing the RDN components to be added to this DN.
+     * @return The subordinate DN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public DN child(final String dn) {
+        Reject.ifNull(dn);
+        return child(valueOf(dn));
+    }
+
+    /**
+     * Returns a DN which is an immediate child of this DN and with an RDN
+     * having the provided attribute type and value decoded using the default
+     * schema.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeValue
+     *            The attribute value.
+     * @return The child DN.
+     * @throws UnknownSchemaElementException
+     *             If {@code attributeType} was not found in the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code attributeValue} was
+     *             {@code null}.
+     */
+    public DN child(final String attributeType, final Object attributeValue) {
+        return child(new RDN(attributeType, attributeValue));
+    }
+
+    @Override
+    public int compareTo(final DN dn) {
+        return toNormalizedByteString().compareTo(dn.toNormalizedByteString());
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DN) {
+            DN otherDN = (DN) obj;
+            return toNormalizedByteString().equals(otherDN.toNormalizedByteString());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return toNormalizedByteString().hashCode();
+    }
+
+    /**
+     * Returns {@code true} if this DN is an immediate child of the provided DN.
+     *
+     * @param dn
+     *            The potential parent DN.
+     * @return {@code true} if this DN is the immediate child of the provided
+     *         DN, otherwise {@code false}.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isChildOf(final DN dn) {
+        // If this is the Root DN then parent will be null but this is ok.
+        return dn.equals(parent);
+    }
+
+    /**
+     * Returns {@code true} if this DN is an immediate child of the provided DN
+     * decoded using the default schema.
+     *
+     * @param dn
+     *            The potential parent DN.
+     * @return {@code true} if this DN is the immediate child of the provided
+     *         DN, otherwise {@code false}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isChildOf(final String dn) {
+        // If this is the Root DN then parent will be null but this is ok.
+        return isChildOf(valueOf(dn));
+    }
+
+    /**
+     * Returns {@code true} if this DN matches the provided base DN and search
+     * scope.
+     *
+     * @param dn
+     *            The base DN.
+     * @param scope
+     *            The search scope.
+     * @return {@code true} if this DN matches the provided base DN and search
+     *         scope, otherwise {@code false}.
+     * @throws NullPointerException
+     *             If {@code dn} or {@code scope} was {@code null}.
+     */
+    public boolean isInScopeOf(DN dn, SearchScope scope) {
+        switch (scope.asEnum()) {
+        case BASE_OBJECT:
+            // The base DN must equal this DN.
+            return equals(dn);
+        case SINGLE_LEVEL:
+            // The parent DN must equal the base DN.
+            return isChildOf(dn);
+        case SUBORDINATES:
+            // This DN must be a descendant of the provided base DN, but
+            // not equal to it.
+            return isSubordinateOrEqualTo(dn) && !equals(dn);
+        case WHOLE_SUBTREE:
+            // This DN must be a descendant of the provided base DN.
+            return isSubordinateOrEqualTo(dn);
+        default:
+            // This is a scope that we don't recognize.
+            return false;
+        }
+    }
+
+    /**
+     * Returns {@code true} if this DN matches the provided base DN and search
+     * scope.
+     *
+     * @param dn
+     *            The base DN.
+     * @param scope
+     *            The search scope.
+     * @return {@code true} if this DN matches the provided base DN and search
+     *         scope, otherwise {@code false}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} or {@code scope} was {@code null}.
+     */
+    public boolean isInScopeOf(String dn, SearchScope scope) {
+        return isInScopeOf(valueOf(dn), scope);
+    }
+
+    /**
+     * Returns {@code true} if this DN is the immediate parent of the provided
+     * DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is the immediate parent of the provided
+     *         DN, otherwise {@code false}.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isParentOf(final DN dn) {
+        // If dn is the Root DN then parent will be null but this is ok.
+        return equals(dn.parent);
+    }
+
+    /**
+     * Returns {@code true} if this DN is the immediate parent of the provided
+     * DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is the immediate parent of the provided
+     *         DN, otherwise {@code false}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isParentOf(final String dn) {
+        // If dn is the Root DN then parent will be null but this is ok.
+        return isParentOf(valueOf(dn));
+    }
+
+    /**
+     * Returns {@code true} if this DN is the Root DN.
+     *
+     * @return {@code true} if this DN is the Root DN, otherwise {@code false}.
+     */
+    public boolean isRootDN() {
+        return size == 0;
+    }
+
+    /**
+     * Returns {@code true} if this DN is subordinate to or equal to the
+     * provided DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is subordinate to or equal to the
+     *         provided DN, otherwise {@code false}.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isSubordinateOrEqualTo(final DN dn) {
+        if (size < dn.size) {
+            return false;
+        } else if (size == dn.size) {
+            return equals(dn);
+        } else {
+            // dn is a potential superior of this.
+            return parent(size - dn.size).equals(dn);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this DN is subordinate to or equal to the
+     * provided DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is subordinate to or equal to the
+     *         provided DN, otherwise {@code false}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isSubordinateOrEqualTo(final String dn) {
+        return isSubordinateOrEqualTo(valueOf(dn));
+    }
+
+    /**
+     * Returns {@code true} if this DN is superior to or equal to the provided
+     * DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is superior to or equal to the provided
+     *         DN, otherwise {@code false}.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isSuperiorOrEqualTo(final DN dn) {
+        if (size > dn.size) {
+            return false;
+        } else if (size == dn.size) {
+            return equals(dn);
+        } else {
+            // dn is a potential subordinate of this.
+            return dn.parent(dn.size - size).equals(this);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this DN is superior to or equal to the provided
+     * DN.
+     *
+     * @param dn
+     *            The potential child DN.
+     * @return {@code true} if this DN is superior to or equal to the provided
+     *         DN, otherwise {@code false}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} is not a valid LDAP string representation of a
+     *             DN.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    public boolean isSuperiorOrEqualTo(final String dn) {
+        return isSuperiorOrEqualTo(valueOf(dn));
+    }
+
+    /**
+     * Returns an iterator of the RDNs contained in this DN. The RDNs will be
+     * returned in the order starting with this DN's RDN, followed by the RDN of
+     * the parent DN, and so on.
+     * <p>
+     * Attempts to remove RDNs using an iterator's {@code remove()} method are
+     * not permitted and will result in an {@code UnsupportedOperationException}
+     * being thrown.
+     *
+     * @return An iterator of the RDNs contained in this DN.
+     */
+    @Override
+    public Iterator<RDN> iterator() {
+        return new Iterator<RDN>() {
+            private DN dn = DN.this;
+
+            @Override
+            public boolean hasNext() {
+                return dn.rdn != null;
+            }
+
+            @Override
+            public RDN next() {
+                if (dn.rdn == null) {
+                    throw new NoSuchElementException();
+                }
+
+                final RDN rdn = dn.rdn;
+                dn = dn.parent;
+                return rdn;
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    /**
+     * Returns the DN whose content is the specified number of RDNs from this
+     * DN. The following equivalences hold:
+     *
+     * <pre>
+     * dn.localName(0).isRootDN();
+     * dn.localName(1).equals(rootDN.child(dn.rdn()));
+     * dn.localName(dn.size()).equals(dn);
+     * </pre>
+     *
+     * Said otherwise, a new DN is built using {@code index} RDNs,
+     * retained in the same order, starting from the left.
+     *
+     * @param index
+     *            The number of RDNs to be included in the local name.
+     * @return The DN whose content is the specified number of RDNs from this
+     *         DN.
+     * @throws IllegalArgumentException
+     *             If {@code index} is less than zero.
+     * @see #parent(int) for the reverse operation (starting from the right)
+     */
+    public DN localName(final int index) {
+        Reject.ifFalse(index >= 0, "index less than zero");
+
+        if (index == 0) {
+            return ROOT_DN;
+        } else if (index >= size) {
+            return this;
+        } else {
+            final DN localName = new DN(this.schema, null, rdn, index);
+            DN nextLocalName = localName;
+            DN lastDN = parent;
+            for (int i = index - 1; i > 0; i--) {
+                nextLocalName.parent = new DN(this.schema, null, lastDN.rdn, i);
+                nextLocalName = nextLocalName.parent;
+                lastDN = lastDN.parent;
+            }
+            nextLocalName.parent = ROOT_DN;
+            return localName;
+        }
+    }
+
+    /**
+     * Returns the DN which is the immediate parent of this DN, or {@code null}
+     * if this DN is the Root DN.
+     * <p>
+     * This method is equivalent to:
+     *
+     * <pre>
+     * parent(1);
+     * </pre>
+     *
+     * @return The DN which is the immediate parent of this DN, or {@code null}
+     *         if this DN is the Root DN.
+     */
+    public DN parent() {
+        return parent;
+    }
+
+    /**
+     * Returns the DN which is equal to this DN with the specified number of
+     * RDNs removed. Note that if {@code index} is zero then this DN will be
+     * returned (identity).
+     *
+     * Said otherwise, a new DN is built using {@code index} RDNs,
+     * retained in the same order, starting from the right.
+     *
+     * @param index
+     *            The number of RDNs to be removed.
+     * @return The DN which is equal to this DN with the specified number of
+     *         RDNs removed, or {@code null} if the parent of the Root DN is
+     *         reached.
+     * @throws IllegalArgumentException
+     *             If {@code index} is less than zero.
+     * @see #localName(int) for the reverse operation (starting from the left)
+     */
+    public DN parent(final int index) {
+        // We allow size + 1 so that we can return null as the parent of the
+        // Root DN.
+        Reject.ifFalse(index >= 0, "index less than zero");
+
+        DN parentDN = this;
+        for (int i = 0; parentDN != null && i < index; i++) {
+            parentDN = parentDN.parent;
+        }
+        return parentDN;
+    }
+
+    /**
+     * Returns the RDN of this DN, or {@code null} if this DN is the Root DN.
+     *
+     * @return The RDN of this DN, or {@code null} if this DN is the Root DN.
+     */
+    public RDN rdn() {
+        return rdn;
+    }
+
+    /**
+     * Returns the RDN at the specified index for this DN,
+     * or {@code null} if no such RDN exists.
+     * <p>
+     * Here is an example usage:
+     * <pre>
+     * DN.valueOf("ou=people,dc=example,dc=com").rdn(0) => "ou=people"
+     * DN.valueOf("ou=people,dc=example,dc=com").rdn(1) => "dc=example"
+     * DN.valueOf("ou=people,dc=example,dc=com").rdn(2) => "dc=com"
+     * DN.valueOf("ou=people,dc=example,dc=com").rdn(3) => null
+     * </pre>
+     *
+     * @param index
+     *            The index of the requested RDN.
+     * @return The RDN at the specified index, or {@code null} if no such RDN exists.
+     * @throws IllegalArgumentException
+     *             If {@code index} is less than zero.
+     */
+    public RDN rdn(int index) {
+        DN parentDN = parent(index);
+        return parentDN != null ? parentDN.rdn : null;
+    }
+
+    /**
+     * Returns a copy of this DN whose parent DN, {@code fromDN}, has been
+     * renamed to the new parent DN, {@code toDN}. If this DN is not subordinate
+     * or equal to {@code fromDN} then this DN is returned (i.e. the DN is not
+     * renamed).
+     *
+     * @param fromDN
+     *            The old parent DN.
+     * @param toDN
+     *            The new parent DN.
+     * @return The renamed DN, or this DN if no renaming was performed.
+     * @throws NullPointerException
+     *             If {@code fromDN} or {@code toDN} was {@code null}.
+     */
+    public DN rename(final DN fromDN, final DN toDN) {
+        Reject.ifNull(fromDN, toDN);
+
+        if (!isSubordinateOrEqualTo(fromDN)) {
+            return this;
+        } else if (equals(fromDN)) {
+            return toDN;
+        } else {
+            return toDN.child(localName(size - fromDN.size));
+        }
+    }
+
+    /**
+     * Returns the number of RDN components in this DN.
+     *
+     * @return The number of RDN components in this DN.
+     */
+    public int size() {
+        return size;
+    }
+
+    /**
+     * Returns the RFC 4514 string representation of this DN.
+     *
+     * @return The RFC 4514 string representation of this DN.
+     * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight
+     *      Directory Access Protocol (LDAP): String Representation of
+     *      Distinguished Names </a>
+     */
+    @Override
+    public String toString() {
+        // We don't care about potential race conditions here.
+        if (stringValue == null) {
+            final StringBuilder builder = rdn.toString(new StringBuilder());
+            if (!parent.isRootDN()) {
+                builder.append(RDN_CHAR_SEPARATOR);
+                builder.append(parent);
+            }
+            stringValue = builder.toString();
+        }
+        return stringValue;
+    }
+
+    /**
+     * Retrieves a normalized byte string representation of this DN.
+     * <p>
+     * This representation is suitable for equality and comparisons, and
+     * for providing a natural hierarchical ordering.
+     * However, it is not a valid DN and can't be reverted to a valid DN.
+     * You should consider using a {@code CompactDn} as an alternative.
+     *
+     * @return The normalized string representation of this DN.
+     */
+    public ByteString toNormalizedByteString() {
+        if (normalizedDN == null) {
+            if (rdn == null) {
+                normalizedDN = ByteString.empty();
+            } else {
+                final ByteString normalizedParent = parent.toNormalizedByteString();
+                final ByteStringBuilder builder = new ByteStringBuilder(normalizedParent.length() + 16);
+                builder.appendBytes(normalizedParent);
+                rdn.toNormalizedByteString(builder);
+                normalizedDN = builder.toByteString();
+            }
+        }
+        return normalizedDN;
+    }
+
+    /**
+     * Retrieves a normalized string representation of this DN.
+     * <p>
+     * This representation is safe to use in an URL or in a file name.
+     * However, it is not a valid DN and can't be reverted to a valid DN.
+     *
+     * @return The normalized string representation of this DN.
+     */
+    public String toNormalizedUrlSafeString() {
+        if (rdn() == null) {
+            return "";
+        }
+
+        // This code differs from toNormalizedByteString(),
+        // because we do not care about ordering comparisons.
+        // (so we do not append the RDN_SEPARATOR first)
+        final StringBuilder builder = new StringBuilder();
+        int i = size() - 1;
+        parent(i).rdn().toNormalizedUrlSafeString(builder);
+        for (i--; i >= 0; i--) {
+            final RDN rdn = parent(i).rdn();
+            // Only add a separator if the RDN is not RDN.maxValue() or RDN.minValue().
+            if (rdn.size() != 0) {
+                builder.append(RDN_CHAR_SEPARATOR);
+            }
+            rdn.toNormalizedUrlSafeString(builder);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Returns a UUID whose content is based on the normalized content of this DN.
+     * Two equivalent DNs subject to the same schema will always yield the same UUID.
+     *
+     * @return the UUID representing this DN
+     */
+    public UUID toUUID() {
+        ByteString normDN = toNormalizedByteString();
+        if (!normDN.isEmpty()) {
+            // remove leading RDN separator
+            normDN = normDN.subSequence(1, normDN.length());
+        }
+        return UUID.nameUUIDFromBytes(normDN.toByteArray());
+    }
+
+    /**
+     * A compact representation of a DN, suitable for equality and comparisons, and providing a natural hierarchical
+     * ordering.
+     * <p>
+     * This representation should be used when it is important to reduce memory usage. The memory consumption compared
+     * to a regular DN object is minimal. Prototypical usage is for static groups implementation where large groups of
+     * DNs must be recorded and must be converted back to DNs.
+     * <p>
+     * This representation can be created either eagerly or lazily.
+     * <ul>
+     *   <li>eagerly: the normalized value is computed immediately at creation time.</li>
+     *   <li>lazily: the normalized value is computed only the first time it is needed.</li>
+     * </ul>
+     *
+     * @deprecated This class will eventually be replaced by a compact implementation of a DN.
+     */
+    @Deprecated
+    public static final class CompactDn implements Comparable<CompactDn> {
+        /** Original string corresponding to the DN. */
+        private final byte[] originalValue;
+
+        /**
+         * Normalized byte string, suitable for equality and comparisons, and providing a natural hierarchical ordering,
+         * but not usable as a valid DN.
+         */
+        private volatile byte[] normalizedValue;
+
+        private final Schema schema;
+
+        private CompactDn(final DN dn) {
+            this.originalValue = getBytes(dn.toString());
+            this.schema = dn.schema;
+        }
+
+        @Override
+        public int compareTo(final CompactDn other) {
+            byte[] normValue = getNormalizedValue();
+            byte[] otherNormValue = other.getNormalizedValue();
+            return ByteString.compareTo(normValue, 0, normValue.length, otherNormValue, 0, otherNormValue.length);
+        }
+
+        /**
+         * Returns the DN corresponding to this compact representation.
+         *
+         *  @return the DN
+         */
+        public DN toDn() {
+            return DN.valueOf(ByteString.toString(originalValue, 0, originalValue.length), schema);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(getNormalizedValue());
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            } else if (obj instanceof CompactDn) {
+                final CompactDn other = (CompactDn) obj;
+                return Arrays.equals(getNormalizedValue(), other.getNormalizedValue());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return ByteString.toString(originalValue, 0, originalValue.length);
+        }
+
+        private byte[] getNormalizedValue() {
+            if (normalizedValue == null) {
+                normalizedValue = toDn().toNormalizedByteString().toByteArray();
+            }
+            return normalizedValue;
+        }
+    }
+
+    /**
+     * Returns a compact representation of this DN, with lazy evaluation of the normalized value.
+     *
+     * @return the DN compact representation
+     */
+    public CompactDn compact() {
+        return new CompactDn(this);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeException.java
new file mode 100644
index 0000000..2aadaae
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeException.java
@@ -0,0 +1,110 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * Thrown when data from an input source cannot be decoded, perhaps due to the
+ * data being malformed in some way. By default decoding exceptions are fatal,
+ * indicating that the associated input source is no longer usable.
+ */
+@SuppressWarnings("serial")
+public final class DecodeException extends IOException implements LocalizableException {
+    /**
+     * Creates a new non-fatal decode exception with the provided message.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @return The new non-fatal decode exception.
+     */
+    public static DecodeException error(final LocalizableMessage message) {
+        return new DecodeException(message, false, null);
+    }
+
+    /**
+     * Creates a new non-fatal decode exception with the provided message and
+     * root cause.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @param cause
+     *            The underlying cause of this exception.
+     * @return The new non-fatal decode exception.
+     */
+    public static DecodeException error(final LocalizableMessage message, final Throwable cause) {
+        return new DecodeException(message, false, cause);
+    }
+
+    /**
+     * Creates a new fatal decode exception with the provided message. The
+     * associated input source can no longer be used.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @return The new fatal decode exception.
+     */
+    public static DecodeException fatalError(final LocalizableMessage message) {
+        return new DecodeException(message, true, null);
+    }
+
+    /**
+     * Creates a new fatal decode exception with the provided message and root
+     * cause. The associated input source can no longer be used.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @param cause
+     *            The underlying cause of this exception.
+     * @return The new fatal decode exception.
+     */
+    public static DecodeException fatalError(final LocalizableMessage message, final Throwable cause) {
+        return new DecodeException(message, true, cause);
+    }
+
+    private final LocalizableMessage message;
+
+    private final boolean isFatal;
+
+    /** Construction is provided via factory methods. */
+    private DecodeException(final LocalizableMessage message, final boolean isFatal,
+            final Throwable cause) {
+        super(message.toString(), cause);
+        this.message = message;
+        this.isFatal = isFatal;
+    }
+
+    @Override
+    public LocalizableMessage getMessageObject() {
+        return message;
+    }
+
+    /**
+     * Indicates whether or not the error was fatal and the associated input
+     * source can no longer be used.
+     *
+     * @return {@code true} if the error was fatal and the associated input
+     *         source can no longer be used, otherwise {@code false} .
+     */
+    public boolean isFatal() {
+        return isFatal;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeOptions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeOptions.java
new file mode 100644
index 0000000..4e634ea
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DecodeOptions.java
@@ -0,0 +1,181 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Decode options allow applications to control how requests and responses are
+ * decoded. In particular:
+ * <ul>
+ * <li>The strategy for selecting which {@code Schema} should be used for
+ * decoding distinguished names, attribute descriptions, and other objects which
+ * require a schema in order to be decoded.
+ * <li>The {@code Attribute} implementation which should be used when decoding
+ * attributes.
+ * <li>The {@code Entry} implementation which should be used when decoding
+ * entries or entry like objects.
+ * </ul>
+ */
+public final class DecodeOptions {
+    private static final class FixedSchemaResolver implements SchemaResolver {
+        private final Schema schema;
+
+        private FixedSchemaResolver(final Schema schema) {
+            this.schema = schema;
+        }
+
+        @Override
+        public Schema resolveSchema(final String dn) {
+            return schema;
+        }
+    }
+
+    private SchemaResolver schemaResolver;
+    private EntryFactory entryFactory;
+    private AttributeFactory attributeFactory;
+
+    /**
+     * Creates a new set of decode options which will always use the default
+     * schema returned by {@link Schema#getDefaultSchema()},
+     * {@link LinkedAttribute}, and {@link LinkedHashMapEntry}.
+     */
+    public DecodeOptions() {
+        this.attributeFactory = LinkedAttribute.FACTORY;
+        this.entryFactory = LinkedHashMapEntry.FACTORY;
+        this.schemaResolver = SchemaResolver.DEFAULT;
+    }
+
+    /**
+     * Creates a new set of decode options having the same initial set of
+     * options as the provided set of decode options.
+     *
+     * @param options
+     *            The set of decode options to be copied.
+     */
+    public DecodeOptions(final DecodeOptions options) {
+        this.attributeFactory = options.attributeFactory;
+        this.entryFactory = options.entryFactory;
+        this.schemaResolver = options.schemaResolver;
+    }
+
+    /**
+     * Returns the {@code AttributeFactory} which will be used for creating new
+     * {@code Attribute} instances when decoding attributes.
+     *
+     * @return The {@code AttributeFactory} which will be used for creating new
+     *         {@code Attribute} instances when decoding attributes.
+     */
+    public final AttributeFactory getAttributeFactory() {
+        return attributeFactory;
+    }
+
+    /**
+     * Returns the {@code EntryFactory} which will be used for creating new
+     * {@code Entry} instances when decoding entries.
+     *
+     * @return The {@code EntryFactory} which will be used for creating new
+     *         {@code Entry} instances when decoding entries.
+     */
+    public final EntryFactory getEntryFactory() {
+        return entryFactory;
+    }
+
+    /**
+     * Returns the strategy for selecting which {@code Schema} should be used
+     * for decoding distinguished names, attribute descriptions, and other
+     * objects which require a {@code Schema} in order to be decoded.
+     *
+     * @return The schema resolver which will be used for decoding.
+     */
+    public final SchemaResolver getSchemaResolver() {
+        return schemaResolver;
+    }
+
+    /**
+     * Sets the {@code AttributeFactory} which will be used for creating new
+     * {@code Attribute} instances when decoding attributes.
+     *
+     * @param factory
+     *            The {@code AttributeFactory} which will be used for creating
+     *            new {@code Attribute} instances when decoding attributes.
+     * @return A reference to this set of decode options.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public final DecodeOptions setAttributeFactory(final AttributeFactory factory) {
+        Reject.ifNull(factory);
+        this.attributeFactory = factory;
+        return this;
+    }
+
+    /**
+     * Sets the {@code EntryFactory} which will be used for creating new
+     * {@code Entry} instances when decoding entries.
+     *
+     * @param factory
+     *            The {@code EntryFactory} which will be used for creating new
+     *            {@code Entry} instances when decoding entries.
+     * @return A reference to this set of decode options.
+     * @throws NullPointerException
+     *             If {@code factory} was {@code null}.
+     */
+    public final DecodeOptions setEntryFactory(final EntryFactory factory) {
+        Reject.ifNull(factory);
+        this.entryFactory = factory;
+        return this;
+    }
+
+    /**
+     * Sets the {@code Schema} which will be used for decoding distinguished
+     * names, attribute descriptions, and other objects which require a schema
+     * in order to be decoded. This setting overrides the currently active
+     * schema resolver set using {@link #setSchemaResolver}.
+     *
+     * @param schema
+     *            The {@code Schema} which will be used for decoding.
+     * @return A reference to this set of decode options.
+     * @throws NullPointerException
+     *             If {@code schema} was {@code null}.
+     */
+    public final DecodeOptions setSchema(final Schema schema) {
+        Reject.ifNull(schema);
+        this.schemaResolver = new FixedSchemaResolver(schema);
+        return this;
+    }
+
+    /**
+     * Sets the strategy for selecting which {@code Schema} should be used for
+     * decoding distinguished names, attribute descriptions, and other objects
+     * which require a {@code Schema} in order to be decoded. This setting
+     * overrides the currently active schema set using {@link #setSchema}.
+     *
+     * @param resolver
+     *            The {@code SchemaResolver} which will be used for decoding.
+     * @return A reference to this set of decode options.
+     * @throws NullPointerException
+     *             If {@code resolver} was {@code null}.
+     */
+    public final DecodeOptions setSchemaResolver(final SchemaResolver resolver) {
+        Reject.ifNull(resolver);
+        this.schemaResolver = resolver;
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DereferenceAliasesPolicy.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DereferenceAliasesPolicy.java
new file mode 100644
index 0000000..7a4a588
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DereferenceAliasesPolicy.java
@@ -0,0 +1,157 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A Search operation alias dereferencing policy as defined in RFC 4511 section
+ * 4.5.1.3 is used to indicate whether or not alias entries (as defined in RFC
+ * 4512) are to be dereferenced during stages of a Search operation. The act of
+ * dereferencing an alias includes recursively dereferencing aliases that refer
+ * to aliases.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511#section-4.5.1.3">RFC 4511 -
+ *      Lightweight Directory Access Protocol (LDAP): The Protocol </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4512">RFC 4512 - Lightweight
+ *      Directory Access Protocol (LDAP): Directory Information Models </a>
+ */
+public final class DereferenceAliasesPolicy {
+    private static final DereferenceAliasesPolicy[] ELEMENTS = new DereferenceAliasesPolicy[4];
+
+    private static final List<DereferenceAliasesPolicy> IMMUTABLE_ELEMENTS = Collections
+            .unmodifiableList(Arrays.asList(ELEMENTS));
+
+    /**
+     * Do not dereference aliases in searching or in locating the base object of
+     * a Search operation.
+     */
+    public static final DereferenceAliasesPolicy NEVER = register(0, "never");
+
+    /**
+     * While searching subordinates of the base object, dereference any alias
+     * within the scope of the Search operation. Dereferenced objects become the
+     * vertices of further search scopes where the Search operation is also
+     * applied. If the search scope is {@code WHOLE_SUBTREE}, the Search
+     * continues in the subtree(s) of any dereferenced object. If the search
+     * scope is {@code SINGLE_LEVEL}, the search is applied to any dereferenced
+     * objects and is not applied to their subordinates.
+     */
+    public static final DereferenceAliasesPolicy IN_SEARCHING = register(1, "search");
+
+    /**
+     * Dereference aliases in locating the base object of a Search operation,
+     * but not when searching subordinates of the base object.
+     */
+    public static final DereferenceAliasesPolicy FINDING_BASE = register(2, "find");
+
+    /** Dereference aliases both in searching and in locating the base object of a Search operation. */
+    public static final DereferenceAliasesPolicy ALWAYS = register(3, "always");
+
+    /**
+     * Returns the alias dereferencing policy having the specified integer value
+     * as defined in RFC 4511 section 4.5.1.
+     *
+     * @param intValue
+     *            The integer value of the alias dereferencing policy.
+     * @return The dereference aliases policy, or {@code null} if there was no
+     *         alias dereferencing policy associated with {@code intValue}.
+     */
+    public static DereferenceAliasesPolicy valueOf(final int intValue) {
+        if (intValue < 0 || intValue >= ELEMENTS.length) {
+            return null;
+        }
+        return ELEMENTS[intValue];
+    }
+
+    /**
+     * Returns an unmodifiable list containing the set of available alias
+     * dereferencing policies indexed on their integer value as defined in RFC
+     * 4511 section 4.5.1.
+     *
+     * @return An unmodifiable list containing the set of available alias
+     *         dereferencing policies.
+     */
+    public static List<DereferenceAliasesPolicy> values() {
+        return IMMUTABLE_ELEMENTS;
+    }
+
+    /**
+     * Creates and registers a new alias dereferencing policy with the
+     * application.
+     *
+     * @param intValue
+     *            The integer value of the alias dereferencing policy as defined
+     *            in RFC 4511 section 4.5.1.
+     * @param name
+     *            The name of the alias dereferencing policy.
+     * @return The new alias dereferencing policy.
+     */
+    private static DereferenceAliasesPolicy register(final int intValue, final String name) {
+        final DereferenceAliasesPolicy t = new DereferenceAliasesPolicy(intValue, name);
+        ELEMENTS[intValue] = t;
+        return t;
+    }
+
+    private final int intValue;
+
+    private final String name;
+
+    /** Prevent direct instantiation. */
+    private DereferenceAliasesPolicy(final int intValue, final String name) {
+        this.intValue = intValue;
+        this.name = name;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof DereferenceAliasesPolicy) {
+            return this.intValue == ((DereferenceAliasesPolicy) obj).intValue;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return intValue;
+    }
+
+    /**
+     * Returns the integer value of this alias dereferencing policy as defined
+     * in RFC 4511 section 4.5.1.
+     *
+     * @return The integer value of this alias dereferencing policy.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    /**
+     * Returns the string representation of this alias dereferencing policy.
+     *
+     * @return The string representation of this alias dereferencing policy.
+     */
+    @Override
+    public String toString() {
+        return name;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java
new file mode 100644
index 0000000..d216898
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entries.java
@@ -0,0 +1,1103 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.ObjectClassType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+import org.forgerock.opendj.ldif.LDIF;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Iterables;
+
+import static org.forgerock.opendj.ldap.AttributeDescription.*;
+import static org.forgerock.opendj.ldap.LdapException.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class contains methods for creating and manipulating entries.
+ *
+ * @see Entry
+ */
+public final class Entries {
+    /**
+     * Options for controlling the behavior of the
+     * {@link Entries#diffEntries(Entry, Entry, DiffOptions) diffEntries}
+     * method. {@code DiffOptions} specify which attributes are compared, how
+     * they are compared, and the type of modifications generated.
+     *
+     * @see Entries#diffEntries(Entry, Entry, DiffOptions)
+     */
+    public static final class DiffOptions {
+        /** Selects which attributes will be compared. By default all user attributes will be compared. */
+        private AttributeFilter attributeFilter = USER_ATTRIBUTES_ONLY_FILTER;
+
+        /**
+         * When true, attribute values are compared byte for byte, otherwise
+         * they are compared using their matching rules.
+         */
+        private boolean useExactMatching;
+
+        /**
+         * When greater than 0, modifications with REPLACE type will be
+         * generated for the new attributes containing at least
+         * "useReplaceMaxValues" attribute values. Otherwise, modifications with
+         * DELETE + ADD types will be generated.
+         */
+        private int useReplaceMaxValues;
+
+        private DiffOptions() {
+            // Nothing to do.
+        }
+
+        /**
+         * Specifies an attribute filter which will be used to determine which
+         * attributes will be compared. By default only user attributes will be
+         * compared.
+         *
+         * @param attributeFilter
+         *            The filter which will be used to determine which
+         *            attributes will be compared.
+         * @return A reference to this set of options.
+         */
+        public DiffOptions attributes(final AttributeFilter attributeFilter) {
+            Reject.ifNull(attributeFilter);
+            this.attributeFilter = attributeFilter;
+            return this;
+        }
+
+        /**
+         * Specifies the list of attributes to be compared. By default only user
+         * attributes will be compared.
+         *
+         * @param attributeDescriptions
+         *            The names of the attributes to be compared.
+         * @return A reference to this set of options.
+         */
+        public DiffOptions attributes(final String... attributeDescriptions) {
+            return attributes(new AttributeFilter(attributeDescriptions));
+        }
+
+        /**
+         * Requests that attribute values should be compared byte for byte,
+         * rather than using their matching rules. This is useful when a client
+         * wishes to perform trivial changes to an attribute value which would
+         * otherwise be ignored by the matching rule, such as removing extra
+         * white space from an attribute, or capitalizing a user's name.
+         *
+         * @return A reference to this set of options.
+         */
+        public DiffOptions useExactMatching() {
+            this.useExactMatching = true;
+            return this;
+        }
+
+        /**
+         * Requests that all generated changes should use the
+         * {@link ModificationType#REPLACE REPLACE} modification type, rather
+         * than a combination of {@link ModificationType#DELETE DELETE} and
+         * {@link ModificationType#ADD ADD}.
+         * <p>
+         * Note that the generated changes will not be reversible, nor will they
+         * be efficient for attributes containing many values (such as groups).
+         * Enabling this option may result in more efficient updates for single
+         * valued attributes and reduce the amount of replication meta-data that
+         * needs to be maintained..
+         *
+         * @return A reference to this set of options.
+         */
+        public DiffOptions alwaysReplaceAttributes() {
+            return replaceMaxValues(Integer.MAX_VALUE);
+        }
+
+        /**
+         * Requests that the generated changes should use the
+         * {@link ModificationType#REPLACE REPLACE} modification type when the
+         * new attribute contains at most one attribute value. All other changes
+         * will use a combination of {@link ModificationType#DELETE DELETE} then
+         * {@link ModificationType#ADD ADD}.
+         * <p>
+         * Specifying this option will usually provide the best overall
+         * performance for single and multi-valued attribute updates, but the
+         * generated changes will probably not be reversible.
+         *
+         * @return A reference to this set of options.
+         */
+        public DiffOptions replaceSingleValuedAttributes() {
+            return replaceMaxValues(1);
+        }
+
+        /**
+         * Requests that the generated changes should use the
+         * {@link ModificationType#REPLACE REPLACE} modification type when the
+         * new attribute contains {@code maxValues} attribute values or less.
+         * All other changes will use a combination of
+         * {@link ModificationType#DELETE DELETE} then
+         * {@link ModificationType#ADD ADD}.
+         *
+         * @param maxValues
+         *            The maximum number of attribute values a modified
+         *            attribute can contain before reversible changes will be
+         *            generated.
+         * @return A reference to this set of options.
+         */
+        private DiffOptions replaceMaxValues(final int maxValues) {
+            // private until we can think of a good use case and better name.
+            Reject.ifFalse(maxValues >= 0, "maxValues must be >= 0");
+            this.useReplaceMaxValues = maxValues;
+            return this;
+        }
+
+        private Entry filter(final Entry entry) {
+            return attributeFilter.filteredViewOf(entry);
+        }
+    }
+
+    private static final class UnmodifiableEntry implements Entry {
+        private final Entry entry;
+
+        private UnmodifiableEntry(final Entry entry) {
+            this.entry = entry;
+        }
+
+        @Override
+        public boolean addAttribute(final Attribute attribute) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean addAttribute(final Attribute attribute,
+                final Collection<? super ByteString> duplicateValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry addAttribute(final String attributeDescription, final Object... values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry clearAttributes() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean containsAttribute(final Attribute attribute,
+                final Collection<? super ByteString> missingValues) {
+            return entry.containsAttribute(attribute, missingValues);
+        }
+
+        @Override
+        public boolean containsAttribute(final String attributeDescription, final Object... values) {
+            return entry.containsAttribute(attributeDescription, values);
+        }
+
+        @Override
+        public boolean equals(final Object object) {
+            return object == this || entry.equals(object);
+        }
+
+        @Override
+        public Iterable<Attribute> getAllAttributes() {
+            return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry
+                    .getAllAttributes(), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+        }
+
+        @Override
+        public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+            return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry
+                    .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+        }
+
+        @Override
+        public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+            return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry
+                    .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+        }
+
+        @Override
+        public Attribute getAttribute(final AttributeDescription attributeDescription) {
+            final Attribute attribute = entry.getAttribute(attributeDescription);
+            if (attribute != null) {
+                return Attributes.unmodifiableAttribute(attribute);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public Attribute getAttribute(final String attributeDescription) {
+            final Attribute attribute = entry.getAttribute(attributeDescription);
+            if (attribute != null) {
+                return Attributes.unmodifiableAttribute(attribute);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public int getAttributeCount() {
+            return entry.getAttributeCount();
+        }
+
+        @Override
+        public DN getName() {
+            return entry.getName();
+        }
+
+        @Override
+        public int hashCode() {
+            return entry.hashCode();
+        }
+
+        @Override
+        public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+            return entry.parseAttribute(attributeDescription);
+        }
+
+        @Override
+        public AttributeParser parseAttribute(final String attributeDescription) {
+            return entry.parseAttribute(attributeDescription);
+        }
+
+        @Override
+        public boolean removeAttribute(final Attribute attribute,
+                final Collection<? super ByteString> missingValues) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean removeAttribute(final AttributeDescription attributeDescription) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry removeAttribute(final String attributeDescription, final Object... values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean replaceAttribute(final Attribute attribute) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry replaceAttribute(final String attributeDescription, final Object... values) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry setName(final DN dn) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Entry setName(final String dn) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String toString() {
+            return entry.toString();
+        }
+    }
+
+    private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() {
+        @Override
+        public int compare(final Entry o1, final Entry o2) {
+            return o1.getName().compareTo(o2.getName());
+        }
+    };
+
+    private static final AttributeFilter USER_ATTRIBUTES_ONLY_FILTER = new AttributeFilter();
+    private static final DiffOptions DEFAULT_DIFF_OPTIONS = new DiffOptions();
+
+    private static final Function<Attribute, Attribute, NeverThrowsException> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
+            new Function<Attribute, Attribute, NeverThrowsException>() {
+                @Override
+                public Attribute apply(final Attribute value) {
+                    return Attributes.unmodifiableAttribute(value);
+                }
+
+            };
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries by name
+     * using the natural order for DN comparisons (parent before children).
+     * <p>
+     * In order to sort entries in reverse order (children first) use the
+     * following code:
+     *
+     * <pre>
+     * Collections.reverseOrder(Entries.compareByName());
+     * </pre>
+     *
+     * For more complex sort orders involving one or more attributes refer to
+     * the {@link SortKey} class.
+     *
+     * @return The {@code Comparator}.
+     */
+    public static Comparator<Entry> compareByName() {
+        return COMPARATOR;
+    }
+
+    /**
+     * Returns {@code true} if the provided entry is valid according to the
+     * specified schema and schema validation policy.
+     * <p>
+     * If attribute value validation is enabled then following checks will be
+     * performed:
+     * <ul>
+     * <li>checking that there is at least one value
+     * <li>checking that single-valued attributes contain only a single value
+     * </ul>
+     * In particular, attribute values will not be checked for conformance to
+     * their syntax since this is expected to have already been performed while
+     * adding the values to the entry.
+     *
+     * @param entry
+     *            The entry to be validated.
+     * @param schema
+     *            The schema against which the entry will be validated.
+     * @param policy
+     *            The schema validation policy.
+     * @param errorMessages
+     *            A collection into which any schema validation warnings or
+     *            error messages can be placed, or {@code null} if they should
+     *            not be saved.
+     * @return {@code true} if the provided entry is valid according to the
+     *         specified schema and schema validation policy.
+     * @see Schema#validateEntry(Entry, SchemaValidationPolicy, Collection)
+     */
+    public static boolean conformsToSchema(final Entry entry, final Schema schema,
+            final SchemaValidationPolicy policy, final Collection<LocalizableMessage> errorMessages) {
+        return schema.validateEntry(entry, policy, errorMessages);
+    }
+
+    /**
+     * Returns {@code true} if the provided entry is valid according to the
+     * default schema and schema validation policy.
+     * <p>
+     * If attribute value validation is enabled then following checks will be
+     * performed:
+     * <ul>
+     * <li>checking that there is at least one value
+     * <li>checking that single-valued attributes contain only a single value
+     * </ul>
+     * In particular, attribute values will not be checked for conformance to
+     * their syntax since this is expected to have already been performed while
+     * adding the values to the entry.
+     *
+     * @param entry
+     *            The entry to be validated.
+     * @param policy
+     *            The schema validation policy.
+     * @param errorMessages
+     *            A collection into which any schema validation warnings or
+     *            error messages can be placed, or {@code null} if they should
+     *            not be saved.
+     * @return {@code true} if the provided entry is valid according to the
+     *         default schema and schema validation policy.
+     * @see Schema#validateEntry(Entry, SchemaValidationPolicy, Collection)
+     */
+    public static boolean conformsToSchema(final Entry entry, final SchemaValidationPolicy policy,
+            final Collection<LocalizableMessage> errorMessages) {
+        return conformsToSchema(entry, Schema.getDefaultSchema(), policy, errorMessages);
+    }
+
+    /**
+     * Check if the provided entry contains the provided object class.
+     * <p>
+     * This method uses the default schema for decoding the object class
+     * attribute values.
+     * <p>
+     * The provided object class must be recognized by the schema, otherwise the
+     * method returns false.
+     *
+     * @param entry
+     *            The entry which is checked against the object class.
+     * @param objectClass
+     *            The object class to check.
+     * @return {@code true} if and only if entry contains the object class and
+     *         the object class is recognized by the default schema,
+     *         {@code false} otherwise
+     */
+    public static boolean containsObjectClass(final Entry entry, final ObjectClass objectClass) {
+        return containsObjectClass(entry, Schema.getDefaultSchema(), objectClass);
+    }
+
+    /**
+     * Check if the provided entry contains the provided object class.
+     * <p>
+     * The provided object class must be recognized by the provided schema,
+     * otherwise the method returns false.
+     *
+     * @param entry
+     *            The entry which is checked against the object class.
+     * @param schema
+     *            The schema which should be used for decoding the object class
+     *            attribute values.
+     * @param objectClass
+     *            The object class to check.
+     * @return {@code true} if and only if entry contains the object class and
+     *         the object class is recognized by the provided schema,
+     *         {@code false} otherwise
+     */
+    public static boolean containsObjectClass(final Entry entry, final Schema schema, final ObjectClass objectClass) {
+        return getObjectClasses(entry, schema).contains(objectClass);
+    }
+
+    /**
+     * Creates a new modify request containing a list of modifications which can
+     * be used to transform {@code fromEntry} into entry {@code toEntry}.
+     * <p>
+     * The changes will be generated using a default set of {@link DiffOptions
+     * options}. More specifically, only user attributes will be compared,
+     * attributes will be compared using their matching rules, and all generated
+     * changes will be reversible: it will contain only modifications of type
+     * {@link ModificationType#DELETE DELETE} then {@link ModificationType#ADD
+     * ADD}.
+     * <p>
+     * Finally, the modify request will use the distinguished name taken from
+     * {@code fromEntry}. This method will not check to see if both
+     * {@code fromEntry} and {@code toEntry} have the same distinguished name.
+     * <p>
+     * This method is equivalent to:
+     *
+     * <pre>
+     * ModifyRequest request = Requests.newModifyRequest(fromEntry, toEntry);
+     * </pre>
+     *
+     * Or:
+     *
+     * <pre>
+     * ModifyRequest request = diffEntries(fromEntry, toEntry, Entries.diffOptions());
+     * </pre>
+     *
+     * @param fromEntry
+     *            The source entry.
+     * @param toEntry
+     *            The destination entry.
+     * @return A modify request containing a list of modifications which can be
+     *         used to transform {@code fromEntry} into entry {@code toEntry}.
+     *         The returned request will always be non-{@code null} but may not
+     *         contain any modifications.
+     * @throws NullPointerException
+     *             If {@code fromEntry} or {@code toEntry} were {@code null}.
+     * @see Requests#newModifyRequest(Entry, Entry)
+     */
+    public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry) {
+        return diffEntries(fromEntry, toEntry, DEFAULT_DIFF_OPTIONS);
+    }
+
+    /**
+     * Creates a new modify request containing a list of modifications which can
+     * be used to transform {@code fromEntry} into entry {@code toEntry}.
+     * <p>
+     * The changes will be generated using the provided set of
+     * {@link DiffOptions}.
+     * <p>
+     * Finally, the modify request will use the distinguished name taken from
+     * {@code fromEntry}. This method will not check to see if both
+     * {@code fromEntry} and {@code toEntry} have the same distinguished name.
+     *
+     * @param fromEntry
+     *            The source entry.
+     * @param toEntry
+     *            The destination entry.
+     * @param options
+     *            The set of options which will control which attributes are
+     *            compared, how they are compared, and the type of modifications
+     *            generated.
+     * @return A modify request containing a list of modifications which can be
+     *         used to transform {@code fromEntry} into entry {@code toEntry}.
+     *         The returned request will always be non-{@code null} but may not
+     *         contain any modifications.
+     * @throws NullPointerException
+     *             If {@code fromEntry}, {@code toEntry}, or {@code options}
+     *             were {@code null}.
+     */
+    public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry,
+            final DiffOptions options) {
+        Reject.ifNull(fromEntry, toEntry, options);
+
+        final ModifyRequest request = Requests.newModifyRequest(fromEntry.getName());
+        final Entry tfrom = toFilteredTreeMapEntry(fromEntry, options);
+        final Entry tto = toFilteredTreeMapEntry(toEntry, options);
+        final Iterator<Attribute> ifrom = tfrom.getAllAttributes().iterator();
+        final Iterator<Attribute> ito = tto.getAllAttributes().iterator();
+
+        Attribute afrom = ifrom.hasNext() ? ifrom.next() : null;
+        Attribute ato = ito.hasNext() ? ito.next() : null;
+
+        while (afrom != null && ato != null) {
+            final AttributeDescription adfrom = afrom.getAttributeDescription();
+            final AttributeDescription adto = ato.getAttributeDescription();
+
+            final int cmp = adfrom.compareTo(adto);
+            if (cmp == 0) {
+                /* Attribute is in both entries so compute the differences between the old and new. */
+                if (options.useReplaceMaxValues >= ato.size()) {
+                    // This attribute is a candidate for replacing.
+                    if (diffAttributeNeedsReplacing(afrom, ato, options)) {
+                        request.addModification(new Modification(ModificationType.REPLACE, ato));
+                    }
+                } else if (afrom.size() == 1 && ato.size() == 1) {
+                    // Fast-path for single valued attributes.
+                    if (diffFirstValuesAreDifferent(options, afrom, ato)) {
+                        diffDeleteValues(request, afrom);
+                        diffAddValues(request, ato);
+                    }
+                } else if (options.useExactMatching) {
+                    /*
+                     * Compare multi-valued attributes using exact matching. Use
+                     * a hash sets for membership checking rather than the
+                     * attributes in order to avoid matching rule based
+                     * comparisons.
+                     */
+                    final Set<ByteString> oldValues = new LinkedHashSet<>(afrom);
+                    final Set<ByteString> newValues = new LinkedHashSet<>(ato);
+
+                    final Set<ByteString> deletedValues = new LinkedHashSet<>(oldValues);
+                    deletedValues.removeAll(newValues);
+                    diffDeleteValues(request, deletedValues.size() == afrom.size() ? afrom
+                            : new LinkedAttribute(adfrom, deletedValues));
+
+                    final Set<ByteString> addedValues = newValues;
+                    addedValues.removeAll(oldValues);
+                    diffAddValues(request, addedValues.size() == ato.size() ? ato
+                            : new LinkedAttribute(adto, addedValues));
+                } else {
+                    // Compare multi-valued attributes using matching rules.
+                    final Attribute deletedValues = new LinkedAttribute(afrom);
+                    deletedValues.removeAll(ato);
+                    diffDeleteValues(request, deletedValues);
+
+                    final Attribute addedValues = new LinkedAttribute(ato);
+                    addedValues.removeAll(afrom);
+                    diffAddValues(request, addedValues);
+                }
+
+                afrom = ifrom.hasNext() ? ifrom.next() : null;
+                ato = ito.hasNext() ? ito.next() : null;
+            } else if (cmp < 0) {
+                // afrom in source, but not destination.
+                diffDeleteAttribute(request, afrom, options);
+                afrom = ifrom.hasNext() ? ifrom.next() : null;
+            } else {
+                // ato in destination, but not in source.
+                diffAddAttribute(request, ato, options);
+                ato = ito.hasNext() ? ito.next() : null;
+            }
+        }
+
+        // Additional attributes in source entry: these must be deleted.
+        if (afrom != null) {
+            diffDeleteAttribute(request, afrom, options);
+        }
+        while (ifrom.hasNext()) {
+            diffDeleteAttribute(request, ifrom.next(), options);
+        }
+
+        // Additional attributes in destination entry: these must be added.
+        if (ato != null) {
+            diffAddAttribute(request, ato, options);
+        }
+        while (ito.hasNext()) {
+            diffAddAttribute(request, ito.next(), options);
+        }
+
+        return request;
+    }
+
+    /**
+     * Returns a new set of options which may be used to control how entries are
+     * compared and changes generated using
+     * {@link #diffEntries(Entry, Entry, DiffOptions)}. By default only user
+     * attributes will be compared, matching rules will be used for comparisons,
+     * and all generated changes will be reversible.
+     *
+     * @return A new set of options which may be used to control how entries are
+     *         compared and changes generated.
+     */
+    public static DiffOptions diffOptions() {
+        return new DiffOptions();
+    }
+
+    /**
+     * Returns an unmodifiable set containing the object classes associated with
+     * the provided entry. This method will ignore unrecognized object classes.
+     * <p>
+     * This method uses the default schema for decoding the object class
+     * attribute values.
+     *
+     * @param entry
+     *            The entry whose object classes are required.
+     * @return An unmodifiable set containing the object classes associated with
+     *         the provided entry.
+     */
+    public static Set<ObjectClass> getObjectClasses(final Entry entry) {
+        return getObjectClasses(entry, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns an unmodifiable set containing the object classes associated with
+     * the provided entry. This method will ignore unrecognized object classes.
+     *
+     * @param entry
+     *            The entry whose object classes are required.
+     * @param schema
+     *            The schema which should be used for decoding the object class
+     *            attribute values.
+     * @return An unmodifiable set containing the object classes associated with
+     *         the provided entry.
+     */
+    public static Set<ObjectClass> getObjectClasses(final Entry entry, final Schema schema) {
+        final Attribute objectClassAttribute =
+                entry.getAttribute(AttributeDescription.objectClass());
+        if (objectClassAttribute == null) {
+            return Collections.emptySet();
+        } else {
+            final Set<ObjectClass> objectClasses = new HashSet<>(objectClassAttribute.size());
+            for (final ByteString v : objectClassAttribute) {
+                final String objectClassName = v.toString();
+                final ObjectClass objectClass;
+                try {
+                    objectClass = schema.getObjectClass(objectClassName);
+                    objectClasses.add(objectClass);
+                } catch (final UnknownSchemaElementException e) {
+                    // Ignore.
+                    continue;
+                }
+            }
+            return Collections.unmodifiableSet(objectClasses);
+        }
+    }
+
+    /**
+     * Returns the structural object class associated with the provided entry,
+     * or {@code null} if none was found. If the entry contains multiple
+     * structural object classes then the first will be returned. This method
+     * will ignore unrecognized object classes.
+     * <p>
+     * This method uses the default schema for decoding the object class
+     * attribute values.
+     *
+     * @param entry
+     *            The entry whose structural object class is required.
+     * @return The structural object class associated with the provided entry,
+     *         or {@code null} if none was found.
+     */
+    public static ObjectClass getStructuralObjectClass(final Entry entry) {
+        return getStructuralObjectClass(entry, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Builds an entry from the provided lines of LDIF.
+     * <p>
+     * Sample usage:
+     * <pre>
+     * Entry john = makeEntry(
+     *   "dn: cn=John Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: John Smith",
+     *   "sn: Smith",
+     *   "givenname: John");
+     * </pre>
+     *
+     * @param ldifLines
+     *          LDIF lines that contains entry definition.
+     * @return an entry
+     * @throws LocalizedIllegalArgumentException
+     *            If {@code ldifLines} did not contain an LDIF entry, or
+     *            contained multiple entries, or contained malformed LDIF, or
+     *            if the entry could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static Entry makeEntry(String... ldifLines) {
+        return LDIF.makeEntry(ldifLines);
+    }
+
+    /**
+     * Builds a list of entries from the provided lines of LDIF.
+     * <p>
+     * Sample usage:
+     * <pre>
+     * List&lt;Entry&gt; smiths = TestCaseUtils.makeEntries(
+     *   "dn: cn=John Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: John Smith",
+     *   "sn: Smith",
+     *   "givenname: John",
+     *   "",
+     *   "dn: cn=Jane Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: Jane Smith",
+     *   "sn: Smith",
+     *   "givenname: Jane");
+     * </pre>
+     * @param ldifLines
+     *          LDIF lines that contains entries definition.
+     *          Entries are separated by an empty string: {@code ""}.
+     * @return a non empty list of entries
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain LDIF entries,
+     *             or contained malformed LDIF, or if the entries
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static List<Entry> makeEntries(String... ldifLines) {
+        return LDIF.makeEntries(ldifLines);
+    }
+
+    /**
+     * Returns the structural object class associated with the provided entry,
+     * or {@code null} if none was found. If the entry contains multiple
+     * structural object classes then the first will be returned. This method
+     * will ignore unrecognized object classes.
+     *
+     * @param entry
+     *            The entry whose structural object class is required.
+     * @param schema
+     *            The schema which should be used for decoding the object class
+     *            attribute values.
+     * @return The structural object class associated with the provided entry,
+     *         or {@code null} if none was found.
+     */
+    public static ObjectClass getStructuralObjectClass(final Entry entry, final Schema schema) {
+        ObjectClass structuralObjectClass = null;
+        final Attribute objectClassAttribute = entry.getAttribute(objectClass());
+
+        if (objectClassAttribute == null) {
+            return null;
+        }
+
+        for (final ByteString v : objectClassAttribute) {
+            final String objectClassName = v.toString();
+            final ObjectClass objectClass;
+            try {
+                objectClass = schema.getObjectClass(objectClassName);
+            } catch (final UnknownSchemaElementException e) {
+                // Ignore.
+                continue;
+            }
+
+            if (objectClass.getObjectClassType() == ObjectClassType.STRUCTURAL
+                    && (structuralObjectClass == null || objectClass.isDescendantOf(structuralObjectClass))) {
+                structuralObjectClass = objectClass;
+            }
+        }
+
+        return structuralObjectClass;
+    }
+
+    /**
+     * Applies the provided modification to an entry. This method implements
+     * "permissive" modify semantics, ignoring attempts to add duplicate values
+     * or attempts to remove values which do not exist.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param change
+     *            The modification to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws LdapException
+     *             If an error occurred while performing the change such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             will not have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final Modification change) throws LdapException {
+        return modifyEntry(entry, change, null);
+    }
+
+    /**
+     * Applies the provided modification to an entry. This method implements
+     * "permissive" modify semantics, recording attempts to add duplicate values
+     * or attempts to remove values which do not exist in the provided
+     * collection if provided.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param change
+     *            The modification to be applied to the entry.
+     * @param conflictingValues
+     *            A collection into which duplicate or missing values will be
+     *            added, or {@code null} if conflicting values should not be
+     *            saved.
+     * @return A reference to the updated entry.
+     * @throws LdapException
+     *             If an error occurred while performing the change such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             will not have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final Modification change,
+            final Collection<? super ByteString> conflictingValues) throws LdapException {
+        return modifyEntry0(entry, change, conflictingValues, true);
+    }
+
+    /**
+     * Applies the provided modification request to an entry. This method will
+     * utilize "permissive" modify semantics if the request contains the
+     * {@link PermissiveModifyRequestControl}.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws LdapException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to add duplicate values, remove values which do not
+     *             exist, or increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final ModifyRequest changes) throws LdapException {
+        final boolean isPermissive = changes.containsControl(PermissiveModifyRequestControl.OID);
+        return modifyEntry0(entry, changes.getModifications(), isPermissive);
+    }
+
+    /**
+     * Applies the provided modifications to an entry using "permissive" modify
+     * semantics.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws LdapException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntryPermissive(final Entry entry,
+            final Collection<Modification> changes) throws LdapException {
+        return modifyEntry0(entry, changes, true);
+    }
+
+    /**
+     * Applies the provided modifications to an entry using "strict" modify
+     * semantics. Attempts to add duplicate values or attempts to remove values
+     * which do not exist will cause the update to fail.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws LdapException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to add duplicate values, remove values which do not
+     *             exist, or increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntryStrict(final Entry entry, final Collection<Modification> changes)
+            throws LdapException {
+        return modifyEntry0(entry, changes, false);
+    }
+
+    /**
+     * Returns a read-only view of {@code entry} and its attributes. Query
+     * operations on the returned entry and its attributes "read-through" to the
+     * underlying entry or attribute, and attempts to modify the returned entry
+     * and its attributes either directly or indirectly via an iterator result
+     * in an {@code UnsupportedOperationException}.
+     *
+     * @param entry
+     *            The entry for which a read-only view is to be returned.
+     * @return A read-only view of {@code entry}.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public static Entry unmodifiableEntry(final Entry entry) {
+        if (entry instanceof UnmodifiableEntry) {
+            return entry;
+        } else {
+            return new UnmodifiableEntry(entry);
+        }
+    }
+
+    private static void diffAddAttribute(final ModifyRequest request, final Attribute ato,
+            final DiffOptions diffOptions) {
+        if (diffOptions.useReplaceMaxValues > 0) {
+            request.addModification(new Modification(ModificationType.REPLACE, ato));
+        } else {
+            request.addModification(new Modification(ModificationType.ADD, ato));
+        }
+    }
+
+    private static void diffAddValues(final ModifyRequest request, final Attribute addedValues) {
+        if (addedValues != null && !addedValues.isEmpty()) {
+            request.addModification(new Modification(ModificationType.ADD, addedValues));
+        }
+    }
+
+    private static boolean diffAttributeNeedsReplacing(final Attribute afrom, final Attribute ato,
+            final DiffOptions options) {
+        if (afrom.size() != ato.size()) {
+            return true;
+        } else if (afrom.size() == 1) {
+            return diffFirstValuesAreDifferent(options, afrom, ato);
+        } else if (options.useExactMatching) {
+            /*
+             * Use a hash set for membership checking rather than the attribute
+             * in order to avoid matching rule based comparisons.
+             */
+            final Set<ByteString> oldValues = new LinkedHashSet<>(afrom);
+            return !oldValues.containsAll(ato);
+        } else {
+            return !afrom.equals(ato);
+        }
+    }
+
+    private static void diffDeleteAttribute(final ModifyRequest request, final Attribute afrom,
+            final DiffOptions diffOptions) {
+        if (diffOptions.useReplaceMaxValues > 0) {
+            request.addModification(new Modification(ModificationType.REPLACE, Attributes
+                    .emptyAttribute(afrom.getAttributeDescription())));
+        } else {
+            request.addModification(new Modification(ModificationType.DELETE, afrom));
+        }
+    }
+
+    private static void diffDeleteValues(final ModifyRequest request, final Attribute deletedValues) {
+        if (deletedValues != null && !deletedValues.isEmpty()) {
+            request.addModification(new Modification(ModificationType.DELETE, deletedValues));
+        }
+    }
+
+    private static boolean diffFirstValuesAreDifferent(final DiffOptions diffOptions,
+            final Attribute afrom, final Attribute ato) {
+        if (diffOptions.useExactMatching) {
+            return !afrom.firstValue().equals(ato.firstValue());
+        } else {
+            return !afrom.contains(ato.firstValue());
+        }
+    }
+
+    private static void incrementAttribute(final Entry entry, final Attribute change)
+            throws LdapException {
+        // First parse the change.
+        final AttributeDescription deltaAd = change.getAttributeDescription();
+        if (change.size() != 1) {
+            throw newLdapException(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(deltaAd.toString()).toString());
+        }
+        final long delta;
+        try {
+            delta = change.parse().asLong();
+        } catch (final Exception e) {
+            throw newLdapException(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
+        }
+
+        // Now apply the increment to the attribute.
+        final Attribute oldAttribute = entry.getAttribute(deltaAd);
+        if (oldAttribute == null) {
+            throw newLdapException(ResultCode.NO_SUCH_ATTRIBUTE,
+                    ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE.get(deltaAd.toString()).toString());
+        }
+
+        // Re-use existing attribute description in case it differs in case, etc.
+        final Attribute newAttribute = new LinkedAttribute(oldAttribute.getAttributeDescription());
+        try {
+            for (final Long value : oldAttribute.parse().asSetOfLong()) {
+                newAttribute.add(value + delta);
+            }
+        } catch (final Exception e) {
+            throw newLdapException(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
+        }
+        entry.replaceAttribute(newAttribute);
+    }
+
+    private static Entry modifyEntry0(final Entry entry, final Collection<Modification> changes,
+            final boolean isPermissive) throws LdapException {
+        final Collection<ByteString> conflictingValues =
+                isPermissive ? null : new ArrayList<ByteString>(0);
+        for (final Modification change : changes) {
+            modifyEntry0(entry, change, conflictingValues, isPermissive);
+        }
+        return entry;
+    }
+
+    private static Entry modifyEntry0(final Entry entry, final Modification change,
+            final Collection<? super ByteString> conflictingValues, final boolean isPermissive)
+            throws LdapException {
+        final ModificationType modType = change.getModificationType();
+        if (modType.equals(ModificationType.ADD)) {
+            entry.addAttribute(change.getAttribute(), conflictingValues);
+            if (!isPermissive && !conflictingValues.isEmpty()) {
+                // Duplicate values.
+                throw newLdapException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
+                        ERR_ENTRY_DUPLICATE_VALUES.get(
+                                change.getAttribute().getAttributeDescriptionAsString()).toString());
+            }
+        } else if (modType.equals(ModificationType.DELETE)) {
+            final boolean hasChanged =
+                    entry.removeAttribute(change.getAttribute(), conflictingValues);
+            if (!isPermissive && (!hasChanged || !conflictingValues.isEmpty())) {
+                // Missing attribute or values.
+                throw newLdapException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_ENTRY_NO_SUCH_VALUE.get(
+                        change.getAttribute().getAttributeDescriptionAsString()).toString());
+            }
+        } else if (modType.equals(ModificationType.REPLACE)) {
+            entry.replaceAttribute(change.getAttribute());
+        } else if (modType.equals(ModificationType.INCREMENT)) {
+            incrementAttribute(entry, change.getAttribute());
+        } else {
+            throw newLdapException(ResultCode.UNWILLING_TO_PERFORM,
+                    ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(String.valueOf(modType)).toString());
+        }
+        return entry;
+    }
+
+    private static Entry toFilteredTreeMapEntry(final Entry entry, final DiffOptions options) {
+        if (entry instanceof TreeMapEntry) {
+            return options.filter(entry);
+        } else {
+            return new TreeMapEntry(options.filter(entry));
+        }
+    }
+
+    /** Prevent instantiation. */
+    private Entries() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entry.java
new file mode 100644
index 0000000..f355959
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Entry.java
@@ -0,0 +1,522 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+/**
+ * An entry, comprising of a distinguished name and zero or more attributes.
+ * <p>
+ * Some methods require a schema in order to decode their parameters (e.g.
+ * {@link #addAttribute(String, Object...)} and {@link #setName(String)}). In
+ * these cases the default schema is used unless an alternative schema is
+ * specified in the {@code Entry} constructor. The default schema is not used
+ * for any other purpose. In particular, an {@code Entry} may contain attributes
+ * which have been decoded using different schemas.
+ * <p>
+ * When determining whether or not an entry already contains a particular
+ * attribute, attribute descriptions will be compared using
+ * {@link AttributeDescription#matches}.
+ * <p>
+ * Full LDAP modify semantics are provided via the {@link #addAttribute},
+ * {@link #removeAttribute}, and {@link #replaceAttribute} methods.
+ * <p>
+ * Implementations should specify any constraints or special behavior.
+ * Specifically:
+ * <ul>
+ * <li>Which methods are supported.
+ * <li>The order in which attributes are returned using the
+ * {@link #getAllAttributes()} method.
+ * <li>How existing attributes are modified during calls to
+ * {@link #addAttribute}, {@link #removeAttribute}, and
+ * {@link #replaceAttribute} and the conditions, if any, where a reference to
+ * the passed in attribute is maintained.
+ * </ul>
+ *
+ * @see Entries
+ */
+public interface Entry {
+
+    /**
+     * Ensures that this entry contains the provided attribute and values
+     * (optional operation). This method has the following semantics:
+     * <ul>
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
+     * </ul>
+     * <p>
+     * <b>NOTE:</b> When {@code attribute} is non-empty, this method implements
+     * LDAP Modify add semantics.
+     *
+     * @param attribute
+     *            The attribute values to be added to this entry, merging with
+     *            any existing attribute values.
+     * @return {@code true} if this entry changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be added.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    boolean addAttribute(Attribute attribute);
+
+    /**
+     * Ensures that this entry contains the provided attribute and values
+     * (optional operation). This method has the following semantics:
+     * <ul>
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
+     * </ul>
+     * <p>
+     * <b>NOTE:</b> When {@code attribute} is non-empty, this method implements
+     * LDAP Modify add semantics.
+     *
+     * @param attribute
+     *            The attribute values to be added to this entry, merging with
+     *            any existing attribute values.
+     * @param duplicateValues
+     *            A collection into which duplicate values will be added, or
+     *            {@code null} if duplicate values should not be saved.
+     * @return {@code true} if this entry changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be added.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    boolean addAttribute(Attribute attribute, Collection<? super ByteString> duplicateValues);
+
+    /**
+     * Ensures that this entry contains the provided attribute and values
+     * (optional operation). This method has the following semantics:
+     * <ul>
+     * <li>If this entry does not already contain an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * this entry will be modified such that it contains {@code attribute}, even
+     * if it is empty.
+     * <li>If this entry already contains an attribute with a
+     * {@link AttributeDescription#matches matching} attribute description, then
+     * the attribute values contained in {@code attribute} will be merged with
+     * the existing attribute values.
+     * </ul>
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>NOTE:</b> When {@code attribute} is non-empty, this method implements
+     * LDAP Modify add semantics.
+     *
+     * @param attributeDescription
+     *            The name of the attribute whose values are to be added.
+     * @param values
+     *            The attribute values to be added to this entry, merging any
+     *            existing attribute values.
+     * @return This entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be added.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Entry addAttribute(String attributeDescription, Object... values);
+
+    /**
+     * Removes all the attributes from this entry (optional operation).
+     *
+     * @return This entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes to be removed.
+     */
+    Entry clearAttributes();
+
+    /**
+     * Returns {@code true} if this entry contains all of the attribute values
+     * contained in {@code attribute}. If {@code attribute} is empty then this
+     * method will return {@code true} if the attribute is present in this
+     * entry, regardless of how many values it contains.
+     *
+     * @param attribute
+     *            The attribute values whose presence in this entry is to be
+     *            tested.
+     * @param missingValues
+     *            A collection into which missing values will be added, or
+     *            {@code null} if missing values should not be saved.
+     * @return {@code true} if this entry contains all of the attribute values
+     *         contained in {@code attribute}.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    boolean containsAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    /**
+     * Returns {@code true} if this entry contains all of the attribute values
+     * contained in {@code values}. If {@code values} is {@code null} or empty
+     * then this method will return {@code true} if the attribute is present in
+     * this entry, regardless of how many values it contains.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The name of the attribute whose presence in this entry is to
+     *            be tested.
+     * @param values
+     *            The attribute values whose presence in this entry is to be
+     *            tested, which may be {@code null}.
+     * @return {@code true} if this entry contains all of the attribute values
+     *         contained in {@code values}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    boolean containsAttribute(String attributeDescription, Object... values);
+
+    /**
+     * Returns {@code true} if {@code object} is an entry which is equal to this
+     * entry. Two entries are considered equal if their distinguished names are
+     * equal, they both have the same number of attributes, and every attribute
+     * contained in the first entry is also contained in the second entry.
+     *
+     * @param object
+     *            The object to be tested for equality with this entry.
+     * @return {@code true} if {@code object} is an entry which is equal to this
+     *         entry, or {@code false} if not.
+     */
+    @Override
+    boolean equals(Object object);
+
+    /**
+     * Returns an {@code Iterable} containing all of the attributes in this
+     * entry. The returned {@code Iterable} may be used to remove attributes if
+     * permitted by this entry.
+     *
+     * @return An {@code Iterable} containing all of the attributes.
+     */
+    Iterable<Attribute> getAllAttributes();
+
+    /**
+     * Returns an {@code Iterable} containing all the attributes in this entry
+     * having an attribute description which is a sub-type of the provided
+     * attribute description. The returned {@code Iterable} may be used to
+     * remove attributes if permitted by this entry.
+     *
+     * @param attributeDescription
+     *            The name of the attributes to be returned.
+     * @return An {@code Iterable} containing the matching attributes.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Iterable<Attribute> getAllAttributes(AttributeDescription attributeDescription);
+
+    /**
+     * Returns an {@code Iterable} containing all the attributes in this entry
+     * having an attribute description which is a sub-type of the provided
+     * attribute description. The returned {@code Iterable} may be used to
+     * remove attributes if permitted by this entry.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     *
+     * @param attributeDescription
+     *            The name of the attributes to be returned.
+     * @return An {@code Iterable} containing the matching attributes.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Iterable<Attribute> getAllAttributes(String attributeDescription);
+
+    /**
+     * Returns the named attribute contained in this entry, or {@code null} if
+     * it is not included with this entry.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be returned.
+     * @return The named attribute, or {@code null} if it is not included with
+     *         this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Attribute getAttribute(AttributeDescription attributeDescription);
+
+    /**
+     * Returns the named attribute contained in this entry, or {@code null} if
+     * it is not included with this entry.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be returned.
+     * @return The named attribute, or {@code null} if it is not included with
+     *         this entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Attribute getAttribute(String attributeDescription);
+
+    /**
+     * Returns the number of attributes in this entry.
+     *
+     * @return The number of attributes.
+     */
+    int getAttributeCount();
+
+    /**
+     * Returns the string representation of the distinguished name of this
+     * entry.
+     *
+     * @return The string representation of the distinguished name.
+     */
+    DN getName();
+
+    /**
+     * Returns the hash code for this entry. It will be calculated as the sum of
+     * the hash codes of the distinguished name and all of the attributes.
+     *
+     * @return The hash code for this entry.
+     */
+    @Override
+    int hashCode();
+
+    /**
+     * Returns a parser for the named attribute contained in this entry.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be parsed.
+     * @return A parser for the named attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    AttributeParser parseAttribute(AttributeDescription attributeDescription);
+
+    /**
+     * Returns a parser for the named attribute contained in this entry.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be parsed.
+     * @return A parser for the named attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    AttributeParser parseAttribute(String attributeDescription);
+
+    /**
+     * Removes all of the attribute values contained in {@code attribute} from
+     * this entry if it is present (optional operation). If {@code attribute} is
+     * empty then the entire attribute will be removed if it is present.
+     * <p>
+     * <b>NOTE:</b> This method implements LDAP Modify delete semantics.
+     *
+     * @param attribute
+     *            The attribute values to be removed from this entry, which may
+     *            be empty if the entire attribute is to be removed.
+     * @param missingValues
+     *            A collection into which missing values will be added, or
+     *            {@code null} if missing values should not be saved.
+     * @return {@code true} if this entry changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be removed.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    boolean removeAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    /**
+     * Removes the named attribute from this entry if it is present (optional
+     * operation). If this attribute does not contain the attribute, the call
+     * leaves this entry unchanged and returns {@code false}.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be removed.
+     * @return {@code true} if this entry changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes to be removed.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    boolean removeAttribute(AttributeDescription attributeDescription);
+
+    /**
+     * Removes all of the attribute values contained in {@code values} from the
+     * named attribute in this entry if it is present (optional operation). If
+     * {@code values} is {@code null} or empty then the entire attribute will be
+     * removed if it is present.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>NOTE:</b> This method implements LDAP Modify delete semantics.
+     *
+     * @param attributeDescription
+     *            The name of the attribute whose values are to be removed.
+     * @param values
+     *            The attribute values to be removed from this entry, which may
+     *            be {@code null} or empty if the entire attribute is to be
+     *            removed.
+     * @return This entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be removed.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    Entry removeAttribute(String attributeDescription, Object... values);
+
+    /**
+     * Adds all of the attribute values contained in {@code attribute} to this
+     * entry, replacing any existing attribute values (optional operation). If
+     * {@code attribute} is empty then the entire attribute will be removed if
+     * it is present.
+     * <p>
+     * <b>NOTE:</b> This method implements LDAP Modify replace semantics as
+     * described in <a href="http://tools.ietf.org/html/rfc4511#section-4.6"
+     * >RFC 4511 - Section 4.6. Modify Operation</a>.
+     *
+     * @param attribute
+     *            The attribute values to be added to this entry, replacing any
+     *            existing attribute values, and which may be empty if the
+     *            entire attribute is to be removed.
+     * @return {@code true} if this entry changed as a result of this call.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be replaced.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    boolean replaceAttribute(Attribute attribute);
+
+    /**
+     * Adds all of the attribute values contained in {@code values} to this
+     * entry, replacing any existing attribute values (optional operation). If
+     * {@code values} is {@code null} or empty then the entire attribute will be
+     * removed if it is present.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>NOTE:</b> This method implements LDAP Modify replace semantics as
+     * described in <a href="http://tools.ietf.org/html/rfc4511#section-4.6"
+     * >RFC 4511 - Section 4.6. Modify Operation</a>.
+     *
+     * @param attributeDescription
+     *            The name of the attribute whose values are to be replaced.
+     * @param values
+     *            The attribute values to be added to this entry, replacing any
+     *            existing attribute values, and which may be {@code null} or
+     *            empty if the entire attribute is to be removed.
+     * @return This entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit attributes or their values to
+     *             be replaced.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    Entry replaceAttribute(String attributeDescription, Object... values);
+
+    /**
+     * Sets the distinguished name of this entry (optional operation).
+     *
+     * @param dn
+     *            The distinguished name.
+     * @return This entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit the distinguished name to be
+     *             set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    Entry setName(DN dn);
+
+    /**
+     * Sets the distinguished name of this entry (optional operation).
+     * <p>
+     * The distinguished name will be decoded using the schema associated with
+     * this entry (usually the default schema).
+     *
+     * @param dn
+     *            The string representation of the distinguished name.
+     * @return This entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the schema
+     *             associated with this entry.
+     * @throws UnsupportedOperationException
+     *             If this entry does not permit the distinguished name to be
+     *             set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    Entry setName(String dn);
+
+    /**
+     * Returns a string representation of this entry.
+     *
+     * @return The string representation of this entry.
+     */
+    @Override
+    String toString();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryFactory.java
new file mode 100644
index 0000000..f800630
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryFactory.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * Entry factories are included with a set of {@code DecodeOptions} in order to
+ * allow application to control how {@code Entry} instances are created when
+ * decoding requests and responses.
+ *
+ * @see Entry
+ * @see DecodeOptions
+ */
+public interface EntryFactory {
+    /**
+     * Creates an empty entry using the provided distinguished name and no
+     * attributes.
+     *
+     * @param name
+     *            The distinguished name of the entry to be created.
+     * @return The new entry.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    Entry newEntry(DN name);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryNotFoundException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryNotFoundException.java
new file mode 100644
index 0000000..ef45d3e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/EntryNotFoundException.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * failed because the target entry was not found by the Directory Server. More
+ * specifically, this exception is used for the following error result codes:
+ * <ul>
+ * <li>{@link ResultCode#NO_SUCH_OBJECT NO_SUCH_OBJECT} - the requested
+ * operation failed because it referenced an entry that does not exist.
+ * <li>{@link ResultCode#REFERRAL REFERRAL} - the requested operation failed
+ * because it referenced an entry that is located on another server.
+ * <li>{@link ResultCode#CLIENT_SIDE_NO_RESULTS_RETURNED
+ * CLIENT_SIDE_NO_RESULTS_RETURNED} - the requested single entry search
+ * operation or read operation failed because the Directory Server did not
+ * return any matching entries.
+ * </ul>
+ * <b>NOTE:</b> referrals are handled by the {@link ReferralException}
+ * sub-class.
+ */
+@SuppressWarnings("serial")
+public class EntryNotFoundException extends LdapException {
+    EntryNotFoundException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Filter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Filter.java
new file mode 100644
index 0000000..2246921
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Filter.java
@@ -0,0 +1,1653 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2011 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/**
+ * A search filter as defined in RFC 4511. In addition this class also provides
+ * support for the absolute true and absolute false filters as defined in RFC
+ * 4526.
+ * <p>
+ * This class provides many factory methods for creating common types of filter.
+ * Applications interact with a filter using {@link FilterVisitor} which is
+ * applied to a filter using the {@link #accept(FilterVisitor, Object)} method.
+ * <p>
+ * The RFC 4515 string representation of a filter can be generated using the
+ * {@link #toString} methods and parsed using the {@link #valueOf(String)}
+ * factory method.
+ * <p>
+ * Filters can be constructed using the various factory methods. For example,
+ * the following code illustrates how to create a filter having the string
+ * representation "{@code (&(cn=bjensen)(age>=21))}":
+ *
+ * <pre>
+ * import static org.forgerock.opendj.Filter.*;
+ *
+ * Filter filter = and(equality("cn", "bjensen"), greaterOrEqual("age", 21));
+ *
+ * // Alternatively use a filter template:
+ * Filter filter = Filter.format("(&(cn=%s)(age>=%s))", "bjensen", 21);
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
+ *      Directory Access Protocol (LDAP): The Protocol </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4515">RFC 4515 - String
+ *      Representation of Search Filters </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4526">RFC 4526 - Absolute True
+ *      and False Filters </a>
+ */
+public final class Filter {
+
+    /** The asterisk character. */
+    private static final byte ASTERISK = 0x2A;
+
+    /** The backslash character. */
+    private static final byte BACKSLASH = 0x5C;
+
+    private static final class AndImpl extends Impl {
+        private final List<Filter> subFilters;
+
+        public AndImpl(final List<Filter> subFilters) {
+            this.subFilters = subFilters;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitAndFilter(p, subFilters);
+        }
+
+    }
+
+    private static final class ApproxMatchImpl extends Impl {
+
+        private final ByteString assertionValue;
+
+        private final String attributeDescription;
+
+        public ApproxMatchImpl(final String attributeDescription, final ByteString assertionValue) {
+            this.attributeDescription = attributeDescription;
+            this.assertionValue = assertionValue;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitApproxMatchFilter(p, attributeDescription, assertionValue);
+        }
+
+    }
+
+    private static final class EqualityMatchImpl extends Impl {
+
+        private final ByteString assertionValue;
+
+        private final String attributeDescription;
+
+        public EqualityMatchImpl(final String attributeDescription, final ByteString assertionValue) {
+            this.attributeDescription = attributeDescription;
+            this.assertionValue = assertionValue;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitEqualityMatchFilter(p, attributeDescription, assertionValue);
+        }
+
+    }
+
+    private static final class ExtensibleMatchImpl extends Impl {
+        private final String attributeDescription;
+
+        private final boolean dnAttributes;
+
+        private final String matchingRule;
+
+        private final ByteString matchValue;
+
+        public ExtensibleMatchImpl(final String matchingRule, final String attributeDescription,
+                final ByteString matchValue, final boolean dnAttributes) {
+            this.matchingRule = matchingRule;
+            this.attributeDescription = attributeDescription;
+            this.matchValue = matchValue;
+            this.dnAttributes = dnAttributes;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitExtensibleMatchFilter(p, matchingRule, attributeDescription, matchValue,
+                    dnAttributes);
+        }
+
+    }
+
+    private static final class GreaterOrEqualImpl extends Impl {
+
+        private final ByteString assertionValue;
+
+        private final String attributeDescription;
+
+        public GreaterOrEqualImpl(final String attributeDescription, final ByteString assertionValue) {
+            this.attributeDescription = attributeDescription;
+            this.assertionValue = assertionValue;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitGreaterOrEqualFilter(p, attributeDescription, assertionValue);
+        }
+
+    }
+
+    private static abstract class Impl {
+        protected Impl() {
+            // Nothing to do.
+        }
+
+        public abstract <R, P> R accept(FilterVisitor<R, P> v, P p);
+    }
+
+    private static final class LessOrEqualImpl extends Impl {
+
+        private final ByteString assertionValue;
+
+        private final String attributeDescription;
+
+        public LessOrEqualImpl(final String attributeDescription, final ByteString assertionValue) {
+            this.attributeDescription = attributeDescription;
+            this.assertionValue = assertionValue;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitLessOrEqualFilter(p, attributeDescription, assertionValue);
+        }
+
+    }
+
+    private static final class NotImpl extends Impl {
+        private final Filter subFilter;
+
+        public NotImpl(final Filter subFilter) {
+            this.subFilter = subFilter;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitNotFilter(p, subFilter);
+        }
+
+    }
+
+    private static final class OrImpl extends Impl {
+        private final List<Filter> subFilters;
+
+        public OrImpl(final List<Filter> subFilters) {
+            this.subFilters = subFilters;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitOrFilter(p, subFilters);
+        }
+
+    }
+
+    private static final class PresentImpl extends Impl {
+
+        private final String attributeDescription;
+
+        public PresentImpl(final String attributeDescription) {
+            this.attributeDescription = attributeDescription;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitPresentFilter(p, attributeDescription);
+        }
+
+    }
+
+    private static final class SubstringsImpl extends Impl {
+
+        private final List<ByteString> anyStrings;
+
+        private final String attributeDescription;
+
+        private final ByteString finalString;
+
+        private final ByteString initialString;
+
+        public SubstringsImpl(final String attributeDescription, final ByteString initialString,
+                final List<ByteString> anyStrings, final ByteString finalString) {
+            this.attributeDescription = attributeDescription;
+            this.initialString = initialString;
+            this.anyStrings = anyStrings;
+            this.finalString = finalString;
+
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitSubstringsFilter(p, attributeDescription, initialString, anyStrings,
+                    finalString);
+        }
+
+    }
+
+    private static final class UnrecognizedImpl extends Impl {
+
+        private final ByteString filterBytes;
+
+        private final byte filterTag;
+
+        public UnrecognizedImpl(final byte filterTag, final ByteString filterBytes) {
+            this.filterTag = filterTag;
+            this.filterBytes = filterBytes;
+        }
+
+        @Override
+        public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+            return v.visitUnrecognizedFilter(p, filterTag, filterBytes);
+        }
+
+    }
+
+    /** RFC 4526 - FALSE filter. */
+    private static final Filter FALSE = new Filter(new OrImpl(Collections.<Filter> emptyList()));
+
+    /** Heavily used (objectClass=*) filter. */
+    private static final Filter OBJECT_CLASS_PRESENT = new Filter(new PresentImpl("objectClass"));
+
+    private static final FilterVisitor<StringBuilder, StringBuilder> TO_STRING_VISITOR =
+            new FilterVisitor<StringBuilder, StringBuilder>() {
+
+                @Override
+                public StringBuilder visitAndFilter(final StringBuilder builder,
+                        final List<Filter> subFilters) {
+                    builder.append("(&");
+                    for (final Filter subFilter : subFilters) {
+                        subFilter.accept(this, builder);
+                    }
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitOrFilter(final StringBuilder builder,
+                        final List<Filter> subFilters) {
+                    builder.append("(|");
+                    for (final Filter subFilter : subFilters) {
+                        subFilter.accept(this, builder);
+                    }
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitApproxMatchFilter(final StringBuilder builder,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    return visitBinaryOperator(builder, attributeDescription, "~=", assertionValue);
+                }
+
+                @Override
+                public StringBuilder visitEqualityMatchFilter(final StringBuilder builder,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    return visitBinaryOperator(builder, attributeDescription, "=", assertionValue);
+                }
+
+                @Override
+                public StringBuilder visitGreaterOrEqualFilter(final StringBuilder builder,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    return visitBinaryOperator(builder, attributeDescription, ">=", assertionValue);
+                }
+
+                @Override
+                public StringBuilder visitLessOrEqualFilter(final StringBuilder builder,
+                        final String attributeDescription, final ByteString assertionValue) {
+                    return visitBinaryOperator(builder, attributeDescription, "<=", assertionValue);
+                }
+
+                private StringBuilder visitBinaryOperator(final StringBuilder builder,
+                        final String attributeDescription, final String operator, final ByteString assertionValue) {
+                    builder.append('(');
+                    builder.append(attributeDescription);
+                    builder.append(operator);
+                    valueToFilterString(builder, assertionValue);
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitExtensibleMatchFilter(final StringBuilder builder,
+                        final String matchingRule, final String attributeDescription,
+                        final ByteString assertionValue, final boolean dnAttributes) {
+                    builder.append('(');
+
+                    if (attributeDescription != null) {
+                        builder.append(attributeDescription);
+                    }
+
+                    if (dnAttributes) {
+                        builder.append(":dn");
+                    }
+
+                    if (matchingRule != null) {
+                        builder.append(':');
+                        builder.append(matchingRule);
+                    }
+
+                    builder.append(":=");
+                    valueToFilterString(builder, assertionValue);
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitNotFilter(final StringBuilder builder,
+                        final Filter subFilter) {
+                    builder.append("(!");
+                    subFilter.accept(this, builder);
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitPresentFilter(final StringBuilder builder,
+                        final String attributeDescription) {
+                    builder.append('(');
+                    builder.append(attributeDescription);
+                    builder.append("=*)");
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitSubstringsFilter(final StringBuilder builder,
+                        final String attributeDescription, final ByteString initialSubstring,
+                        final List<ByteString> anySubstrings, final ByteString finalSubstring) {
+                    builder.append('(');
+                    builder.append(attributeDescription);
+                    builder.append("=");
+                    if (initialSubstring != null) {
+                        valueToFilterString(builder, initialSubstring);
+                    }
+                    for (final ByteString anySubstring : anySubstrings) {
+                        builder.append('*');
+                        valueToFilterString(builder, anySubstring);
+                    }
+                    builder.append('*');
+                    if (finalSubstring != null) {
+                        valueToFilterString(builder, finalSubstring);
+                    }
+                    builder.append(')');
+                    return builder;
+                }
+
+                @Override
+                public StringBuilder visitUnrecognizedFilter(final StringBuilder builder,
+                        final byte filterTag, final ByteString filterBytes) {
+                    // Fake up a representation.
+                    builder.append('(');
+                    builder.append(byteToHex(filterTag));
+                    builder.append(':');
+                    builder.append(filterBytes.toHexString());
+                    builder.append(')');
+                    return builder;
+                }
+            };
+
+    /** RFC 4526 - TRUE filter. */
+    private static final Filter TRUE = new Filter(new AndImpl(Collections.<Filter> emptyList()));
+
+    /**
+     * Returns the {@code absolute false} filter as defined in RFC 4526 which is
+     * comprised of an {@code or} filter containing zero components.
+     *
+     * @return The absolute false filter.
+     * @see <a href="http://tools.ietf.org/html/rfc4526">RFC 4526</a>
+     */
+    public static Filter alwaysFalse() {
+        return FALSE;
+    }
+
+    /**
+     * Returns the {@code absolute true} filter as defined in RFC 4526 which is
+     * comprised of an {@code and} filter containing zero components.
+     *
+     * @return The absolute true filter.
+     * @see <a href="http://tools.ietf.org/html/rfc4526">RFC 4526</a>
+     */
+    public static Filter alwaysTrue() {
+        return TRUE;
+    }
+
+    /**
+     * Creates a new {@code and} filter using the provided list of sub-filters.
+     * <p>
+     * Creating a new {@code and} filter with a {@code null} or empty list of
+     * sub-filters is equivalent to calling {@link #alwaysTrue()}.
+     *
+     * @param subFilters
+     *            The list of sub-filters, may be empty or {@code null}.
+     * @return The newly created {@code and} filter.
+     */
+    public static Filter and(final Collection<Filter> subFilters) {
+        if (subFilters == null || subFilters.isEmpty()) {
+            // RFC 4526 - TRUE filter.
+            return alwaysTrue();
+        } else if (subFilters.size() == 1) {
+            final Filter subFilter = subFilters.iterator().next();
+            Reject.ifNull(subFilter);
+            return new Filter(new AndImpl(Collections.singletonList(subFilter)));
+        } else {
+            final List<Filter> subFiltersList = new ArrayList<>(subFilters.size());
+            for (final Filter subFilter : subFilters) {
+                Reject.ifNull(subFilter);
+                subFiltersList.add(subFilter);
+            }
+            return new Filter(new AndImpl(Collections.unmodifiableList(subFiltersList)));
+        }
+    }
+
+    /**
+     * Creates a new {@code and} filter using the provided list of sub-filters.
+     * <p>
+     * Creating a new {@code and} filter with a {@code null} or empty list of
+     * sub-filters is equivalent to calling {@link #alwaysTrue()}.
+     *
+     * @param subFilters
+     *            The list of sub-filters, may be empty or {@code null}.
+     * @return The newly created {@code and} filter.
+     */
+    public static Filter and(final Filter... subFilters) {
+        if (subFilters == null || subFilters.length == 0) {
+            // RFC 4526 - TRUE filter.
+            return alwaysTrue();
+        } else if (subFilters.length == 1) {
+            Reject.ifNull(subFilters[0]);
+            return new Filter(new AndImpl(Collections.singletonList(subFilters[0])));
+        } else {
+            final List<Filter> subFiltersList = new ArrayList<>(subFilters.length);
+            for (final Filter subFilter : subFilters) {
+                Reject.ifNull(subFilter);
+                subFiltersList.add(subFilter);
+            }
+            return new Filter(new AndImpl(Collections.unmodifiableList(subFiltersList)));
+        }
+    }
+
+    /**
+     * Creates a new {@code approximate match} filter using the provided
+     * attribute description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code approximate match} filter.
+     */
+    public static Filter approx(final String attributeDescription, final Object assertionValue) {
+        Reject.ifNull(attributeDescription, assertionValue);
+        return new Filter(new ApproxMatchImpl(attributeDescription, ByteString
+                .valueOfObject(assertionValue)));
+    }
+
+    /**
+     * Creates a new {@code equality match} filter using the provided attribute
+     * description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code equality match} filter.
+     */
+    public static Filter equality(final String attributeDescription, final Object assertionValue) {
+        Reject.ifNull(attributeDescription, assertionValue);
+        return new Filter(new EqualityMatchImpl(attributeDescription, ByteString
+                .valueOfObject(assertionValue)));
+    }
+
+    /**
+     * Returns the LDAP string representation of the provided filter assertion
+     * value in a form suitable for substitution directly into a filter string.
+     * This method may be useful in cases where a filter is to be constructed
+     * from a filter template using {@code String#format(String, Object...)}.
+     * The following example illustrates two approaches to constructing an
+     * equality filter:
+     *
+     * <pre>
+     * // This may contain user input.
+     * String assertionValue = ...;
+     *
+     * // Using the equality filter constructor:
+     * Filter filter = Filter.equality("cn", assertionValue);
+     *
+     * // Using a String template:
+     * String filterTemplate = "(cn=%s)";
+     * String filterString = String.format(filterTemplate,
+     *                                     Filter.escapeAssertionValue(assertionValue));
+     * Filter filter = Filter.valueOf(filterString);
+     * </pre>
+     *
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>Note:</b> assertion values do not and should not be escaped before
+     * passing them to constructors like {@link #equality(String, Object)}.
+     * Escaping is only required when creating filter strings.
+     *
+     * @param assertionValue
+     *            The assertion value.
+     * @return The LDAP string representation of the provided filter assertion
+     *         value in a form suitable for substitution directly into a filter
+     *         string.
+     * @see #format(String, Object...)
+     */
+    public static String escapeAssertionValue(final Object assertionValue) {
+        Reject.ifNull(assertionValue);
+        final ByteString bytes = ByteString.valueOfObject(assertionValue);
+        final StringBuilder builder = new StringBuilder(bytes.length());
+        valueToFilterString(builder, bytes);
+        return builder.toString();
+    }
+
+    /**
+     * Creates a new {@code extensible match} filter.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param matchingRule
+     *            The matching rule name, may be {@code null} if
+     *            {@code attributeDescription} is specified.
+     * @param attributeDescription
+     *            The attribute description, may be {@code null} if
+     *            {@code matchingRule} is specified.
+     * @param assertionValue
+     *            The assertion value.
+     * @param dnAttributes
+     *            Indicates whether DN matching should be performed.
+     * @return The newly created {@code extensible match} filter.
+     */
+    public static Filter extensible(final String matchingRule, final String attributeDescription,
+            final Object assertionValue, final boolean dnAttributes) {
+        Reject.ifFalse(matchingRule != null || attributeDescription != null,
+                "matchingRule and/or attributeDescription must not be null");
+        Reject.ifNull(assertionValue);
+        return new Filter(new ExtensibleMatchImpl(matchingRule, attributeDescription, ByteString
+                .valueOfObject(assertionValue), dnAttributes));
+    }
+
+    /**
+     * Creates a new {@code greater or equal} filter using the provided
+     * attribute description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code greater or equal} filter.
+     */
+    public static Filter greaterOrEqual(final String attributeDescription,
+            final Object assertionValue) {
+        Reject.ifNull(attributeDescription, assertionValue);
+        return new Filter(new GreaterOrEqualImpl(attributeDescription, ByteString
+                .valueOfObject(assertionValue)));
+    }
+
+    /**
+     * Creates a new {@code greater than} filter using the provided attribute
+     * description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>NOTE:</b> since LDAP does not support {@code greater than}
+     * comparisons, this method returns a filter of the form
+     * {@code (&(type>=value)(!(type=value)))}. An alternative is to return a
+     * filter of the form {@code (!(type<=value))} , however the outer
+     * {@code not} filter will often prevent directory servers from optimizing
+     * the search using indexes.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code greater than} filter.
+     */
+    public static Filter greaterThan(final String attributeDescription, final Object assertionValue) {
+        return and(greaterOrEqual(attributeDescription, assertionValue), not(equality(
+                attributeDescription, assertionValue)));
+    }
+
+    /**
+     * Creates a new {@code less or equal} filter using the provided attribute
+     * description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code less or equal} filter.
+     */
+    public static Filter lessOrEqual(final String attributeDescription, final Object assertionValue) {
+        Reject.ifNull(attributeDescription, assertionValue);
+        return new Filter(new LessOrEqualImpl(attributeDescription, ByteString
+                .valueOfObject(assertionValue)));
+    }
+
+    /**
+     * Creates a new {@code less than} filter using the provided attribute
+     * description and assertion value.
+     * <p>
+     * If {@code assertionValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     * <p>
+     * <b>NOTE:</b> since LDAP does not support {@code less than} comparisons,
+     * this method returns a filter of the form
+     * {@code (&(type<=value)(!(type=value)))}. An alternative is to return a
+     * filter of the form {@code (!(type>=value))} , however the outer
+     * {@code not} filter will often prevent directory servers from optimizing
+     * the search using indexes.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return The newly created {@code less than} filter.
+     */
+    public static Filter lessThan(final String attributeDescription, final Object assertionValue) {
+        return and(lessOrEqual(attributeDescription, assertionValue), not(equality(
+                attributeDescription, assertionValue)));
+    }
+
+    /**
+     * Creates a new {@code not} filter using the provided sub-filter.
+     *
+     * @param subFilter
+     *            The sub-filter.
+     * @return The newly created {@code not} filter.
+     */
+    public static Filter not(final Filter subFilter) {
+        Reject.ifNull(subFilter);
+        return new Filter(new NotImpl(subFilter));
+    }
+
+    /**
+     * Returns the {@code objectClass} presence filter {@code (objectClass=*)}.
+     * <p>
+     * A call to this method is equivalent to but more efficient than the
+     * following code:
+     *
+     * <pre>
+     * Filter.present(&quot;objectClass&quot;);
+     * </pre>
+     *
+     * @return The {@code objectClass} presence filter {@code (objectClass=*)}.
+     */
+    public static Filter objectClassPresent() {
+        return OBJECT_CLASS_PRESENT;
+    }
+
+    /**
+     * Creates a new {@code or} filter using the provided list of sub-filters.
+     * <p>
+     * Creating a new {@code or} filter with a {@code null} or empty list of
+     * sub-filters is equivalent to calling {@link #alwaysFalse()}.
+     *
+     * @param subFilters
+     *            The list of sub-filters, may be empty or {@code null}.
+     * @return The newly created {@code or} filter.
+     */
+    public static Filter or(final Collection<Filter> subFilters) {
+        if (subFilters == null || subFilters.isEmpty()) {
+            // RFC 4526 - FALSE filter.
+            return alwaysFalse();
+        } else if (subFilters.size() == 1) {
+            final Filter subFilter = subFilters.iterator().next();
+            Reject.ifNull(subFilter);
+            return new Filter(new OrImpl(Collections.singletonList(subFilter)));
+        } else {
+            final List<Filter> subFiltersList = new ArrayList<>(subFilters.size());
+            for (final Filter subFilter : subFilters) {
+                Reject.ifNull(subFilter);
+                subFiltersList.add(subFilter);
+            }
+            return new Filter(new OrImpl(Collections.unmodifiableList(subFiltersList)));
+        }
+    }
+
+    /**
+     * Creates a new {@code or} filter using the provided list of sub-filters.
+     * <p>
+     * Creating a new {@code or} filter with a {@code null} or empty list of
+     * sub-filters is equivalent to calling {@link #alwaysFalse()}.
+     *
+     * @param subFilters
+     *            The list of sub-filters, may be empty or {@code null}.
+     * @return The newly created {@code or} filter.
+     */
+    public static Filter or(final Filter... subFilters) {
+        if (subFilters == null || subFilters.length == 0) {
+            // RFC 4526 - FALSE filter.
+            return alwaysFalse();
+        } else if (subFilters.length == 1) {
+            Reject.ifNull(subFilters[0]);
+            return new Filter(new OrImpl(Collections.singletonList(subFilters[0])));
+        } else {
+            final List<Filter> subFiltersList = new ArrayList<>(subFilters.length);
+            for (final Filter subFilter : subFilters) {
+                Reject.ifNull(subFilter);
+                subFiltersList.add(subFilter);
+            }
+            return new Filter(new OrImpl(Collections.unmodifiableList(subFiltersList)));
+        }
+    }
+
+    /**
+     * Creates a new {@code present} filter using the provided attribute
+     * description.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @return The newly created {@code present} filter.
+     */
+    public static Filter present(final String attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        if ("objectclass".equals(toLowerCase(attributeDescription))) {
+            return OBJECT_CLASS_PRESENT;
+        }
+        return new Filter(new PresentImpl(attributeDescription));
+    }
+
+    /**
+     * Creates a new {@code substrings} filter using the provided attribute
+     * description, {@code initial}, {@code final}, and {@code any} sub-strings.
+     * <p>
+     * Any substrings which are not instances of {@code ByteString} will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param initialSubstring
+     *            The initial sub-string, may be {@code null} if either
+     *            {@code finalSubstring} or {@code anySubstrings} are specified.
+     * @param anySubstrings
+     *            The final sub-string, may be {@code null} or empty if either
+     *            {@code finalSubstring} or {@code initialSubstring} are
+     *            specified.
+     * @param finalSubstring
+     *            The final sub-string, may be {@code null}, may be {@code null}
+     *            if either {@code initialSubstring} or {@code anySubstrings}
+     *            are specified.
+     * @return The newly created {@code substrings} filter.
+     */
+    public static Filter substrings(final String attributeDescription,
+            final Object initialSubstring, final Collection<?> anySubstrings,
+            final Object finalSubstring) {
+        Reject.ifNull(attributeDescription);
+        Reject.ifFalse(initialSubstring != null
+                || finalSubstring != null
+                || (anySubstrings != null && !anySubstrings.isEmpty()),
+                "at least one substring (initial, any or final) must be specified");
+
+        List<ByteString> anySubstringList;
+        if (anySubstrings == null || anySubstrings.isEmpty()) {
+            anySubstringList = Collections.emptyList();
+        } else if (anySubstrings.size() == 1) {
+            final Object anySubstring = anySubstrings.iterator().next();
+            Reject.ifNull(anySubstring);
+            anySubstringList = Collections.singletonList(ByteString.valueOfObject(anySubstring));
+        } else {
+            anySubstringList = new ArrayList<>(anySubstrings.size());
+            for (final Object anySubstring : anySubstrings) {
+                Reject.ifNull(anySubstring);
+
+                anySubstringList.add(ByteString.valueOfObject(anySubstring));
+            }
+            anySubstringList = Collections.unmodifiableList(anySubstringList);
+        }
+
+        return new Filter(new SubstringsImpl(attributeDescription,
+                initialSubstring != null ? ByteString.valueOfObject(initialSubstring) : null,
+                anySubstringList, finalSubstring != null ? ByteString.valueOfObject(finalSubstring)
+                        : null));
+    }
+
+    /**
+     * Creates a new {@code unrecognized} filter using the provided ASN1 filter
+     * tag and content. This type of filter should be used for filters which are
+     * not part of the standard filter definition.
+     *
+     * @param filterTag
+     *            The ASN.1 tag.
+     * @param filterBytes
+     *            The filter content.
+     * @return The newly created {@code unrecognized} filter.
+     */
+    public static Filter unrecognized(final byte filterTag, final ByteString filterBytes) {
+        Reject.ifNull(filterBytes);
+        return new Filter(new UnrecognizedImpl(filterTag, filterBytes));
+    }
+
+    /**
+     * Parses the provided LDAP string representation of a filter as a
+     * {@code Filter}.
+     *
+     * @param string
+     *            The LDAP string representation of a filter.
+     * @return The parsed {@code Filter}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code string} is not a valid LDAP string representation
+     *             of a filter.
+     * @see #format(String, Object...)
+     */
+    public static Filter valueOf(final String string) {
+        Reject.ifNull(string);
+
+        // If the filter is enclosed in a pair of single quotes it
+        // is invalid (issue #1024).
+        if (string.length() > 1 && string.startsWith("'") && string.endsWith("'")) {
+            final LocalizableMessage message = ERR_LDAP_FILTER_ENCLOSED_IN_APOSTROPHES.get(string);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        try {
+            if (string.startsWith("(")) {
+                if (string.endsWith(")")) {
+                    return valueOf0(string, 1, string.length() - 1);
+                } else {
+                    final LocalizableMessage message =
+                            ERR_LDAP_FILTER_MISMATCHED_PARENTHESES.get(string, 1, string.length());
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+            } else {
+                // We tolerate the top level filter component not being
+                // surrounded by parentheses.
+                return valueOf0(string, 0, string.length());
+            }
+        } catch (final LocalizedIllegalArgumentException liae) {
+            throw liae;
+        } catch (final Exception e) {
+            final LocalizableMessage message =
+                    ERR_LDAP_FILTER_UNCAUGHT_EXCEPTION.get(string, String.valueOf(e));
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Creates a new filter using the provided filter template and unescaped
+     * assertion values. This method first escapes each of the assertion values
+     * and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP filter using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a filter is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;(|(cn=%s)(uid=user.%s))&quot;;
+     * Filter filter = Filter.format(template, &quot;alice&quot;, 123);
+     * </pre>
+     *
+     * Any assertion values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param template
+     *            The filter template.
+     * @param assertionValues
+     *            The assertion values to be substituted into the template.
+     * @return The formatted template parsed as a {@code Filter}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a filter.
+     * @see #escapeAssertionValue(Object)
+     */
+    public static Filter format(final String template, final Object... assertionValues) {
+        final String[] assertionValueStrings = new String[assertionValues.length];
+        for (int i = 0; i < assertionValues.length; i++) {
+            assertionValueStrings[i] = escapeAssertionValue(assertionValues[i]);
+        }
+        final String filterString = String.format(template, (Object[]) assertionValueStrings);
+        return valueOf(filterString);
+    }
+
+    /** Converts an assertion value to a substring filter. */
+    private static Filter assertionValue2SubstringFilter(final String filterString,
+            final String attrType, final int equalPos, final int endPos) {
+        // Get a binary representation of the value.
+        final byte[] valueBytes = getBytes(filterString.substring(equalPos, endPos));
+
+        // Find the locations of all the asterisks in the value. Also, check to
+        // see if there are any escaped values, since they will need special treatment.
+        boolean hasEscape = false;
+        final LinkedList<Integer> asteriskPositions = new LinkedList<>();
+        for (int i = 0; i < valueBytes.length; i++) {
+            if (valueBytes[i] == ASTERISK) {
+                asteriskPositions.add(i);
+            } else if (valueBytes[i] == BACKSLASH) {
+                hasEscape = true;
+            }
+        }
+
+        // If there were no asterisks, then this isn't a substring filter.
+        if (asteriskPositions.isEmpty()) {
+            final LocalizableMessage message =
+                    ERR_LDAP_FILTER_SUBSTRING_NO_ASTERISKS.get(filterString, equalPos + 1, endPos);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // If the value starts with an asterisk, then there is no subInitial
+        // component. Otherwise, parse out the subInitial.
+        ByteString subInitial;
+        int firstPos = asteriskPositions.removeFirst();
+        if (firstPos == 0) {
+            subInitial = null;
+        } else if (hasEscape) {
+            final ByteStringBuilder buffer = new ByteStringBuilder(firstPos);
+            escapeHexChars(buffer, attrType, valueBytes, 0, firstPos, equalPos);
+            subInitial = buffer.toByteString();
+        } else {
+            subInitial = ByteString.wrap(valueBytes, 0, firstPos);
+        }
+
+        // Next, process through the rest of the asterisks to get the subAny values.
+        final ArrayList<ByteString> subAny = new ArrayList<>();
+        for (final int asteriskPos : asteriskPositions) {
+            final int length = asteriskPos - firstPos - 1;
+
+            if (hasEscape) {
+                final ByteStringBuilder buffer = new ByteStringBuilder(length);
+                escapeHexChars(buffer, attrType, valueBytes, firstPos + 1, asteriskPos, equalPos);
+                subAny.add(buffer.toByteString());
+                buffer.clear();
+            } else {
+                subAny.add(ByteString.wrap(valueBytes, firstPos + 1, length));
+            }
+            firstPos = asteriskPos;
+        }
+
+        // Finally, see if there is anything after the last asterisk, which
+        // would be the subFinal value.
+        ByteString subFinal;
+        if (firstPos == (valueBytes.length - 1)) {
+            subFinal = null;
+        } else {
+            final int length = valueBytes.length - firstPos - 1;
+
+            if (hasEscape) {
+                final ByteStringBuilder buffer = new ByteStringBuilder(length);
+                escapeHexChars(buffer, attrType, valueBytes, firstPos + 1, valueBytes.length,
+                        equalPos);
+                subFinal = buffer.toByteString();
+            } else {
+                subFinal = ByteString.wrap(valueBytes, firstPos + 1, length);
+            }
+        }
+        return new Filter(new SubstringsImpl(attrType, subInitial, subAny, subFinal));
+    }
+
+    private static void escapeHexChars(final ByteStringBuilder valueBuffer, final String string,
+            final byte[] valueBytes, final int fromIndex, final int len, final int errorIndex) {
+        for (int i = fromIndex; i < len; i++) {
+            if (valueBytes[i] == BACKSLASH) {
+                // The next two bytes must be the hex characters that comprise
+                // the binary value.
+                if (i + 2 >= valueBytes.length) {
+                    final LocalizableMessage message =
+                            ERR_LDAP_FILTER_INVALID_ESCAPED_BYTE.get(string, errorIndex + i + 1);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                byte byteValue = 0;
+                switch (valueBytes[++i]) {
+                case 0x30: // '0'
+                    break;
+                case 0x31: // '1'
+                    byteValue = (byte) 0x10;
+                    break;
+                case 0x32: // '2'
+                    byteValue = (byte) 0x20;
+                    break;
+                case 0x33: // '3'
+                    byteValue = (byte) 0x30;
+                    break;
+                case 0x34: // '4'
+                    byteValue = (byte) 0x40;
+                    break;
+                case 0x35: // '5'
+                    byteValue = (byte) 0x50;
+                    break;
+                case 0x36: // '6'
+                    byteValue = (byte) 0x60;
+                    break;
+                case 0x37: // '7'
+                    byteValue = (byte) 0x70;
+                    break;
+                case 0x38: // '8'
+                    byteValue = (byte) 0x80;
+                    break;
+                case 0x39: // '9'
+                    byteValue = (byte) 0x90;
+                    break;
+                case 0x41: // 'A'
+                case 0x61: // 'a'
+                    byteValue = (byte) 0xA0;
+                    break;
+                case 0x42: // 'B'
+                case 0x62: // 'b'
+                    byteValue = (byte) 0xB0;
+                    break;
+                case 0x43: // 'C'
+                case 0x63: // 'c'
+                    byteValue = (byte) 0xC0;
+                    break;
+                case 0x44: // 'D'
+                case 0x64: // 'd'
+                    byteValue = (byte) 0xD0;
+                    break;
+                case 0x45: // 'E'
+                case 0x65: // 'e'
+                    byteValue = (byte) 0xE0;
+                    break;
+                case 0x46: // 'F'
+                case 0x66: // 'f'
+                    byteValue = (byte) 0xF0;
+                    break;
+                default:
+                    final LocalizableMessage message =
+                            ERR_LDAP_FILTER_INVALID_ESCAPED_BYTE.get(string, errorIndex + i + 1);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                switch (valueBytes[++i]) {
+                case 0x30: // '0'
+                    break;
+                case 0x31: // '1'
+                    byteValue |= 0x01;
+                    break;
+                case 0x32: // '2'
+                    byteValue |= 0x02;
+                    break;
+                case 0x33: // '3'
+                    byteValue |= 0x03;
+                    break;
+                case 0x34: // '4'
+                    byteValue |= 0x04;
+                    break;
+                case 0x35: // '5'
+                    byteValue |= 0x05;
+                    break;
+                case 0x36: // '6'
+                    byteValue |= 0x06;
+                    break;
+                case 0x37: // '7'
+                    byteValue |= 0x07;
+                    break;
+                case 0x38: // '8'
+                    byteValue |= 0x08;
+                    break;
+                case 0x39: // '9'
+                    byteValue |= 0x09;
+                    break;
+                case 0x41: // 'A'
+                case 0x61: // 'a'
+                    byteValue |= 0x0A;
+                    break;
+                case 0x42: // 'B'
+                case 0x62: // 'b'
+                    byteValue |= 0x0B;
+                    break;
+                case 0x43: // 'C'
+                case 0x63: // 'c'
+                    byteValue |= 0x0C;
+                    break;
+                case 0x44: // 'D'
+                case 0x64: // 'd'
+                    byteValue |= 0x0D;
+                    break;
+                case 0x45: // 'E'
+                case 0x65: // 'e'
+                    byteValue |= 0x0E;
+                    break;
+                case 0x46: // 'F'
+                case 0x66: // 'f'
+                    byteValue |= 0x0F;
+                    break;
+                default:
+                    final LocalizableMessage message =
+                            ERR_LDAP_FILTER_INVALID_ESCAPED_BYTE.get(string, errorIndex + i + 1);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                valueBuffer.appendByte(byteValue);
+            } else {
+                valueBuffer.appendByte(valueBytes[i]);
+            }
+        }
+    }
+
+    private static Filter valueOf0(final String string, final int beginIndex /* inclusive */,
+            final int endIndex /* exclusive */) {
+        if (beginIndex >= endIndex) {
+            final LocalizableMessage message = ERR_LDAP_FILTER_STRING_NULL.get();
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        final int index = beginIndex;
+        final char c = string.charAt(index);
+
+        if (c == '&') {
+            final List<Filter> subFilters = valueOfFilterList(string, index + 1, endIndex);
+            if (subFilters.isEmpty()) {
+                return alwaysTrue();
+            } else {
+                return new Filter(new AndImpl(subFilters));
+            }
+        } else if (c == '|') {
+            final List<Filter> subFilters = valueOfFilterList(string, index + 1, endIndex);
+            if (subFilters.isEmpty()) {
+                return alwaysFalse();
+            } else {
+                return new Filter(new OrImpl(subFilters));
+            }
+        } else if (c == '!') {
+            if ((string.charAt(index + 1) != '(') || (string.charAt(endIndex - 1) != ')')) {
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_COMPOUND_MISSING_PARENTHESES.get(string, index,
+                                endIndex - 1);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+            final List<Filter> subFilters = valueOfFilterList(string, index + 1, endIndex);
+            if (subFilters.size() != 1) {
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_NOT_EXACTLY_ONE.get(string, index, endIndex);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+            return new Filter(new NotImpl(subFilters.get(0)));
+        } else {
+            // It must be a simple filter. It must have an equal sign at some
+            // point, so find it.
+            final int equalPos = indexOf(string, index, endIndex);
+            if (equalPos <= index) {
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_NO_EQUAL_SIGN.get(string, index, endIndex);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Look at the character immediately before the equal sign,
+            // because it may help determine the filter type.
+            String attributeDescription;
+            ByteString assertionValue;
+
+            switch (string.charAt(equalPos - 1)) {
+            case '~':
+                attributeDescription = valueOfAttributeDescription(string, index, equalPos - 1);
+                assertionValue = valueOfAssertionValue(string, equalPos + 1, endIndex);
+                return new Filter(new ApproxMatchImpl(attributeDescription, assertionValue));
+            case '>':
+                attributeDescription = valueOfAttributeDescription(string, index, equalPos - 1);
+                assertionValue = valueOfAssertionValue(string, equalPos + 1, endIndex);
+                return new Filter(new GreaterOrEqualImpl(attributeDescription, assertionValue));
+            case '<':
+                attributeDescription = valueOfAttributeDescription(string, index, equalPos - 1);
+                assertionValue = valueOfAssertionValue(string, equalPos + 1, endIndex);
+                return new Filter(new LessOrEqualImpl(attributeDescription, assertionValue));
+            case ':':
+                return valueOfExtensibleFilter(string, index, equalPos, endIndex);
+            default:
+                attributeDescription = valueOfAttributeDescription(string, index, equalPos);
+                return valueOfGenericFilter(string, attributeDescription, equalPos + 1, endIndex);
+            }
+        }
+    }
+
+    private static int indexOf(final String string, final int index, final int endIndex) {
+        for (int i = index; i < endIndex; i++) {
+            if (string.charAt(i) == '=') {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private static ByteString valueOfAssertionValue(final String string, final int startIndex,
+            final int endIndex) {
+        final byte[] valueBytes = getBytes(string.substring(startIndex, endIndex));
+        if (hasEscape(valueBytes)) {
+            final ByteStringBuilder valueBuffer = new ByteStringBuilder(valueBytes.length);
+            escapeHexChars(valueBuffer, string, valueBytes, 0, valueBytes.length, startIndex);
+            return valueBuffer.toByteString();
+        } else {
+            return ByteString.wrap(valueBytes);
+        }
+    }
+
+    private static boolean hasEscape(final byte[] valueBytes) {
+        for (final byte valueByte : valueBytes) {
+            if (valueByte == BACKSLASH) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String valueOfAttributeDescription(final String string, final int startIndex,
+            final int endIndex) {
+        // The part of the filter string before the equal sign should be the
+        // attribute type. Make sure that the characters it contains are
+        // acceptable for attribute types, including those allowed by
+        // attribute name exceptions (ASCII letters and digits, the dash,
+        // and the underscore). We also need to allow attribute options,
+        // which includes the semicolon and the equal sign.
+        final String attrType = string.substring(startIndex, endIndex);
+        for (int i = 0; i < attrType.length(); i++) {
+            switch (attrType.charAt(i)) {
+            case '-':
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            case ';':
+            case '=':
+            case 'A':
+            case 'B':
+            case 'C':
+            case 'D':
+            case 'E':
+            case 'F':
+            case 'G':
+            case 'H':
+            case 'I':
+            case 'J':
+            case 'K':
+            case 'L':
+            case 'M':
+            case 'N':
+            case 'O':
+            case 'P':
+            case 'Q':
+            case 'R':
+            case 'S':
+            case 'T':
+            case 'U':
+            case 'V':
+            case 'W':
+            case 'X':
+            case 'Y':
+            case 'Z':
+            case '_':
+            case 'a':
+            case 'b':
+            case 'c':
+            case 'd':
+            case 'e':
+            case 'f':
+            case 'g':
+            case 'h':
+            case 'i':
+            case 'j':
+            case 'k':
+            case 'l':
+            case 'm':
+            case 'n':
+            case 'o':
+            case 'p':
+            case 'q':
+            case 'r':
+            case 's':
+            case 't':
+            case 'u':
+            case 'v':
+            case 'w':
+            case 'x':
+            case 'y':
+            case 'z':
+                // These are all OK.
+                break;
+
+            case '.':
+            case '/':
+            case ':':
+            case '<':
+            case '>':
+            case '?':
+            case '@':
+            case '[':
+            case '\\':
+            case ']':
+            case '^':
+            case '`':
+                // These are not allowed, but they are explicitly called out
+                // because they are included in the range of values between '-'
+                // and 'z', and making sure all possible characters are included
+                // can help make the switch statement more efficient. We'll fall
+                // through to the default clause to reject them.
+            default:
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_INVALID_CHAR_IN_ATTR_TYPE.get(attrType, String
+                                .valueOf(attrType.charAt(i)), i);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        return attrType;
+    }
+
+    private static Filter valueOfExtensibleFilter(final String string, final int startIndex,
+            final int equalIndex, final int endIndex) {
+        String attributeDescription = null;
+        boolean dnAttributes = false;
+        String matchingRule = null;
+
+        // Look at the first character. If it is a colon, then it must be
+        // followed by either the string "dn" or the matching rule ID. If it
+        // is not, then must be the attribute type.
+        final String lowerLeftStr = toLowerCase(string.substring(startIndex, equalIndex));
+        if (string.charAt(startIndex) == ':') {
+            // See if it starts with ":dn". Otherwise, it much be the matching
+            // rule ID.
+            if (lowerLeftStr.startsWith(":dn:")) {
+                dnAttributes = true;
+
+                if ((startIndex + 4) < (equalIndex - 1)) {
+                    matchingRule = string.substring(startIndex + 4, equalIndex - 1);
+                }
+            } else {
+                matchingRule = string.substring(startIndex + 1, equalIndex - 1);
+            }
+        } else {
+            final int colonPos = string.indexOf(':', startIndex);
+            if (colonPos < 0) {
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_EXTENSIBLE_MATCH_NO_COLON.get(string, startIndex);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            attributeDescription = string.substring(startIndex, colonPos);
+
+            // If there is anything left, then it should be ":dn" and/or ":"
+            // followed by the matching rule ID.
+            if (colonPos < (equalIndex - 1)) {
+                if (lowerLeftStr.startsWith(":dn:", colonPos - startIndex)) {
+                    dnAttributes = true;
+
+                    if ((colonPos + 4) < (equalIndex - 1)) {
+                        matchingRule = string.substring(colonPos + 4, equalIndex - 1);
+                    }
+                } else {
+                    matchingRule = string.substring(colonPos + 1, equalIndex - 1);
+                }
+            }
+        }
+
+        // Parse out the attribute value.
+        final ByteString matchValue = valueOfAssertionValue(string, equalIndex + 1, endIndex);
+
+        // Make sure that the filter has at least one of an attribute
+        // description and/or a matching rule ID.
+        if (attributeDescription == null && matchingRule == null) {
+            final LocalizableMessage message =
+                    ERR_LDAP_FILTER_EXTENSIBLE_MATCH_NO_AD_OR_MR.get(string, startIndex);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        return new Filter(new ExtensibleMatchImpl(matchingRule, attributeDescription, matchValue,
+                dnAttributes));
+    }
+
+    private static List<Filter> valueOfFilterList(final String string, final int startIndex,
+            final int endIndex) {
+        // If the end index is equal to the start index, then there are no
+        // components.
+        if (startIndex >= endIndex) {
+            return Collections.emptyList();
+        }
+
+        // At least one sub-filter.
+        Filter firstFilter = null;
+        List<Filter> subFilters = null;
+
+        // The first and last characters must be parentheses. If not, then
+        // that's an error.
+        if ((string.charAt(startIndex) != '(') || (string.charAt(endIndex - 1) != ')')) {
+            final LocalizableMessage message =
+                    ERR_LDAP_FILTER_COMPOUND_MISSING_PARENTHESES.get(string, startIndex, endIndex);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // Iterate through the characters in the value. Whenever an open
+        // parenthesis is found, locate the corresponding close parenthesis
+        // by counting the number of intermediate open/close parentheses.
+        int pendingOpens = 0;
+        int openIndex = -1;
+        for (int i = startIndex; i < endIndex; i++) {
+            final char c = string.charAt(i);
+            if (c == '(') {
+                if (openIndex < 0) {
+                    openIndex = i;
+                }
+                pendingOpens++;
+            } else if (c == ')') {
+                pendingOpens--;
+                if (pendingOpens == 0) {
+                    final Filter subFilter = valueOf0(string, openIndex + 1, i);
+                    if (subFilters != null) {
+                        subFilters.add(subFilter);
+                    } else if (firstFilter != null) {
+                        subFilters = new LinkedList<>();
+                        subFilters.add(firstFilter);
+                        subFilters.add(subFilter);
+                        firstFilter = null;
+                    } else {
+                        firstFilter = subFilter;
+                    }
+                    openIndex = -1;
+                } else if (pendingOpens < 0) {
+                    final LocalizableMessage message =
+                            ERR_LDAP_FILTER_NO_CORRESPONDING_OPEN_PARENTHESIS.get(string, i);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+            } else if (pendingOpens <= 0) {
+                final LocalizableMessage message =
+                        ERR_LDAP_FILTER_COMPOUND_MISSING_PARENTHESES.get(string, startIndex,
+                                endIndex);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        // At this point, we have parsed the entire set of filter
+        // components. The list of open parenthesis positions must be empty.
+        if (pendingOpens != 0) {
+            final LocalizableMessage message =
+                    ERR_LDAP_FILTER_NO_CORRESPONDING_CLOSE_PARENTHESIS.get(string, openIndex);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        if (subFilters != null) {
+            return Collections.unmodifiableList(subFilters);
+        } else {
+            return Collections.singletonList(firstFilter);
+        }
+    }
+
+    private static Filter valueOfGenericFilter(final String string,
+            final String attributeDescription, final int startIndex, final int endIndex) {
+        final int asteriskIdx = string.indexOf('*', startIndex);
+        if (startIndex >= endIndex) {
+            // Equality filter with empty assertion value.
+            return new Filter(new EqualityMatchImpl(attributeDescription, ByteString.empty()));
+        } else if ((endIndex - startIndex == 1) && (string.charAt(startIndex) == '*')) {
+            // Single asterisk is a present filter.
+            return present(attributeDescription);
+        } else if (asteriskIdx > 0 && asteriskIdx <= endIndex) {
+            // Substring filter.
+            return assertionValue2SubstringFilter(string, attributeDescription, startIndex,
+                    endIndex);
+        } else {
+            // equality filter.
+            final ByteString assertionValue = valueOfAssertionValue(string, startIndex, endIndex);
+            return new Filter(new EqualityMatchImpl(attributeDescription, assertionValue));
+        }
+    }
+
+    /**
+     * Appends a properly-cleaned version of the provided value to the given
+     * builder so that it can be safely used in string representations of this
+     * search filter. The formatting changes that may be performed will be in
+     * compliance with the specification in RFC 2254.
+     *
+     * @param builder
+     *            The builder to which the "safe" version of the value will be
+     *            appended.
+     * @param value
+     *            The value to be appended to the builder.
+     */
+    private static void valueToFilterString(final StringBuilder builder, final ByteString value) {
+        // Get the binary representation of the value and iterate through
+        // it to see if there are any unsafe characters. If there are,
+        // then escape them and replace them with a two-digit hex
+        // equivalent.
+        builder.ensureCapacity(builder.length() + value.length());
+        for (int i = 0; i < value.length(); i++) {
+            // TODO: this is a bit overkill - it will escape all non-ascii
+            // chars!
+            final byte b = value.byteAt(i);
+            if (((b & 0x7F) != b) // Not 7-bit clean
+                    || b <= 0x1F  // Below the printable character range
+                    || b == 0x28  // Open parenthesis
+                    || b == 0x29  // Close parenthesis
+                    || b == ASTERISK
+                    || b == BACKSLASH
+                    || b == 0x7F  /* Delete character */) {
+                builder.append('\\');
+                builder.append(byteToHex(b));
+            } else {
+                builder.append((char) b);
+            }
+        }
+    }
+
+    private final Impl pimpl;
+
+    private Filter(final Impl pimpl) {
+        this.pimpl = pimpl;
+    }
+
+    /**
+     * Applies a {@code FilterVisitor} to this {@code Filter}.
+     *
+     * @param <R>
+     *            The return type of the visitor's methods.
+     * @param <P>
+     *            The type of the additional parameters to the visitor's
+     *            methods.
+     * @param v
+     *            The filter visitor.
+     * @param p
+     *            Optional additional visitor parameter.
+     * @return A result as specified by the visitor.
+     */
+    public <R, P> R accept(final FilterVisitor<R, P> v, final P p) {
+        return pimpl.accept(v, p);
+    }
+
+    /**
+     * Returns a {@code Matcher} which can be used to compare this
+     * {@code Filter} against entries using the default schema.
+     *
+     * @return The {@code Matcher}.
+     */
+    public Matcher matcher() {
+        return new Matcher(this, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns a {@code Matcher} which can be used to compare this
+     * {@code Filter} against entries using the provided {@code Schema}.
+     *
+     * @param schema
+     *            The schema which the {@code Matcher} should use for
+     *            comparisons.
+     * @return The {@code Matcher}.
+     */
+    public Matcher matcher(final Schema schema) {
+        return new Matcher(this, schema);
+    }
+
+    /**
+     * Indicates whether this {@code Filter} matches the provided {@code Entry}
+     * using the default schema.
+     * <p>
+     * Calling this method is equivalent to the following:
+     *
+     * <pre>
+     * matcher().matches(entry);
+     * </pre>
+     *
+     * @param entry
+     *            The entry to be matched.
+     * @return The result of matching the provided {@code Entry} against this
+     *         {@code Filter} using the default schema.
+     */
+    public ConditionResult matches(final Entry entry) {
+        return matcher(Schema.getDefaultSchema()).matches(entry);
+    }
+
+    /**
+     * Returns a {@code String} whose contents is the LDAP string representation
+     * of this {@code Filter}.
+     *
+     * @return The LDAP string representation of this {@code Filter}.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        return pimpl.accept(TO_STRING_VISITOR, builder).toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/FilterVisitor.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/FilterVisitor.java
new file mode 100644
index 0000000..939506d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/FilterVisitor.java
@@ -0,0 +1,192 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.List;
+
+/**
+ * A visitor of {@code Filter}s, in the style of the visitor design pattern.
+ * <p>
+ * Classes implementing this interface can query filters in a type-safe manner.
+ * When a visitor is passed to a filter's accept method, the corresponding visit
+ * method most applicable to that filter is invoked.
+ *
+ * @param <R>
+ *            The return type of this visitor's methods. Use
+ *            {@link java.lang.Void} for visitors that do not need to return
+ *            results.
+ * @param <P>
+ *            The type of the additional parameter to this visitor's methods.
+ *            Use {@link java.lang.Void} for visitors that do not need an
+ *            additional parameter.
+ */
+public interface FilterVisitor<R, P> {
+
+    /**
+     * Visits an {@code and} filter.
+     * <p>
+     * <b>Implementation note</b>: for the purposes of matching an empty
+     * sub-filter list should always evaluate to {@code true} as per RFC 4526.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param subFilters
+     *            The unmodifiable list of sub-filters.
+     * @return Returns a visitor specified result.
+     */
+    R visitAndFilter(P p, List<Filter> subFilters);
+
+    /**
+     * Visits an {@code approximate match} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return Returns a visitor specified result.
+     */
+    R visitApproxMatchFilter(P p, String attributeDescription, ByteString assertionValue);
+
+    /**
+     * Visits an {@code equality match} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return Returns a visitor specified result.
+     */
+    R visitEqualityMatchFilter(P p, String attributeDescription, ByteString assertionValue);
+
+    /**
+     * Visits an {@code extensible} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param matchingRule
+     *            The matching rule name, may be {@code null} if
+     *            {@code attributeDescription} is specified.
+     * @param attributeDescription
+     *            The attribute description, may be {@code null} if
+     *            {@code matchingRule} is specified.
+     * @param assertionValue
+     *            The assertion value.
+     * @param dnAttributes
+     *            Indicates whether DN matching should be performed.
+     * @return Returns a visitor specified result.
+     */
+    R visitExtensibleMatchFilter(P p, String matchingRule, String attributeDescription,
+            ByteString assertionValue, boolean dnAttributes);
+
+    /**
+     * Visits a {@code greater or equal} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return Returns a visitor specified result.
+     */
+    R visitGreaterOrEqualFilter(P p, String attributeDescription, ByteString assertionValue);
+
+    /**
+     * Visits a {@code less or equal} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @param assertionValue
+     *            The assertion value.
+     * @return Returns a visitor specified result.
+     */
+    R visitLessOrEqualFilter(P p, String attributeDescription, ByteString assertionValue);
+
+    /**
+     * Visits a {@code not} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param subFilter
+     *            The sub-filter.
+     * @return Returns a visitor specified result.
+     */
+    R visitNotFilter(P p, Filter subFilter);
+
+    /**
+     * Visits an {@code or} filter.
+     * <p>
+     * <b>Implementation note</b>: for the purposes of matching an empty
+     * sub-filter list should always evaluate to {@code false} as per RFC 4526.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param subFilters
+     *            The unmodifiable list of sub-filters.
+     * @return Returns a visitor specified result.
+     */
+    R visitOrFilter(P p, List<Filter> subFilters);
+
+    /**
+     * Visits a {@code present} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @return Returns a visitor specified result.
+     */
+    R visitPresentFilter(P p, String attributeDescription);
+
+    /**
+     * Visits a {@code substrings} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param attributeDescription
+     *            The attribute description.
+     * @param initialSubstring
+     *            The initial sub-string, may be {@code null}.
+     * @param anySubstrings
+     *            The unmodifiable list of any sub-strings, may be empty.
+     * @param finalSubstring
+     *            The final sub-string, may be {@code null}.
+     * @return Returns a visitor specified result.
+     */
+    R visitSubstringsFilter(P p, String attributeDescription, ByteString initialSubstring,
+            List<ByteString> anySubstrings, ByteString finalSubstring);
+
+    /**
+     * Visits an {@code unrecognized} filter.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param filterTag
+     *            The ASN.1 tag.
+     * @param filterBytes
+     *            The filter content.
+     * @return Returns a visitor specified result.
+     */
+    R visitUnrecognizedFilter(P p, byte filterTag, ByteString filterBytes);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Functions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Functions.java
new file mode 100644
index 0000000..6c5465e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Functions.java
@@ -0,0 +1,438 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * Common {@link Function} implementations which may be used when parsing
+ * attributes.
+ *
+ * @see Entry#parseAttribute
+ * @see Attribute#parse
+ * @see AttributeParser
+ */
+public final class Functions {
+
+    private static final Function<ByteString, String, NeverThrowsException> BYTESTRING_TO_STRING =
+            new Function<ByteString, String, NeverThrowsException>() {
+                @Override
+                public String apply(final ByteString value) {
+                    return value.toString();
+                }
+            };
+
+    private static final Function<Object, Object, NeverThrowsException> IDENTITY =
+            new Function<Object, Object, NeverThrowsException>() {
+                @Override
+                public Object apply(final Object value) {
+                    return value;
+                }
+            };
+
+    private static final Function<String, String, NeverThrowsException> NORMALIZE_STRING =
+            new Function<String, String, NeverThrowsException>() {
+                @Override
+                public String apply(final String value) {
+                    return StaticUtils.toLowerCase(value).trim();
+                }
+            };
+
+    private static final Function<Object, ByteString, NeverThrowsException> OBJECT_TO_BYTESTRING =
+            new Function<Object, ByteString, NeverThrowsException>() {
+                @Override
+                public ByteString apply(final Object value) {
+                    return ByteString.valueOfObject(value);
+                }
+            };
+
+    private static final Function<String, Boolean, NeverThrowsException> STRING_TO_BOOLEAN =
+            new Function<String, Boolean, NeverThrowsException>() {
+                @Override
+                public Boolean apply(final String value) {
+                    final String valueString = StaticUtils.toLowerCase(value);
+                    if ("true".equals(valueString) || "yes".equals(valueString)
+                            || "on".equals(valueString) || "1".equals(valueString)) {
+                        return Boolean.TRUE;
+                    } else if ("false".equals(valueString) || "no".equals(valueString)
+                            || "off".equals(valueString) || "0".equals(valueString)) {
+                        return Boolean.FALSE;
+                    } else {
+                        throw new LocalizedIllegalArgumentException(
+                                WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN.get(valueString));
+                    }
+                }
+            };
+
+    private static final Function<String, GeneralizedTime, NeverThrowsException> STRING_TO_GENERALIZED_TIME =
+            new Function<String, GeneralizedTime, NeverThrowsException>() {
+                @Override
+                public GeneralizedTime apply(final String value) {
+                    return GeneralizedTime.valueOf(value);
+                }
+            };
+
+    private static final Function<String, Integer, NeverThrowsException> STRING_TO_INTEGER =
+            new Function<String, Integer, NeverThrowsException>() {
+                @Override
+                public Integer apply(final String value) {
+                    try {
+                        return Integer.valueOf(value);
+                    } catch (final NumberFormatException e) {
+                        final LocalizableMessage message = FUNCTIONS_TO_INTEGER_FAIL.get(value);
+                        throw new LocalizedIllegalArgumentException(message);
+                    }
+                }
+            };
+
+    private static final Function<String, Long, NeverThrowsException> STRING_TO_LONG =
+            new Function<String, Long, NeverThrowsException>() {
+                @Override
+                public Long apply(final String value) {
+                    try {
+                        return Long.valueOf(value);
+                    } catch (final NumberFormatException e) {
+                        final LocalizableMessage message = FUNCTIONS_TO_LONG_FAIL.get(value);
+                        throw new LocalizedIllegalArgumentException(message);
+                    }
+                }
+            };
+
+    private static final Function<ByteString, Boolean, NeverThrowsException> BYTESTRING_TO_BOOLEAN = compose(
+            byteStringToString(), STRING_TO_BOOLEAN);
+
+    private static final Function<ByteString, GeneralizedTime, NeverThrowsException> BYTESTRING_TO_GENERALIZED_TIME =
+            compose(byteStringToString(), STRING_TO_GENERALIZED_TIME);
+
+    private static final Function<ByteString, Integer, NeverThrowsException> BYTESTRING_TO_INTEGER = compose(
+            byteStringToString(), STRING_TO_INTEGER);
+
+    private static final Function<ByteString, Long, NeverThrowsException> BYTESTRING_TO_LONG = compose(
+            byteStringToString(), STRING_TO_LONG);
+
+    /**
+     * Creates a function that returns constant value for any input.
+     *
+     * @param <M>
+     *            The type of input values transformed by this function.
+     * @param <N>
+     *            The type of output values returned by this function.
+     * @param constant
+     *            The constant value for the function to return
+     * @return A function that always returns constant value.
+     */
+    public static <M, N> Function<M, N, NeverThrowsException> returns(final N constant) {
+        return new Function<M, N, NeverThrowsException>() {
+            @Override
+            public N apply(M value) {
+                return constant;
+            }
+        };
+    }
+
+    /**
+     * Returns the composition of two functions. The result of the first
+     * function will be passed to the second.
+     *
+     * @param <M>
+     *            The type of input values transformed by this function.
+     * @param <N>
+     *            The type of output values returned by this function.
+     * @param <X>
+     *            The type of intermediate values passed between the two
+     *            functions.
+     * @param first
+     *            The first function which will consume the input.
+     * @param second
+     *            The second function which will produce the result.
+     * @return The composition.
+     */
+    public static <M, X, N> Function<M, N, NeverThrowsException> compose(
+            final Function<M, X, NeverThrowsException> first, final Function<X, N, NeverThrowsException> second) {
+        return new Function<M, N, NeverThrowsException>() {
+            @Override
+            public N apply(final M value) {
+                return second.apply(first.apply(value));
+            }
+        };
+    }
+
+    /**
+     * Returns a function which always returns the value that it was provided
+     * with.
+     *
+     * @param <M>
+     *            The type of values transformed by this function.
+     * @return A function which always returns the value that it was provided
+     *         with.
+     */
+    @SuppressWarnings("unchecked")
+    public static <M> Function<M, M, NeverThrowsException> identityFunction() {
+        return (Function<M, M, NeverThrowsException>) IDENTITY;
+    }
+
+    /**
+     * Returns a function which converts a {@code String} to lower case using
+     * {@link StaticUtils#toLowerCase} and then trims it.
+     *
+     * @return A function which converts a {@code String} to lower case using
+     *         {@link StaticUtils#toLowerCase} and then trims it.
+     */
+    public static Function<String, String, NeverThrowsException> normalizeString() {
+        return NORMALIZE_STRING;
+    }
+
+    /**
+     * Returns a function which converts an {@code Object} to a
+     * {@code ByteString} using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @return A function which converts an {@code Object} to a
+     *         {@code ByteString} .
+     */
+    public static Function<Object, ByteString, NeverThrowsException> objectToByteString() {
+        return OBJECT_TO_BYTESTRING;
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * default schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<String, AttributeDescription, NeverThrowsException> stringToAttributeDescription() {
+        return stringToAttributeDescription(getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * provided schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @param schema
+     *            The schema to use for decoding attribute descriptions.
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<String, AttributeDescription, NeverThrowsException> stringToAttributeDescription(
+            final Schema schema) {
+        return new Function<String, AttributeDescription, NeverThrowsException>() {
+            @Override
+            public AttributeDescription apply(final String value) {
+                return AttributeDescription.valueOf(value, schema);
+            }
+        };
+    }
+
+    /**
+     * Returns a function which parses {@code Boolean} values. The function will
+     * accept the values {@code 0}, {@code false}, {@code no}, {@code off},
+     * {@code 1}, {@code true}, {@code yes}, {@code on}. All other values will
+     * result in a {@code NumberFormatException}.
+     *
+     * @return A function which parses {@code Boolean} values.
+     */
+    public static Function<String, Boolean, NeverThrowsException> stringToBoolean() {
+        return STRING_TO_BOOLEAN;
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the default schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<String, DN, NeverThrowsException> stringToDN() {
+        return stringToDN(getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the provided schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @param schema
+     *            The schema to use for decoding DNs.
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<String, DN, NeverThrowsException> stringToDN(final Schema schema) {
+        return new Function<String, DN, NeverThrowsException>() {
+            @Override
+            public DN apply(final String value) {
+                return DN.valueOf(value, schema);
+            }
+        };
+    }
+
+    /**
+     * Returns a function which parses generalized time strings. Invalid values
+     * will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses generalized time strings.
+     */
+    public static Function<String, GeneralizedTime, NeverThrowsException> stringToGeneralizedTime() {
+        return STRING_TO_GENERALIZED_TIME;
+    }
+
+    /**
+     * Returns a function which parses {@code Integer} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Integer} string values.
+     */
+    public static Function<String, Integer, NeverThrowsException> stringToInteger() {
+        return STRING_TO_INTEGER;
+    }
+
+    /**
+     * Returns a function which parses {@code Long} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Long} string values.
+     */
+    public static Function<String, Long, NeverThrowsException> stringToLong() {
+        return STRING_TO_LONG;
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * default schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<ByteString, AttributeDescription, NeverThrowsException> byteStringToAttributeDescription() {
+        return byteStringToAttributeDescription(getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * provided schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @param schema
+     *            The schema to use for decoding attribute descriptions.
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<ByteString, AttributeDescription, NeverThrowsException> byteStringToAttributeDescription(
+            final Schema schema) {
+        return compose(byteStringToString(), new Function<String, AttributeDescription, NeverThrowsException>() {
+            @Override
+            public AttributeDescription apply(final String value) {
+                return AttributeDescription.valueOf(value, schema);
+            }
+        });
+    }
+
+    /**
+     * Returns a function which parses {@code Boolean} values. The function will
+     * accept the values {@code 0}, {@code false}, {@code no}, {@code off},
+     * {@code 1}, {@code true}, {@code yes}, {@code on}. All other values will
+     * result in a {@code NumberFormatException}.
+     *
+     * @return A function which parses {@code Boolean} values.
+     */
+    public static Function<ByteString, Boolean, NeverThrowsException> byteStringToBoolean() {
+        return BYTESTRING_TO_BOOLEAN;
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the default schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<ByteString, DN, NeverThrowsException> byteStringToDN() {
+        return byteStringToDN(getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the provided schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @param schema
+     *            The schema to use for decoding DNs.
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<ByteString, DN, NeverThrowsException> byteStringToDN(final Schema schema) {
+        return compose(byteStringToString(), new Function<String, DN, NeverThrowsException>() {
+            @Override
+            public DN apply(final String value) {
+                return DN.valueOf(value, schema);
+            }
+        });
+    }
+
+    /**
+     * Returns a function which parses generalized time strings. Invalid values
+     * will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses generalized time strings.
+     */
+    public static Function<ByteString, GeneralizedTime, NeverThrowsException> byteStringToGeneralizedTime() {
+        return BYTESTRING_TO_GENERALIZED_TIME;
+    }
+
+    /**
+     * Returns a function which parses {@code Integer} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Integer} string values.
+     */
+    public static Function<ByteString, Integer, NeverThrowsException> byteStringToInteger() {
+        return BYTESTRING_TO_INTEGER;
+    }
+
+    /**
+     * Returns a function which parses {@code Long} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Long} string values.
+     */
+    public static Function<ByteString, Long, NeverThrowsException> byteStringToLong() {
+        return BYTESTRING_TO_LONG;
+    }
+
+    /**
+     * Returns a function which parses a {@code ByteString} as a UTF-8 encoded
+     * {@code String}.
+     *
+     * @return A function which parses the string representation of a
+     *         {@code ByteString} as a UTF-8 encoded {@code String}.
+     */
+    public static Function<ByteString, String, NeverThrowsException> byteStringToString() {
+        return BYTESTRING_TO_STRING;
+    }
+
+    /** Prevent instantiation. */
+    private Functions() {
+        // Do nothing.
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GSERParser.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GSERParser.java
new file mode 100644
index 0000000..0643b5d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GSERParser.java
@@ -0,0 +1,396 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.ldap;
+
+import java.math.BigInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.forgerock.util.Reject;
+import org.forgerock.i18n.LocalizableMessage;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_NO_VALID_IDENTIFIEDCHOICE;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_NO_VALID_IDENTIFIER;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_NO_VALID_INTEGER;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_NO_VALID_SEPARATOR;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_NO_VALID_STRING;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_PATTERN_NO_MATCH;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_GSER_SPACE_CHAR_EXPECTED;
+
+/**
+ * This class implements a parser for strings which are encoded using the
+ * Generic String Encoding Rules (GSER) defined in RFC 3641.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3641">RFC 3641 - Generic String
+ * Encoding Rules (GSER) for ASN.1 Types</a>
+ */
+public final class GSERParser {
+
+    private final String gserValue;
+
+    private int pos;
+
+    private final int length;
+
+    /**
+     * Pattern to match an identifier defined in RFC 3641, section 3.4.
+     * <pre>
+     * An &lt;identifier&gt; conforms to the definition of an identifier in ASN.1
+     * notation (Clause 11.3 of X.680 [8]).  It begins with a lowercase
+     * letter and is followed by zero or more letters, digits, and hyphens.
+     * A hyphen is not permitted to be the last character, nor is it to be
+     * followed by another hyphen.  The case of letters in an identifier is
+     * always significant.
+     *
+     *    identifier    = lowercase *alphanumeric *(hyphen 1*alphanumeric)
+     *    alphanumeric  = uppercase / lowercase / decimal-digit
+     *    uppercase     = %x41-5A  ; "A" to "Z"
+     *    lowercase     = %x61-7A  ; "a" to "z"
+     *    decimal-digit = %x30-39  ; "0" to "9"
+     *    hyphen        = "-"
+     * </pre>
+     */
+    private static final Pattern GSER_IDENTIFIER = Pattern.compile("^([a-z]([A-Za-z0-9]|(-[A-Za-z0-9]))*)");
+
+    /**
+     * Pattern to match the identifier part (including the colon) of an
+     * IdentifiedChoiceValue defined in RFC 3641, section 3.12.
+     * <pre>
+     *    IdentifiedChoiceValue = identifier ":" Value
+     * </pre>
+     */
+    private static final Pattern GSER_CHOICE_IDENTIFIER = Pattern.compile("^([a-z]([A-Za-z0-9]|(-[A-Za-z0-9]))*:)");
+
+    /**
+     * Pattern to match "sp", containing zero, one or more space characters.
+     * <pre>
+     *    sp = *%x20  ; zero, one or more space characters
+     * </pre>
+     */
+    private static final Pattern GSER_SP = Pattern.compile("^( *)");
+
+    /**
+     * Pattern to match "msp", containing at least one space character.
+     * <pre>
+     *    msp = 1*%x20  ; one or more space characters
+     * </pre>
+     */
+    private static final Pattern GSER_MSP = Pattern.compile("^( +)");
+
+    /**
+     * Pattern to match an Integer value.
+     */
+    private static final Pattern GSER_INTEGER = Pattern.compile("^(\\d+)");
+
+    /**
+     * Pattern to match a GSER StringValue, defined in RFC 3641, section 3.2:
+     * <pre>
+     * Any embedded double quotes in the resulting UTF-8 character string
+     * are escaped by repeating the double quote characters.
+     *
+     * [...]
+     *
+     *    StringValue       = dquote *SafeUTF8Character dquote
+     *    dquote            = %x22 ; &quot; (double quote)
+     * </pre>
+     */
+    private static final Pattern GSER_STRING = Pattern.compile("^(\"([^\"]|(\"\"))*\")");
+
+    /**
+     * Pattern to match the beginning of a GSER encoded Sequence.
+     * <pre>
+     *    SequenceValue = ComponentList
+     *    ComponentList = "{" [ sp NamedValue *( "," sp NamedValue) ] sp "}"
+     * </pre>
+     */
+    private static final Pattern GSER_SEQUENCE_START = Pattern.compile("^(\\{)");
+
+    /**
+     * Pattern to match the end of a GSER encoded Sequence.
+     * <pre>
+     *    SequenceValue = ComponentList
+     *    ComponentList = "{" [ sp NamedValue *( "," sp NamedValue) ] sp "}"
+     * </pre>
+     */
+    private static final Pattern GSER_SEQUENCE_END = Pattern.compile("^(\\})");
+
+    /**
+     * Pattern to match the separator used in GSER encoded sequences.
+     */
+    private static final Pattern GSER_SEP = Pattern.compile("^(,)");
+
+    /**
+     * Creates a new GSER Parser.
+     *
+     * @param value the GSER encoded String value
+     */
+    public GSERParser(CharSequence value) {
+        Reject.checkNotNull(value);
+        this.gserValue = value.toString();
+        this.pos = 0;
+        this.length = value.length();
+    }
+
+    /**
+     * Determines if the GSER String contains at least one character to be read.
+     *
+     * @return <code>true</code> if there is at least one remaining character or
+     * <code>false</code> otherwise.
+     */
+    public boolean hasNext() {
+        return pos < length;
+    }
+
+    /**
+     * Determines if the remaining GSER String matches the provided pattern.
+     *
+     * @param pattern the pattern to search for
+     *
+     * @return <code>true</code> if the remaining string matches the pattern or
+     * <code>false</code> otherwise.
+     */
+    private boolean hasNext(Pattern pattern) {
+        if (!hasNext()) {
+            return false;
+        }
+
+        Matcher matcher = pattern.matcher(gserValue.substring(pos, length));
+
+        return matcher.find();
+    }
+
+    /**
+     * Returns the String matched by the first capturing group of the pattern.
+     * The parser advances past the input matched by the first capturing group.
+     *
+     * @param pattern the pattern to search for
+     *
+     * @return the String matched by the first capturing group of the pattern
+     *
+     * @throws DecodeException If no match could be found
+     */
+    private String next(Pattern pattern) throws DecodeException {
+        Matcher matcher = pattern.matcher(gserValue.substring(pos, length));
+        if (matcher.find() && matcher.groupCount() >= 1) {
+            pos += matcher.end(1);
+            return matcher.group(1);
+        } else {
+            final LocalizableMessage msg =
+                    WARN_GSER_PATTERN_NO_MATCH.get(pattern.pattern(),
+                                                   gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+    }
+
+    /**
+     * Skips the input matched by the first capturing group.
+     *
+     * @param pattern the pattern to search for
+     *
+     * @throws DecodeException If no match could be found
+     */
+    private void skip(Pattern pattern) throws DecodeException {
+        Matcher matcher = pattern.matcher(gserValue.substring(pos, length));
+
+        if (matcher.find() && matcher.groupCount() >= 1) {
+            pos += matcher.end(1);
+        } else {
+            final LocalizableMessage msg =
+                    WARN_GSER_PATTERN_NO_MATCH.get(pattern.pattern(),
+                                                   gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+    }
+
+    /**
+     * Skips the input matching zero, one or more space characters.
+     *
+     * @return reference to this GSERParser
+     *
+     * @throws DecodeException If no match could be found
+     */
+    public GSERParser skipSP() throws DecodeException {
+        skip(GSER_SP);
+        return this;
+    }
+
+    /**
+     * Skips the input matching one or more space characters.
+     *
+     * @return reference to this GSERParser
+     *
+     * @throws DecodeException If no match could be found
+     */
+    public GSERParser skipMSP() throws DecodeException {
+        skip(GSER_MSP);
+        return this;
+    }
+
+    /**
+     * Skips the input matching the start of a sequence and subsequent space
+     * characters.
+     *
+     * @return reference to this GSERParser
+     *
+     * @throws DecodeException If the input does not match the start of a
+     * sequence
+     */
+    public GSERParser readStartSequence() throws DecodeException {
+        next(GSER_SEQUENCE_START);
+        skip(GSER_SP);
+        return this;
+    }
+
+    /**
+     * Skips the input matching the end of a sequence and preceding space
+     * characters.
+     *
+     * @return reference to this GSERParser
+     *
+     * @throws DecodeException If the input does not match the end of a sequence
+     */
+    public GSERParser readEndSequence() throws DecodeException {
+        skip(GSER_SP);
+        next(GSER_SEQUENCE_END);
+        return this;
+    }
+
+    /**
+     * Skips the input matching the separator pattern (",") and subsequenct
+     * space characters.
+     *
+     * @return reference to this GSERParser
+     *
+     * @throws DecodeException If the input does not match the separator
+     * pattern.
+     */
+    public GSERParser skipSeparator() throws DecodeException {
+        if (!hasNext(GSER_SEP)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_SEPARATOR.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        skip(GSER_SEP);
+        skip(GSER_SP);
+        return this;
+    }
+
+    /**
+     * Returns the next element as a String.
+     *
+     * @return the input matching the String pattern
+     *
+     * @throws DecodeException If the input does not match the string pattern.
+     */
+    public String nextString() throws DecodeException {
+        if (!hasNext(GSER_STRING)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_STRING.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+
+        String str = next(GSER_STRING);
+
+        // Strip leading and trailing dquotes; unescape double dquotes
+        return str.substring(1, str.length() - 1).replace("\"\"", "\"");
+    }
+
+    /**
+     * Returns the next element as an Integer.
+     *
+     * @return the input matching the integer pattern
+     *
+     * @throws DecodeException If the input does not match the integer pattern
+     */
+    public int nextInteger() throws DecodeException {
+        if (!hasNext(GSER_INTEGER)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_INTEGER.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        return Integer.valueOf(next(GSER_INTEGER)).intValue();
+    }
+
+    /**
+     * Returns the next element as a BigInteger.
+     *
+     * @return the input matching the integer pattern
+     *
+     * @throws DecodeException If the input does not match the integer pattern
+     */
+    public BigInteger nextBigInteger() throws DecodeException {
+        if (!hasNext(GSER_INTEGER)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_INTEGER.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        return new BigInteger(next(GSER_INTEGER));
+    }
+
+    /**
+     * Returns the identifier of the next NamedValue element.
+     *
+     * @return the identifier of the NamedValue element
+     *
+     * @throws DecodeException If the input does not match the identifier
+     * pattern of a NamedValue
+     */
+    public String nextNamedValueIdentifier() throws DecodeException {
+        if (!hasNext(GSER_IDENTIFIER)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_IDENTIFIER.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        String identifier = next(GSER_IDENTIFIER);
+        if (!hasNext(GSER_MSP)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_SPACE_CHAR_EXPECTED.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        skipMSP();
+        return identifier;
+    }
+
+    /**
+     * Return the identifier of the next IdentifiedChoiceValue element.
+     *
+     * @return the identifier of the IdentifiedChoiceValue element
+     *
+     * @throws DecodeException If the input does not match the identifier
+     * pattern of an IdentifiedChoiceValue
+     */
+    public String nextChoiceValueIdentifier() throws DecodeException {
+        if (!hasNext(GSER_CHOICE_IDENTIFIER)) {
+            final LocalizableMessage msg =
+                    WARN_GSER_NO_VALID_IDENTIFIEDCHOICE.get(gserValue.substring(pos, length));
+            throw DecodeException.error(msg);
+        }
+        String identifier = next(GSER_CHOICE_IDENTIFIER);
+
+        // Remove the colon at the end of the identifier
+        return identifier.substring(0, identifier.length() - 1);
+    }
+
+    /**
+     * Returns the GSER encoded String value.
+     *
+     * @return The GSER encoded String value.
+     */
+    @Override
+    public String toString() {
+        return gserValue;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java
new file mode 100644
index 0000000..66218be
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java
@@ -0,0 +1,992 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.util.Reject;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * An LDAP generalized time as defined in RFC 4517. This class facilitates
+ * parsing of generalized time values to and from {@link Date} and
+ * {@link Calendar} classes.
+ * <p>
+ * The following are examples of generalized time values:
+ *
+ * <pre>
+ * 199412161032Z
+ * 199412160532-0500
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4517#section-3.3.13">RFC 4517 -
+ *      Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching
+ *      Rules </a>
+ */
+public final class GeneralizedTime implements Comparable<GeneralizedTime> {
+    /** UTC TimeZone is assumed to never change over JVM lifetime. */
+    private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone("UTC");
+
+    /** The smallest time representable using the generalized time syntax. */
+    public static final GeneralizedTime MIN_GENERALIZED_TIME = valueOf("00010101000000Z");
+
+    /** The smallest time in milli-seconds representable using the generalized time syntax. */
+    public static final long MIN_GENERALIZED_TIME_MS = MIN_GENERALIZED_TIME.getTimeInMillis();
+
+    /**
+     * Returns a generalized time whose value is the current time, using the
+     * default time zone and locale.
+     *
+     * @return A generalized time whose value is the current time.
+     */
+    public static GeneralizedTime currentTime() {
+        return valueOf(Calendar.getInstance());
+    }
+
+    /**
+     * Returns a generalized time representing the provided {@code Calendar}.
+     * <p>
+     * The provided calendar will be defensively copied in order to preserve
+     * immutability.
+     *
+     * @param calendar
+     *            The calendar to be converted to a generalized time.
+     * @return A generalized time representing the provided {@code Calendar}.
+     */
+    public static GeneralizedTime valueOf(final Calendar calendar) {
+        Reject.ifNull(calendar);
+        return new GeneralizedTime((Calendar) calendar.clone(), null, Long.MIN_VALUE, null);
+    }
+
+    /**
+     * Returns a generalized time representing the provided {@code Date}.
+     * <p>
+     * The provided date will be defensively copied in order to preserve
+     * immutability.
+     *
+     * @param date
+     *            The date to be converted to a generalized time.
+     * @return A generalized time representing the provided {@code Date}.
+     */
+    public static GeneralizedTime valueOf(final Date date) {
+        Reject.ifNull(date);
+        return new GeneralizedTime(null, (Date) date.clone(), Long.MIN_VALUE, null);
+    }
+
+    /**
+     * Returns a generalized time representing the provided time in milliseconds
+     * since the epoch.
+     *
+     * @param timeMS
+     *            The time to be converted to a generalized time.
+     * @return A generalized time representing the provided time in milliseconds
+     *         since the epoch.
+     */
+    public static GeneralizedTime valueOf(final long timeMS) {
+        Reject.ifTrue(timeMS < MIN_GENERALIZED_TIME_MS, "timeMS is too old to represent as a generalized time");
+        return new GeneralizedTime(null, null, timeMS, null);
+    }
+
+    /**
+     * Parses the provided string as an LDAP generalized time.
+     *
+     * @param time
+     *            The generalized time value to be parsed.
+     * @return The parsed generalized time.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code time} cannot be parsed as a valid generalized time
+     *             string.
+     * @throws NullPointerException
+     *             If {@code time} was {@code null}.
+     */
+    public static GeneralizedTime valueOf(final String time) {
+        int year = 0;
+        int month = 0;
+        int day = 0;
+        int hour = 0;
+        int minute = 0;
+        int second = 0;
+
+        // Get the value as a string and verify that it is at least long
+        // enough for "YYYYMMDDhhZ", which is the shortest allowed value.
+        final String valueString = time.toUpperCase();
+        final int length = valueString.length();
+        if (length < 11) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // The first four characters are the century and year, and they must
+        // be numeric digits between 0 and 9.
+        for (int i = 0; i < 4; i++) {
+            char c = valueString.charAt(i);
+            final int val = toInt(c,
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR, valueString, String.valueOf(c));
+            year = (year * 10) + val;
+        }
+
+        // The next two characters are the month, and they must form the
+        // string representation of an integer between 01 and 12.
+        char m1 = valueString.charAt(4);
+        final char m2 = valueString.charAt(5);
+        final String monthValue = valueString.substring(4, 6);
+        switch (m1) {
+        case '0':
+            // m2 must be a digit between 1 and 9.
+            switch (m2) {
+            case '1':
+                month = Calendar.JANUARY;
+                break;
+
+            case '2':
+                month = Calendar.FEBRUARY;
+                break;
+
+            case '3':
+                month = Calendar.MARCH;
+                break;
+
+            case '4':
+                month = Calendar.APRIL;
+                break;
+
+            case '5':
+                month = Calendar.MAY;
+                break;
+
+            case '6':
+                month = Calendar.JUNE;
+                break;
+
+            case '7':
+                month = Calendar.JULY;
+                break;
+
+            case '8':
+                month = Calendar.AUGUST;
+                break;
+
+            case '9':
+                month = Calendar.SEPTEMBER;
+                break;
+
+            default:
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
+            }
+            break;
+        case '1':
+            // m2 must be a digit between 0 and 2.
+            switch (m2) {
+            case '0':
+                month = Calendar.OCTOBER;
+                break;
+
+            case '1':
+                month = Calendar.NOVEMBER;
+                break;
+
+            case '2':
+                month = Calendar.DECEMBER;
+                break;
+
+            default:
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
+            }
+            break;
+        default:
+            throw new LocalizedIllegalArgumentException(
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
+        }
+
+        // The next two characters should be the day of the month, and they
+        // must form the string representation of an integer between 01 and
+        // 31. This doesn't do any validation against the year or month, so
+        // it will allow dates like April 31, or February 29 in a non-leap
+        // year, but we'll let those slide.
+        final char d1 = valueString.charAt(6);
+        final char d2 = valueString.charAt(7);
+        final String dayValue = valueString.substring(6, 8);
+        switch (d1) {
+        case '0':
+            // d2 must be a digit between 1 and 9.
+            day = toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
+            if (day == 0) {
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
+            }
+            break;
+
+        case '1':
+            // d2 must be a digit between 0 and 9.
+            day = 10 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
+            break;
+
+        case '2':
+            // d2 must be a digit between 0 and 9.
+            day = 20 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
+            break;
+
+        case '3':
+            // d2 must be either 0 or 1.
+            switch (d2) {
+            case '0':
+                day = 30;
+                break;
+
+            case '1':
+                day = 31;
+                break;
+
+            default:
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
+            }
+            break;
+
+        default:
+            throw new LocalizedIllegalArgumentException(
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
+        }
+
+        // The next two characters must be the hour, and they must form the
+        // string representation of an integer between 00 and 23.
+        final char h1 = valueString.charAt(8);
+        final char h2 = valueString.charAt(9);
+        final String hourValue = valueString.substring(8, 10);
+        switch (h1) {
+        case '0':
+            hour = toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue);
+            break;
+
+        case '1':
+            hour = 10 + toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue);
+            break;
+
+        case '2':
+            switch (h2) {
+            case '0':
+                hour = 20;
+                break;
+
+            case '1':
+                hour = 21;
+                break;
+
+            case '2':
+                hour = 22;
+                break;
+
+            case '3':
+                hour = 23;
+                break;
+
+            default:
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue));
+            }
+            break;
+
+        default:
+            throw new LocalizedIllegalArgumentException(
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue));
+        }
+
+        // Next, there should be either two digits comprising an integer
+        // between 00 and 59 (for the minute), a letter 'Z' (for the UTC
+        // specifier), a plus or minus sign followed by two or four digits
+        // (for the UTC offset), or a period or comma representing the
+        // fraction.
+        m1 = valueString.charAt(10);
+        switch (m1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 13) {
+                throw invalidChar(valueString, m1, 10);
+            }
+
+            minute = 10 * (m1 - '0');
+            minute += toInt(valueString.charAt(11),
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(10, 12));
+
+            break;
+
+        case 'Z':
+        case 'z':
+                // This is fine only if we are at the end of the value.
+            if (length == 11) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, m1, 10);
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if (length == 13 || length == 15) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 10);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, m1, 10);
+            }
+
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 11, year, month, day, hour, minute, second,
+                    3600000);
+
+        default:
+            throw invalidChar(valueString, m1, 10);
+        }
+
+        // Next, there should be either two digits comprising an integer
+        // between 00 and 60 (for the second, including a possible leap
+        // second), a letter 'Z' (for the UTC specifier), a plus or minus
+        // sign followed by two or four digits (for the UTC offset), or a
+        // period or comma to start the fraction.
+        final char s1 = valueString.charAt(12);
+        switch (s1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 15) {
+                throw invalidChar(valueString, s1, 12);
+            }
+
+            second = 10 * (s1 - '0');
+            second += toInt(valueString.charAt(13),
+                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(12, 14));
+
+            break;
+
+        case '6':
+            // There must be at least two more characters and the next one
+            // must be a 0.
+            if (length < 15) {
+                throw invalidChar(valueString, s1, 12);
+            }
+
+            if (valueString.charAt(13) != '0') {
+                throw new LocalizedIllegalArgumentException(
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get(
+                                valueString, valueString.substring(12, 14)));
+            }
+
+            second = 60;
+            break;
+
+        case 'Z':
+        case 'z':
+            // This is fine only if we are at the end of the value.
+            if (length == 13) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, s1, 12);
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if (length == 15 || length == 17) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 12);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, s1, 12);
+            }
+
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 13, year, month, day, hour, minute, second,
+                    60000);
+
+        default:
+            throw invalidChar(valueString, s1, 12);
+        }
+
+        // Next, there should be either a period or comma followed by
+        // between one and three digits (to specify the sub-second), a
+        // letter 'Z' (for the UTC specifier), or a plus or minus sign
+        // followed by two our four digits (for the UTC offset).
+        switch (valueString.charAt(14)) {
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 15, year, month, day, hour, minute, second,
+                    1000);
+
+        case 'Z':
+        case 'z':
+            // This is fine only if we are at the end of the value.
+            if (length == 15) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, valueString.charAt(14), 14);
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if (length == 17 || length == 19) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 14);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                throw invalidChar(valueString, valueString.charAt(14), 14);
+            }
+
+        default:
+            throw invalidChar(valueString, valueString.charAt(14), 14);
+        }
+    }
+
+    private static LocalizedIllegalArgumentException invalidChar(String valueString, char c, int pos) {
+        return new LocalizedIllegalArgumentException(
+                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
+                        valueString, String.valueOf(c), pos));
+    }
+
+    private static int toInt(char c, Arg2<Object, Object> invalidSyntaxMsg, String valueString, String unitValue) {
+        switch (c) {
+        case '0':
+            return 0;
+        case '1':
+            return 1;
+        case '2':
+            return 2;
+        case '3':
+            return 3;
+        case '4':
+            return 4;
+        case '5':
+            return 5;
+        case '6':
+            return 6;
+        case '7':
+            return 7;
+        case '8':
+            return 8;
+        case '9':
+            return 9;
+        default:
+            throw new LocalizedIllegalArgumentException(
+                invalidSyntaxMsg.get(valueString, unitValue));
+        }
+    }
+
+    /**
+     * Returns a generalized time object representing the provided date / time
+     * parameters.
+     *
+     * @param value
+     *            The generalized time string representation.
+     * @param year
+     *            The year.
+     * @param month
+     *            The month.
+     * @param day
+     *            The day.
+     * @param hour
+     *            The hour.
+     * @param minute
+     *            The minute.
+     * @param second
+     *            The second.
+     * @param tz
+     *            The timezone.
+     * @return A generalized time representing the provided date / time
+     *         parameters.
+     * @throws LocalizedIllegalArgumentException
+     *             If the generalized time could not be created.
+     */
+    private static GeneralizedTime createTime(final String value, final int year, final int month,
+            final int day, final int hour, final int minute, final int second, final TimeZone tz) {
+        try {
+            final GregorianCalendar calendar = new GregorianCalendar();
+            calendar.setLenient(false);
+            calendar.setTimeZone(tz);
+            calendar.set(year, month, day, hour, minute, second);
+            calendar.set(Calendar.MILLISECOND, 0);
+            return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value);
+        } catch (final Exception e) {
+            // This should only happen if the provided date wasn't legal
+            // (e.g., September 31).
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
+            throw new LocalizedIllegalArgumentException(message, e);
+        }
+    }
+
+    /**
+     * Completes decoding the generalized time value containing a fractional
+     * component. It will also decode the trailing 'Z' or offset.
+     *
+     * @param value
+     *            The whole value, including the fractional component and time
+     *            zone information.
+     * @param startPos
+     *            The position of the first character after the period in the
+     *            value string.
+     * @param year
+     *            The year decoded from the provided value.
+     * @param month
+     *            The month decoded from the provided value.
+     * @param day
+     *            The day decoded from the provided value.
+     * @param hour
+     *            The hour decoded from the provided value.
+     * @param minute
+     *            The minute decoded from the provided value.
+     * @param second
+     *            The second decoded from the provided value.
+     * @param multiplier
+     *            The multiplier value that should be used to scale the fraction
+     *            appropriately. If it's a fraction of an hour, then it should
+     *            be 3600000 (60*60*1000). If it's a fraction of a minute, then
+     *            it should be 60000. If it's a fraction of a second, then it
+     *            should be 1000.
+     * @return The timestamp created from the provided generalized time value
+     *         including the fractional element.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided value cannot be parsed as a valid generalized
+     *             time string.
+     */
+    private static GeneralizedTime finishDecodingFraction(final String value, final int startPos,
+            final int year, final int month, final int day, final int hour, final int minute,
+            final int second, final int multiplier) {
+        final int length = value.length();
+        final StringBuilder fractionBuffer = new StringBuilder((2 + length) - startPos);
+        fractionBuffer.append("0.");
+
+        TimeZone timeZone = null;
+
+    outerLoop:
+        for (int i = startPos; i < length; i++) {
+            final char c = value.charAt(i);
+            switch (c) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                fractionBuffer.append(c);
+                break;
+
+            case 'Z':
+            case 'z':
+                // This is only acceptable if we're at the end of the value.
+                if (i != (value.length() - 1)) {
+                    final LocalizableMessage message =
+                            WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value,
+                                    String.valueOf(c));
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+
+                timeZone = TIME_ZONE_UTC_OBJ;
+                break outerLoop;
+
+            case '+':
+            case '-':
+                timeZone = getTimeZoneForOffset(value, i);
+                break outerLoop;
+
+            default:
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value, String
+                                .valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        if (fractionBuffer.length() == 2) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        if (timeZone == null) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        final Double fractionValue = Double.parseDouble(fractionBuffer.toString());
+        final int additionalMilliseconds = (int) Math.round(fractionValue * multiplier);
+
+        try {
+            final GregorianCalendar calendar = new GregorianCalendar();
+            calendar.setLenient(false);
+            calendar.setTimeZone(timeZone);
+            calendar.set(year, month, day, hour, minute, second);
+            calendar.set(Calendar.MILLISECOND, additionalMilliseconds);
+            return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value);
+        } catch (final Exception e) {
+            // This should only happen if the provided date wasn't legal
+            // (e.g., September 31).
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
+            throw new LocalizedIllegalArgumentException(message, e);
+        }
+    }
+
+    /**
+     * Decodes a time zone offset from the provided value.
+     *
+     * @param value
+     *            The whole value, including the offset.
+     * @param startPos
+     *            The position of the first character that is contained in the
+     *            offset. This should be the position of the plus or minus
+     *            character.
+     * @return The {@code TimeZone} object representing the decoded time zone.
+     */
+    private static TimeZone getTimeZoneForOffset(final String value, final int startPos) {
+        final String offSetStr = value.substring(startPos);
+        final int len = offSetStr.length();
+        if (len != 3 && len != 5) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // The first character must be either a plus or minus.
+        switch (offSetStr.charAt(0)) {
+        case '+':
+        case '-':
+            // These are OK.
+            break;
+
+        default:
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // The first two characters must be an integer between 00 and 23.
+        switch (offSetStr.charAt(1)) {
+        case '0':
+        case '1':
+            switch (offSetStr.charAt(2)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+
+            default:
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+            break;
+
+        case '2':
+            switch (offSetStr.charAt(2)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+                // These are all fine.
+                break;
+
+            default:
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+            break;
+
+        default:
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        // If there are two more characters, then they must be an integer
+        // between 00 and 59.
+        if (offSetStr.length() == 5) {
+            switch (offSetStr.charAt(3)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+                switch (offSetStr.charAt(4)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    // These are all fine.
+                    break;
+
+                default:
+                    final LocalizableMessage message =
+                            WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+                    throw new LocalizedIllegalArgumentException(message);
+                }
+                break;
+
+            default:
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        // If we've gotten here, then it looks like a valid offset. We can
+        // create a time zone by using "GMT" followed by the offset.
+        return TimeZone.getTimeZone("GMT" + offSetStr);
+    }
+
+    /** Lazily constructed internal representations. */
+    private volatile Calendar calendar;
+    private volatile Date date;
+    private volatile String stringValue;
+    private volatile long timeMS;
+
+    private GeneralizedTime(final Calendar calendar, final Date date, final long time,
+            final String stringValue) {
+        this.calendar = calendar;
+        this.date = date;
+        this.timeMS = time;
+        this.stringValue = stringValue;
+    }
+
+    @Override
+    public int compareTo(final GeneralizedTime o) {
+        final Long timeMS1 = getTimeInMillis();
+        final Long timeMS2 = o.getTimeInMillis();
+        return timeMS1.compareTo(timeMS2);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof GeneralizedTime) {
+            return getTimeInMillis() == ((GeneralizedTime) obj).getTimeInMillis();
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the value of this generalized time in milliseconds since the
+     * epoch.
+     *
+     * @return The value of this generalized time in milliseconds since the
+     *         epoch.
+     */
+    public long getTimeInMillis() {
+        long tmpTimeMS = timeMS;
+        if (tmpTimeMS == Long.MIN_VALUE) {
+            if (date != null) {
+                tmpTimeMS = date.getTime();
+            } else {
+                tmpTimeMS = calendar.getTimeInMillis();
+            }
+            timeMS = tmpTimeMS;
+        }
+        return tmpTimeMS;
+    }
+
+    @Override
+    public int hashCode() {
+        return ((Long) getTimeInMillis()).hashCode();
+    }
+
+    /**
+     * Returns a {@code Calendar} representation of this generalized time.
+     * <p>
+     * Subsequent modifications to the returned calendar will not alter the
+     * internal state of this generalized time.
+     *
+     * @return A {@code Calendar} representation of this generalized time.
+     */
+    public Calendar toCalendar() {
+        return (Calendar) getCalendar().clone();
+    }
+
+    /**
+     * Returns a {@code Date} representation of this generalized time.
+     * <p>
+     * Subsequent modifications to the returned date will not alter the internal
+     * state of this generalized time.
+     *
+     * @return A {@code Date} representation of this generalized time.
+     */
+    public Date toDate() {
+        Date tmpDate = date;
+        if (tmpDate == null) {
+            tmpDate = new Date(getTimeInMillis());
+            date = tmpDate;
+        }
+        return (Date) tmpDate.clone();
+    }
+
+    @Override
+    public String toString() {
+        String tmpString = stringValue;
+        if (tmpString == null) {
+            // Do this in a thread-safe non-synchronized fashion.
+            // (Simple)DateFormat is neither fast nor thread-safe.
+            final StringBuilder sb = new StringBuilder(19);
+            final Calendar tmpCalendar = getCalendar();
+
+            // Format the year yyyy.
+            int n = tmpCalendar.get(Calendar.YEAR);
+            if (n < 0) {
+                throw new IllegalArgumentException("Year cannot be < 0:" + n);
+            } else if (n < 10) {
+                sb.append("000");
+            } else if (n < 100) {
+                sb.append("00");
+            } else if (n < 1000) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the month MM.
+            n = tmpCalendar.get(Calendar.MONTH) + 1;
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the day dd.
+            n = tmpCalendar.get(Calendar.DAY_OF_MONTH);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the hour HH.
+            n = tmpCalendar.get(Calendar.HOUR_OF_DAY);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the minute mm.
+            n = tmpCalendar.get(Calendar.MINUTE);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the seconds ss.
+            n = tmpCalendar.get(Calendar.SECOND);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the milli-seconds.
+            n = tmpCalendar.get(Calendar.MILLISECOND);
+            if (n != 0) {
+                sb.append('.');
+                if (n < 10) {
+                    sb.append("00");
+                } else if (n < 100) {
+                    sb.append("0");
+                }
+                sb.append(n);
+            }
+
+            // Format the timezone.
+            n = tmpCalendar.get(Calendar.ZONE_OFFSET) + tmpCalendar.get(Calendar.DST_OFFSET);
+            if (n == 0) {
+                sb.append('Z');
+            } else {
+                if (n < 0) {
+                    sb.append('-');
+                    n = -n;
+                } else {
+                    sb.append('+');
+                }
+                n = n / 60000; // Minutes.
+
+                final int h = n / 60;
+                if (h < 10) {
+                    sb.append("0");
+                }
+                sb.append(h);
+
+                final int m = n % 60;
+                if (m < 10) {
+                    sb.append("0");
+                }
+                sb.append(m);
+            }
+            tmpString = sb.toString();
+            stringValue = tmpString;
+        }
+        return tmpString;
+    }
+
+    private Calendar getCalendar() {
+        Calendar tmpCalendar = calendar;
+        if (tmpCalendar == null) {
+            tmpCalendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
+            tmpCalendar.setLenient(false);
+            tmpCalendar.setTimeInMillis(getTimeInMillis());
+            calendar = tmpCalendar;
+        }
+        return tmpCalendar;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/IntermediateResponseHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/IntermediateResponseHandler.java
new file mode 100644
index 0000000..d68dcc2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/IntermediateResponseHandler.java
@@ -0,0 +1,58 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+
+/**
+ * A completion handler for consuming intermediate responses returned from
+ * extended operations, or other operations for which an appropriate control was
+ * sent.
+ * <p>
+ * Intermediate responses are rarely used in practice and are therefore only
+ * supported in a few specialized cases where they are most likely to be
+ * encountered:
+ * <ul>
+ * <li>when performing extended requests using the
+ * {@link Connection#extendedRequest} methods
+ * <li>when using the asynchronous operation methods, such as
+ * {@link Connection#addAsync}
+ * </ul>
+ * When no handler is provided any intermediate responses will be discarded.
+ * <p>
+ * The {@link #handleIntermediateResponse} method is invoked each time a
+ * Intermediate Response is returned from the Directory Server.
+ * <p>
+ * Implementations of these methods should complete in a timely manner so as to
+ * avoid keeping the invoking thread from dispatching to other completion
+ * handlers.
+ */
+public interface IntermediateResponseHandler {
+    /**
+     * Invoked each time an intermediate response is returned from the Directory
+     * Server.
+     *
+     * @param response
+     *            The intermediate response.
+     * @return {@code true} if this handler should continue to be notified of
+     *         any remaining intermediate responses, or {@code false} if the
+     *         remaining responses should be skipped for some reason (e.g. a
+     *         client side size limit has been reached).
+     */
+    boolean handleIntermediateResponse(IntermediateResponse response);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnection.java
new file mode 100644
index 0000000..f7ec3ec
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnection.java
@@ -0,0 +1,186 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.ExtendedResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.ResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
+import org.forgerock.util.Reject;
+
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
+/**
+ * This class defines a pseudo-connection object that can be used for performing
+ * internal operations directly against a {@code ServerConnection}
+ * implementation.
+ */
+final class InternalConnection extends AbstractAsynchronousConnection {
+    private final ServerConnection<Integer> serverConnection;
+    private final List<ConnectionEventListener> listeners = new CopyOnWriteArrayList<>();
+    private final AtomicInteger messageID = new AtomicInteger();
+
+    /**
+     * Sets the server connection associated with this internal connection.
+     *
+     * @param serverConnection
+     *            The server connection.
+     */
+    InternalConnection(final ServerConnection<Integer> serverConnection) {
+        this.serverConnection = serverConnection;
+    }
+
+    @Override
+    public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+        final int i = messageID.getAndIncrement();
+        serverConnection.handleAbandon(i, request);
+        return newSuccessfulLdapPromise((Void) null, i);
+    }
+
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ResultLdapPromiseImpl<AddRequest, Result> promise =
+                newResultLdapPromise(i, request, intermediateResponseHandler, this);
+        serverConnection.handleAdd(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public void addConnectionEventListener(final ConnectionEventListener listener) {
+        Reject.ifNull(listener);
+        listeners.add(listener);
+    }
+
+    @Override
+    public LdapPromise<BindResult> bindAsync(final BindRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final BindResultLdapPromiseImpl promise =
+                newBindLdapPromise(i, request, null, intermediateResponseHandler);
+        serverConnection.handleBind(i, 3, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public void close(final UnbindRequest request, final String reason) {
+        final int i = messageID.getAndIncrement();
+        serverConnection.handleConnectionClosed(i, request);
+    }
+
+    @Override
+    public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ResultLdapPromiseImpl<CompareRequest, CompareResult> promise =
+                newCompareLdapPromise(i, request, intermediateResponseHandler, this);
+        serverConnection.handleCompare(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public LdapPromise<Result> deleteAsync(final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ResultLdapPromiseImpl<DeleteRequest, Result> promise =
+                newResultLdapPromise(i, request, intermediateResponseHandler, this);
+        serverConnection.handleDelete(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ExtendedResultLdapPromiseImpl<R> promise = newExtendedLdapPromise(i, request, intermediateResponseHandler,
+                this);
+        serverConnection.handleExtendedRequest(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public boolean isClosed() {
+        // FIXME: this should be true after close has been called.
+        return false;
+    }
+
+    @Override
+    public boolean isValid() {
+        // FIXME: this should be false if this connection is disconnected.
+        return true;
+    }
+
+    @Override
+    public LdapPromise<Result> modifyAsync(final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ResultLdapPromiseImpl<ModifyRequest, Result> promise =
+                newResultLdapPromise(i, request, intermediateResponseHandler, this);
+        serverConnection.handleModify(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int i = messageID.getAndIncrement();
+        final ResultLdapPromiseImpl<ModifyDNRequest, Result> promise =
+                newResultLdapPromise(i, request, intermediateResponseHandler, this);
+        serverConnection.handleModifyDN(i, request, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public void removeConnectionEventListener(final ConnectionEventListener listener) {
+        Reject.ifNull(listener);
+        listeners.remove(listener);
+    }
+
+    @Override
+    public LdapPromise<Result> searchAsync(final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
+        final int i = messageID.getAndIncrement();
+        final SearchResultLdapPromiseImpl promise =
+                newSearchLdapPromise(i, request, entryHandler, intermediateResponseHandler, this);
+        serverConnection.handleSearch(i, request, promise, promise, promise);
+        return promise;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + serverConnection + ')';
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnectionFactory.java
new file mode 100644
index 0000000..58f75ab
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/InternalConnectionFactory.java
@@ -0,0 +1,88 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.promise.Promise;
+
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.forgerock.util.promise.Promises.*;
+
+
+/**
+ * A special {@code ConnectionFactory} which waits for internal connection
+ * requests and binds them to a {@link ServerConnection} created using the
+ * provided {@link ServerConnectionFactory}.
+ * <p>
+ * When processing requests, {@code ServerConnection} implementations are passed
+ * an integer as the first parameter. This integer represents a pseudo
+ * {@code requestID} which is incremented for each successive internal request
+ * on a per connection basis. The request ID may be useful for logging purposes.
+ * <p>
+ * An {@code InternalConnectionFactory} does not require
+ * {@code ServerConnection} implementations to return a result when processing
+ * requests. However, it is recommended that implementations do always return
+ * results even for abandoned requests. This is because application client
+ * threads may block indefinitely waiting for results.
+ *
+ * @param <C>
+ *            The type of client context.
+ */
+final class InternalConnectionFactory<C> implements ConnectionFactory {
+    private final ServerConnectionFactory<C, Integer> factory;
+    private final C clientContext;
+
+    InternalConnectionFactory(final ServerConnectionFactory<C, Integer> factory,
+            final C clientContext) {
+        this.factory = factory;
+        this.clientContext = clientContext;
+    }
+
+    @Override
+    public void close() {
+        // Nothing to do.
+    }
+
+    @Override
+    public Connection getConnection() throws LdapException {
+        final ServerConnection<Integer> serverConnection = factory.handleAccept(clientContext);
+        return new InternalConnection(serverConnection);
+    }
+
+    @Override
+    public Promise<Connection, LdapException> getConnectionAsync() {
+        final ServerConnection<Integer> serverConnection;
+        try {
+            serverConnection = factory.handleAccept(clientContext);
+        } catch (final LdapException e) {
+            return newFailedLdapPromise(e);
+        }
+
+        return newResultPromise((Connection) new InternalConnection(serverConnection));
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("InternalConnectionFactory(");
+        builder.append(clientContext);
+        builder.append(',');
+        builder.append(factory);
+        builder.append(')');
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/KeyManagers.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/KeyManagers.java
new file mode 100644
index 0000000..39f5573
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/KeyManagers.java
@@ -0,0 +1,283 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509KeyManager;
+
+import org.forgerock.util.Reject;
+
+/** This class contains methods for creating common types of key manager. */
+public final class KeyManagers {
+    /**
+     * This class implements an X.509 key manager that will be used to wrap an
+     * existing key manager and makes it possible to configure which
+     * certificate(s) should be used for client and/or server operations. The
+     * certificate selection will be based on the alias (also called the
+     * nickname) of the certificate.
+     */
+    private static final class SelectCertificate extends X509ExtendedKeyManager {
+        private final String alias;
+        private final X509KeyManager keyManager;
+
+        private SelectCertificate(final X509KeyManager keyManager, final String alias) {
+            this.keyManager = keyManager;
+            this.alias = alias;
+        }
+
+        @Override
+        public String chooseClientAlias(final String[] keyType, final Principal[] issuers,
+                final Socket socket) {
+            for (final String type : keyType) {
+                final String[] clientAliases = keyManager.getClientAliases(type, issuers);
+                if (clientAliases != null) {
+                    for (final String clientAlias : clientAliases) {
+                        if (clientAlias.equals(alias)) {
+                            return alias;
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public String chooseEngineClientAlias(final String[] keyType, final Principal[] issuers,
+                final SSLEngine engine) {
+            for (final String type : keyType) {
+                final String[] clientAliases = keyManager.getClientAliases(type, issuers);
+                if (clientAliases != null) {
+                    for (final String clientAlias : clientAliases) {
+                        if (clientAlias.equals(alias)) {
+                            return alias;
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public String chooseEngineServerAlias(final String keyType, final Principal[] issuers,
+                final SSLEngine engine) {
+            final String[] serverAliases = keyManager.getServerAliases(keyType, issuers);
+            if (serverAliases != null) {
+                for (final String serverAlias : serverAliases) {
+                    if (serverAlias.equalsIgnoreCase(alias)) {
+                        return serverAlias;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public String chooseServerAlias(final String keyType, final Principal[] issuers,
+                final Socket socket) {
+            final String[] serverAliases = keyManager.getServerAliases(keyType, issuers);
+            if (serverAliases != null) {
+                for (final String serverAlias : serverAliases) {
+                    if (serverAlias.equals(alias)) {
+                        return alias;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public X509Certificate[] getCertificateChain(final String alias) {
+            return keyManager.getCertificateChain(alias);
+        }
+
+        @Override
+        public String[] getClientAliases(final String keyType, final Principal[] issuers) {
+            return keyManager.getClientAliases(keyType, issuers);
+        }
+
+        @Override
+        public PrivateKey getPrivateKey(final String alias) {
+            return keyManager.getPrivateKey(alias);
+        }
+
+        @Override
+        public String[] getServerAliases(final String keyType, final Principal[] issuers) {
+            return keyManager.getServerAliases(keyType, issuers);
+        }
+    }
+
+    /**
+     * Creates a new {@code X509KeyManager} which will use the named key store
+     * file for retrieving certificates. It will use the default key store
+     * format for the JVM (e.g. {@code JKS}) and will not use a password to open
+     * the key store.
+     *
+     * @param file
+     *            The key store file name.
+     * @return A new {@code X509KeyManager} which will use the named key store
+     *         file for retrieving certificates.
+     * @throws GeneralSecurityException
+     *             If the key store could not be loaded, perhaps due to
+     *             incorrect format, or missing algorithms.
+     * @throws IOException
+     *             If the key store file could not be found or could not be
+     *             read.
+     * @throws NullPointerException
+     *             If {@code file} was {@code null}.
+     */
+    public static X509KeyManager useKeyStoreFile(final String file)
+            throws GeneralSecurityException, IOException {
+        return useKeyStoreFile(file, null, null);
+    }
+
+    /**
+     * Creates a new {@code X509KeyManager} which will use the named key store
+     * file for retrieving certificates. It will use the provided key store
+     * format and password.
+     *
+     * @param file
+     *            The key store file name.
+     * @param password
+     *            The key store password, which may be {@code null}.
+     * @param format
+     *            The key store format, which may be {@code null} to indicate
+     *            that the default key store format for the JVM (e.g.
+     *            {@code JKS}) should be used.
+     * @return A new {@code X509KeyManager} which will use the named key store
+     *         file for retrieving certificates.
+     * @throws GeneralSecurityException
+     *             If the key store could not be loaded, perhaps due to
+     *             incorrect format, or missing algorithms.
+     * @throws IOException
+     *             If the key store file could not be found or could not be
+     *             read.
+     * @throws NullPointerException
+     *             If {@code file} was {@code null}.
+     */
+    public static X509KeyManager useKeyStoreFile(final String file, final char[] password,
+            final String format) throws GeneralSecurityException, IOException {
+        Reject.ifNull(file);
+
+        final File keyStoreFile = new File(file);
+        final String keyStoreFormat = format != null ? format : KeyStore.getDefaultType();
+
+        final KeyStore keyStore = KeyStore.getInstance(keyStoreFormat);
+        try (FileInputStream fos = new FileInputStream(keyStoreFile)) {
+            keyStore.load(fos, password);
+        }
+
+        final KeyManagerFactory kmf =
+                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(keyStore, password);
+
+        X509KeyManager x509km = null;
+        for (final KeyManager km : kmf.getKeyManagers()) {
+            if (km instanceof X509KeyManager) {
+                x509km = (X509KeyManager) km;
+                break;
+            }
+        }
+
+        if (x509km == null) {
+            throw new NoSuchAlgorithmException();
+        }
+
+        return x509km;
+    }
+
+    /**
+     * Creates a new {@code X509KeyManager} which will use a PKCS#11 token for
+     * retrieving certificates.
+     *
+     * @param password
+     *            The password to use for accessing the PKCS#11 token, which may
+     *            be {@code null} if no password is required.
+     * @return A new {@code X509KeyManager} which will use a PKCS#11 token for
+     *         retrieving certificates.
+     * @throws GeneralSecurityException
+     *             If the PKCS#11 token could not be accessed, perhaps due to
+     *             incorrect password, or missing algorithms.
+     * @throws IOException
+     *             If the PKCS#11 token could not be found or could not be read.
+     */
+    public static X509KeyManager usePKCS11Token(final char[] password)
+            throws GeneralSecurityException, IOException {
+        final KeyStore keyStore = KeyStore.getInstance("PKCS11");
+        keyStore.load(null, password);
+        final KeyManagerFactory kmf =
+                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(keyStore, password);
+
+        X509KeyManager x509km = null;
+        for (final KeyManager km : kmf.getKeyManagers()) {
+            if (km instanceof X509KeyManager) {
+                x509km = (X509KeyManager) km;
+                break;
+            }
+        }
+
+        if (x509km == null) {
+            throw new NoSuchAlgorithmException();
+        }
+
+        return x509km;
+    }
+
+    /**
+     * Returns a new {@code X509KeyManager} which selects the named certificate
+     * from the provided {@code X509KeyManager}.
+     *
+     * @param alias
+     *            The nickname of the certificate that should be selected for
+     *            operations involving this key manager.
+     * @param keyManager
+     *            The key manager to be filtered.
+     * @return The filtered key manager.
+     * @throws NullPointerException
+     *             If {@code keyManager} or {@code alias} was {@code null}.
+     */
+    public static X509KeyManager useSingleCertificate(final String alias,
+            final X509KeyManager keyManager) {
+        Reject.ifNull(alias, keyManager);
+        return new SelectCertificate(keyManager, alias);
+    }
+
+    /** Prevent insantiation. */
+    private KeyManagers() {
+        // Nothing to do.
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPClientContext.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPClientContext.java
new file mode 100644
index 0000000..7d5b69c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPClientContext.java
@@ -0,0 +1,162 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.net.InetSocketAddress;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * An LDAP client which has connected to a {@link ServerConnectionFactory}. An
+ * LDAP client context can be used to query information about the client's
+ * connection such as their network address, as well as managing the state of
+ * the connection.
+ */
+public interface LDAPClientContext {
+
+    /**
+     * Disconnects the client without sending a disconnect notification.
+     * <p>
+     * <b>Server connections:</b> invoking this method causes
+     * {@link ServerConnection#handleConnectionDisconnected
+     * handleConnectionDisconnected} to be called before this method returns.
+     */
+    void disconnect();
+
+    /**
+     * Disconnects the client and sends a disconnect notification, if possible,
+     * containing the provided result code and diagnostic message.
+     * <p>
+     * <b>Server connections:</b> invoking this method causes
+     * {@link ServerConnection#handleConnectionDisconnected
+     * handleConnectionDisconnected} to be called before this method returns.
+     *
+     * @param resultCode
+     *            The result code which should be included with the disconnect
+     *            notification.
+     * @param message
+     *            The diagnostic message, which may be empty or {@code null}
+     *            indicating that none was provided.
+     */
+    void disconnect(ResultCode resultCode, String message);
+
+    /**
+     * Returns the {@code InetSocketAddress} associated with the local system.
+     *
+     * @return The {@code InetSocketAddress} associated with the local system.
+     */
+    InetSocketAddress getLocalAddress();
+
+    /**
+     * Returns the {@code InetSocketAddress} associated with the remote system.
+     *
+     * @return The {@code InetSocketAddress} associated with the remote system.
+     */
+    InetSocketAddress getPeerAddress();
+
+    /**
+     * Returns the cipher strength, in bits, currently in use by the underlying
+     * connection. This value is analogous to the
+     * {@code javax.servlet.request.key_size} property defined in the Servlet
+     * specification (section 3.8 "SSL Attributes"). It provides no indication
+     * of the relative strength of different cipher algorithms, their known
+     * weaknesses, nor the strength of other cryptographic information used
+     * during SSL/TLS negotiation.
+     *
+     * @return The cipher strength, in bits, currently in use by the underlying
+     *         connection.
+     */
+    int getSecurityStrengthFactor();
+
+    /**
+     * Returns the SSL session currently in use by the underlying connection, or
+     * {@code null} if SSL/TLS is not enabled.
+     *
+     * @return The SSL session currently in use by the underlying connection, or
+     *         {@code null} if SSL/TLS is not enabled.
+     */
+    SSLSession getSSLSession();
+
+    /**
+     * Returns {@code true} if the underlying connection has been closed as a
+     * result of a client disconnect, a fatal connection error, or a server-side
+     * {@link #disconnect}.
+     * <p>
+     * This method provides a polling mechanism which can be used by synchronous
+     * request handler implementations to detect connection termination.
+     * <p>
+     * <b>Server connections:</b> this method will always return {@code true}
+     * when called from within {@link ServerConnection#handleConnectionClosed
+     * handleConnectionClosed},
+     * {@link ServerConnection#handleConnectionDisconnected
+     * handleConnectionDisconnected}, or
+     * {@link ServerConnection#handleConnectionError handleConnectionError}.
+     *
+     * @return {@code true} if the underlying connection has been closed.
+     */
+    boolean isClosed();
+
+    /**
+     * Sends an unsolicited notification to the client.
+     *
+     * @param notification
+     *            The notification to send.
+     */
+    void sendUnsolicitedNotification(ExtendedResult notification);
+
+    /**
+     * Installs the provided connection security layer to the underlying
+     * connection. This may be used to add a SASL integrity and/or
+     * confidentiality protection layer after SASL authentication has completed,
+     * but could also be used to add other layers such as compression. Multiple
+     * layers may be installed.
+     *
+     * @param layer
+     *            The negotiated bind context that can be used to encode and
+     *            decode data on the connection.
+     */
+    void enableConnectionSecurityLayer(ConnectionSecurityLayer layer);
+
+    /**
+     * Installs the TLS/SSL security layer on the underlying connection. The
+     * TLS/SSL security layer will be installed beneath any existing connection
+     * security layers and can only be installed at most once.
+     *
+     * @param sslContext
+     *            The {@code SSLContext} which should be used to secure the
+     * @param protocols
+     *            Names of all the protocols to enable or {@code null} to use
+     *            the default protocols.
+     * @param suites
+     *            Names of all the suites to enable or {@code null} to use the
+     *            default cipher suites.
+     * @param wantClientAuth
+     *            Set to {@code true} if client authentication is requested, or
+     *            {@code false} if no client authentication is desired.
+     * @param needClientAuth
+     *            Set to {@code true} if client authentication is required, or
+     *            {@code false} if no client authentication is desired.
+     * @throws IllegalStateException
+     *             If the TLS/SSL security layer has already been installed.
+     */
+    void enableTLS(SSLContext sslContext, String[] protocols, String[] suites,
+            boolean wantClientAuth, boolean needClientAuth);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java
new file mode 100644
index 0000000..320ed4a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java
@@ -0,0 +1,1224 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_CONNECTION_CLOSED_BY_CLIENT;
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_FAILED;
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_TIMEOUT;
+import static com.forgerock.opendj.ldap.CoreMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
+import static com.forgerock.opendj.util.StaticUtils.DEFAULT_SCHEDULER;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newStartTLSExtendedRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.unmodifiableSearchRequest;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newGenericExtendedResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.newLdapPromiseImpl;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise;
+import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.AbstractQueuedSynchronizer;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldap.spi.ConnectionState;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.time.Duration;
+import org.forgerock.util.time.TimeService;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * A factory class which can be used to obtain connections to an LDAP Directory Server. A connection attempt comprises
+ * of the following steps:
+ * <ul>
+ * <li>first of all a TCP connection to the remote LDAP server is obtained. The attempt will fail if a connection is
+ *     not obtained within the configured {@link #CONNECT_TIMEOUT connect timeout}
+ * <li>if LDAPS (not StartTLS) is requested then an SSL handshake is performed. LDAPS is enabled by specifying the
+ *     {@link #SSL_CONTEXT} option along with {@link #SSL_USE_STARTTLS} set to {@code false}
+ * <li>if StartTLS is requested then a StartTLS request is sent and then an SSL handshake performed once the response
+ *     has been received. StartTLS is enabled by specifying the {@link #SSL_CONTEXT} option along with
+ *     {@link #SSL_USE_STARTTLS} set to {@code true}
+ * <li>an initial authentication request is sent if the {@link #AUTHN_BIND_REQUEST} option is specified
+ * <li>if heart-beat support is enabled via the {@link #HEARTBEAT_ENABLED} option, and none of steps 2-4 were performed,
+ *     then an initial heart-beat is sent in order to determine whether the directory service is available.
+ * <li>the connect attempt will fail if it does not complete within the configured connection timeout. If the SSL
+ *     handshake, StartTLS request, initial bind request, or initial heart-beat fail for any reason then the connection
+ *     attempt will be deemed to have failed and an appropriate error returned.
+ * </ul>
+ * Once a connection has been established heart-beats will be sent periodically on the connection based on the
+ * configured heart-beat interval. If the heart-beat times out then the server is assumed to be down and an appropriate
+ * {@link ConnectionException} generated and published to any registered {@link ConnectionEventListener}s. Note
+ * however, that heart-beats will only be sent when the connection is determined to be reasonably idle: there is no
+ * point in sending heart-beats if the connection has recently received a response. A connection is deemed to be idle
+ * if no response has been received during a period equivalent to half the heart-beat interval.
+ * <p>
+ * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are being
+ * performed. Likewise, a bind or startTLS request will cause active operations to be aborted. This factory coordinates
+ * heart-beats with bind or startTLS requests, ensuring that they are not performed concurrently. Specifically, bind and
+ * startTLS requests are queued up while a heart-beat is pending, and heart-beats are not sent at all while there are
+ * pending bind or startTLS requests.
+ */
+public final class LDAPConnectionFactory extends CommonLDAPOptions implements ConnectionFactory {
+    /**
+     * Configures the connection factory to return pre-authenticated connections using the specified {@link
+     * BindRequest}. The connections returned by the connection factory will support all operations with the exception
+     * of Bind requests. Attempts to perform a Bind will result in an {@code UnsupportedOperationException}.
+     * <p>
+     * If the Bind request fails for some reason (e.g. invalid credentials), then the connection attempt will fail and
+     * an {@link LdapException} will be thrown.
+     */
+    public static final Option<BindRequest> AUTHN_BIND_REQUEST = Option.of(BindRequest.class, null);
+
+    /**
+     * Specifies the connect timeout spcified. If a connection is not established within the timeout period (incl. SSL
+     * negotiation, initial bind request, and/or heart-beat), then a {@link TimeoutResultException} error result will be
+     * returned.
+     * <p>
+     * The default operation timeout is 10 seconds and may be configured using the {@code
+     * org.forgerock.opendj.io.connectTimeout} property. A timeout setting of 0 causes the OS connect timeout to be
+     * used.
+     */
+    public static final Option<Duration> CONNECT_TIMEOUT =
+            Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.connectTimeout", 10000),
+                                            TimeUnit.MILLISECONDS));
+
+    /**
+     * Configures the connection factory to periodically send "heart-beat" or "keep-alive" requests to the Directory
+     * Server. This feature allows client applications to proactively detect network problems or unresponsive
+     * servers. In addition, frequent heartbeat requests may also prevent load-balancers or Directory Servers from
+     * closing otherwise idle connections.
+     * <p>
+     * Before returning new connections to the application the factory will first send an initial heart-beat request in
+     * order to determine that the remote server is responsive. If the heart-beat request fails or is too slow to
+     * respond then the connection is closed immediately and an error returned to the client.
+     * <p>
+     * Once a connection has been established successfully (including the initial heart-beat request), the connection
+     * factory will periodically send heart-beat requests on the connection based on the configured heart-beat interval.
+     * If the Directory Server is too slow to respond to the heart-beat then the server is assumed to be down and an
+     * appropriate {@link ConnectionException} generated and published to any registered
+     * {@link ConnectionEventListener}s. Note however, that heart-beat requests will only be sent when the connection
+     * is determined to be reasonably idle: there is no point in sending heart-beats if the connection has recently
+     * received a response. A connection is deemed to be idle if no response has been received during a period
+     * equivalent to half the heart-beat interval.
+     * <p>
+     * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are
+     * being performed. Likewise, a bind or startTLS request will cause active operations to be aborted. The LDAP
+     * connection factory coordinates heart-beats with bind or startTLS requests, ensuring that they are not performed
+     * concurrently. Specifically, bind and startTLS requests are queued up while a heart-beat is pending, and
+     * heart-beats are not sent at all while there are pending bind or startTLS requests.
+     */
+    public static final Option<Boolean> HEARTBEAT_ENABLED = Option.withDefault(false);
+
+    /**
+     * Specifies the time between successive heart-beat requests (default interval is 10 seconds). Heart-beats will only
+     * be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<Duration> HEARTBEAT_INTERVAL = Option.withDefault(new Duration(10L, SECONDS));
+
+    /**
+     * Specifies the scheduler which will be used for periodically sending heart-beat requests. A system-wide scheduler
+     * will be used by default. Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<ScheduledExecutorService> HEARTBEAT_SCHEDULER =
+            Option.of(ScheduledExecutorService.class, null);
+
+    /**
+     * Specifies the timeout for heart-beat requests, after which the remote Directory Server will be deemed to be
+     * unavailable (default timeout is 3 seconds). Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to
+     * {@code true}. If a {@link #REQUEST_TIMEOUT request timeout} is also set then the lower of the two will be used
+     * for sending heart-beats.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<Duration> HEARTBEAT_TIMEOUT = Option.withDefault(new Duration(3L, SECONDS));
+
+    /**
+     * Specifies the operation timeout. If a response is not received from the Directory Server within the timeout
+     * period, then the operation will be abandoned and a {@link TimeoutResultException} error result returned. A
+     * timeout setting of 0 disables operation timeout limits.
+     * <p>
+     * The default operation timeout is 0 (no timeout) and may be configured using the {@code
+     * org.forgerock.opendj.io.requestTimeout} property or the deprecated {@code org.forgerock.opendj.io.timeout}
+     * property.
+     */
+    public static final Option<Duration> REQUEST_TIMEOUT =
+            Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.requestTimeout",
+                                                                  getIntProperty("org.forgerock.opendj.io.timeout", 0)),
+                                            TimeUnit.MILLISECONDS));
+
+    /**
+     * Specifies the SSL context which will be used when initiating connections with the Directory Server.
+     * <p>
+     * By default no SSL context will be used, indicating that connections will not be secured. If an SSL context is set
+     * then connections will be secured using either SSL or StartTLS depending on {@link #SSL_USE_STARTTLS}.
+     */
+    public static final Option<SSLContext> SSL_CONTEXT = Option.of(SSLContext.class, null);
+
+    /**
+     * Specifies the cipher suites enabled for secure connections with the Directory Server.
+     * <p>
+     * The suites must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the suites listed
+     * in the parameter are enabled for use.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static final Option<List<String>> SSL_ENABLED_CIPHER_SUITES =
+            (Option) Option.of(List.class, Collections.<String>emptyList());
+
+    /**
+     * Specifies the protocol versions enabled for secure connections with the Directory Server.
+     * <p>
+     * The protocols must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the protocols
+     * listed in the parameter are enabled for use.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static final Option<List<String>> SSL_ENABLED_PROTOCOLS =
+            (Option) Option.of(List.class, Collections.<String>emptyList());
+
+    /**
+     * Specifies whether SSL or StartTLS should be used for securing connections when an SSL context is specified.
+     * <p>
+     * By default SSL will be used in preference to StartTLS.
+     */
+    public static final Option<Boolean> SSL_USE_STARTTLS = Option.withDefault(false);
+
+    /** Default heart-beat which will target the root DSE but not return any results. */
+    private static final SearchRequest DEFAULT_HEARTBEAT =
+            unmodifiableSearchRequest(newSearchRequest("", SearchScope.BASE_OBJECT, "(objectClass=*)", "1.1"));
+
+    /**
+     * Specifies the parameters of the search request that will be used for heart-beats. The default heart-beat search
+     * request is a base object search against the root DSE requesting no attributes. Heart-beats will only be sent if
+     * {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<SearchRequest> HEARTBEAT_SEARCH_REQUEST =
+            Option.of(SearchRequest.class, DEFAULT_HEARTBEAT);
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The overall timeout to use when establishing connections, including SSL, bind, and heart-beat. */
+    private final long connectTimeoutMS;
+
+    /**
+     * The minimum amount of time the connection should remain idle (no responses) before starting to send heartbeats.
+     */
+    private final long heartBeatDelayMS;
+
+    /** Indicates whether heartbeats should be performed. */
+    private final Boolean heartBeatEnabled;
+
+    /** The heartbeat search request. */
+    private final SearchRequest heartBeatRequest;
+
+    /**
+     * The heartbeat timeout in milli-seconds. The connection will be marked as failed if no heartbeat response is
+     * received within the timeout.
+     */
+    private final long heartBeatTimeoutMS;
+
+    /** The interval between successive heartbeats. */
+    private final long heartBeatintervalMS;
+
+    /** The factory responsible for handling the low-level network communication with the Directory Server. */
+    private final LDAPConnectionFactoryImpl impl;
+
+    /** The optional bind request which will be used as the initial heartbeat if specified. */
+    private final BindRequest initialBindRequest;
+
+    /** Flag which indicates whether this factory has been closed. */
+    private final AtomicBoolean isClosed = new AtomicBoolean();
+
+    /** A copy of the original options. This is only useful for debugging. */
+    private final Options options;
+
+    /** Transport provider that provides the implementation of this factory. */
+    private final TransportProvider provider;
+
+    /**
+     * Prevents the scheduler being released when there are remaining references (this factory or any connections). It
+     * is initially set to 1 because this factory has a reference.
+     */
+    private final AtomicInteger referenceCount = new AtomicInteger(1);
+
+    /** The heartbeat scheduler. */
+    private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler;
+
+    /** Non-null if SSL or StartTLS should be used when creating new connections. */
+    private final SSLContext sslContext;
+
+    /** The list of permitted SSL ciphers for SSL negotiation. */
+    private final List<String> sslEnabledCipherSuites;
+
+    /** The list of permitted SSL protocols for SSL negotiation. */
+    private final List<String> sslEnabledProtocols;
+
+    /** Indicates whether a StartTLS request should be sent immediately after connecting. */
+    private final boolean sslUseStartTLS;
+
+    /** List of valid connections to which heartbeats will be sent. */
+    private final List<ConnectionImpl> validConnections = new LinkedList<>();
+
+    /** This is package private in order to allow unit tests to inject fake time stamps. */
+    TimeService timeService = TimeService.SYSTEM;
+
+    /** Scheduled task which sends heart beats for all valid idle connections. */
+    private final Runnable sendHeartBeatRunnable = new Runnable() {
+        @Override
+        public void run() {
+            boolean heartBeatSent = false;
+            for (final ConnectionImpl connection : getValidConnections()) {
+                heartBeatSent |= connection.sendHeartBeat();
+            }
+            if (heartBeatSent) {
+                scheduler.get().schedule(checkHeartBeatRunnable, heartBeatTimeoutMS, TimeUnit.MILLISECONDS);
+            }
+        }
+    };
+
+    /** Scheduled task which checks that all heart beats have been received within the timeout period. */
+    private final Runnable checkHeartBeatRunnable = new Runnable() {
+        @Override
+        public void run() {
+            for (final ConnectionImpl connection : getValidConnections()) {
+                connection.checkForHeartBeat();
+            }
+        }
+    };
+
+    /** The heartbeat scheduled future - which may be null if heartbeats are not being sent (no valid connections). */
+    private ScheduledFuture<?> heartBeatFuture;
+
+    /**
+     * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the
+     * provided host and port number.
+     *
+     * @param host
+     *         The host name.
+     * @param port
+     *         The port number.
+     * @throws NullPointerException
+     *         If {@code host} was {@code null}.
+     * @throws ProviderNotFoundException
+     *         if no provider is available or if the provider requested using options is not found.
+     */
+    public LDAPConnectionFactory(final String host, final int port) {
+        this(host, port, Options.defaultOptions());
+    }
+
+    /**
+     * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the
+     * provided host and port number.
+     *
+     * @param host
+     *         The host name.
+     * @param port
+     *         The port number.
+     * @param options
+     *         The LDAP options to use when creating connections.
+     * @throws NullPointerException
+     *         If {@code host} or {@code options} was {@code null}.
+     * @throws ProviderNotFoundException
+     *         if no provider is available or if the provider requested using options is not found.
+     */
+    public LDAPConnectionFactory(final String host, final int port, final Options options) {
+        Reject.ifNull(host, options);
+
+        this.connectTimeoutMS = options.get(CONNECT_TIMEOUT).to(TimeUnit.MILLISECONDS);
+        Reject.ifTrue(connectTimeoutMS < 0, "connect timeout must be >= 0");
+        Reject.ifTrue(options.get(REQUEST_TIMEOUT).getValue() < 0, "request timeout must be >= 0");
+
+        this.heartBeatEnabled = options.get(HEARTBEAT_ENABLED);
+        this.heartBeatintervalMS = options.get(HEARTBEAT_INTERVAL).to(TimeUnit.MILLISECONDS);
+        this.heartBeatTimeoutMS = options.get(HEARTBEAT_TIMEOUT).to(TimeUnit.MILLISECONDS);
+        this.heartBeatDelayMS = heartBeatintervalMS / 2;
+        this.heartBeatRequest = options.get(HEARTBEAT_SEARCH_REQUEST);
+        if (heartBeatEnabled) {
+            Reject.ifTrue(heartBeatintervalMS <= 0, "heart-beat interval must be positive");
+            Reject.ifTrue(heartBeatTimeoutMS <= 0, "heart-beat timeout must be positive");
+        }
+
+        this.provider = getTransportProvider(options);
+        this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(options.get(HEARTBEAT_SCHEDULER));
+        this.impl = provider.getLDAPConnectionFactory(host, port, options);
+        this.initialBindRequest = options.get(AUTHN_BIND_REQUEST);
+        this.sslContext = options.get(SSL_CONTEXT);
+        this.sslUseStartTLS = options.get(SSL_USE_STARTTLS);
+        this.sslEnabledProtocols = options.get(SSL_ENABLED_PROTOCOLS);
+        this.sslEnabledCipherSuites = options.get(SSL_ENABLED_CIPHER_SUITES);
+
+        this.options = Options.copyOf(options);
+    }
+
+    @Override
+    public void close() {
+        if (isClosed.compareAndSet(false, true)) {
+            synchronized (validConnections) {
+                if (!validConnections.isEmpty()) {
+                    logger.debug(LocalizableMessage.raw(
+                            "HeartbeatConnectionFactory '%s' is closing while %d active connections remain",
+                            this,
+                            validConnections.size()));
+                }
+            }
+            releaseScheduler();
+            impl.close();
+        }
+    }
+
+    @Override
+    public Connection getConnection() throws LdapException {
+        return getConnectionAsync().getOrThrowUninterruptibly();
+    }
+
+    @Override
+    public Promise<Connection, LdapException> getConnectionAsync() {
+        acquireScheduler(); // Protect scheduler.
+
+        // Register the connect timeout timer.
+        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
+        final AtomicReference<LDAPConnectionImpl> connectionHolder = new AtomicReference<>();
+        final ScheduledFuture<?> timeoutFuture;
+        if (connectTimeoutMS > 0) {
+            timeoutFuture = scheduler.get().schedule(new Runnable() {
+                @Override
+                public void run() {
+                    if (promise.tryHandleException(newConnectTimeoutError())) {
+                        closeSilently(connectionHolder.get());
+                        releaseScheduler();
+                    }
+                }
+            }, connectTimeoutMS, TimeUnit.MILLISECONDS);
+        } else {
+            timeoutFuture = null;
+        }
+
+        // Now connect, negotiate SSL, etc.
+        impl.getConnectionAsync()
+            // Save the connection.
+            .then(new Function<LDAPConnectionImpl, LDAPConnectionImpl, LdapException>() {
+                @Override
+                public LDAPConnectionImpl apply(final LDAPConnectionImpl connection) throws LdapException {
+                    connectionHolder.set(connection);
+                    return connection;
+                }
+            })
+            .thenAsync(performStartTLSIfNeeded())
+            .thenAsync(performSSLHandShakeIfNeeded(connectionHolder))
+            .thenAsync(performInitialBindIfNeeded(connectionHolder))
+            .thenAsync(performInitialHeartBeatIfNeeded(connectionHolder))
+            .thenOnResult(new ResultHandler<Result>() {
+                @Override
+                public void handleResult(Result result) {
+                    if (timeoutFuture != null) {
+                        timeoutFuture.cancel(false);
+                    }
+                    final LDAPConnectionImpl connection = connectionHolder.get();
+                    final ConnectionImpl connectionImpl = new ConnectionImpl(connection);
+                    if (!promise.tryHandleResult(registerConnection(connectionImpl))) {
+                        connectionImpl.close();
+                    }
+                }
+            })
+            .thenOnException(new ExceptionHandler<LdapException>() {
+                @Override
+                public void handleException(final LdapException e) {
+                    if (timeoutFuture != null) {
+                        timeoutFuture.cancel(false);
+                    }
+                    final LdapException connectException;
+                    if (e instanceof ConnectionException || e instanceof AuthenticationException) {
+                        connectException = e;
+                    } else if (e instanceof TimeoutResultException) {
+                        connectException = newHeartBeatTimeoutError();
+                    } else {
+                        connectException = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
+                                                            HBCF_HEARTBEAT_FAILED.get(),
+                                                            e);
+                    }
+                    if (promise.tryHandleException(connectException)) {
+                        closeSilently(connectionHolder.get());
+                        releaseScheduler();
+                    }
+                }
+            });
+
+        return promise;
+    }
+
+    /**
+     * Returns the host name of the Directory Server. The returned host name is the same host name that was provided
+     * during construction and may be an IP address. More specifically, this method will not perform a reverse DNS
+     * lookup.
+     *
+     * @return The host name of the Directory Server.
+     */
+    public String getHostName() {
+        return impl.getHostName();
+    }
+
+    /**
+     * Returns the port of the Directory Server.
+     *
+     * @return The port of the Directory Server.
+     */
+    public int getPort() {
+        return impl.getPort();
+    }
+
+    /**
+     * Returns the name of the transport provider, which provides the implementation of this factory.
+     *
+     * @return The name of actual transport provider.
+     */
+    public String getProviderName() {
+        return provider.getName();
+    }
+
+    @Override
+    public String toString() {
+        return "LDAPConnectionFactory(provider=`" + getProviderName() + ", host='" + getHostName() + "', port="
+                + getPort() + ", options=" + options + ")";
+    }
+
+    private void acquireScheduler() {
+        /*
+         * If the factory is not closed then we need to prevent the scheduler from being released while the
+         * connection attempt is in progress.
+         */
+        referenceCount.incrementAndGet();
+        if (isClosed.get()) {
+            releaseScheduler();
+            throw new IllegalStateException("Attempted to get a connection on closed factory");
+        }
+    }
+
+    private ConnectionImpl[] getValidConnections() {
+        synchronized (validConnections) {
+            return validConnections.toArray(new ConnectionImpl[validConnections.size()]);
+        }
+    }
+
+    private LdapException newConnectTimeoutError() {
+        final LocalizableMessage msg = LDAP_CONNECTION_CONNECT_TIMEOUT.get(impl.getSocketAddress(), connectTimeoutMS);
+        return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, msg.toString());
+    }
+
+    private LdapException newHeartBeatTimeoutError() {
+        return newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, HBCF_HEARTBEAT_TIMEOUT.get(heartBeatTimeoutMS));
+    }
+
+    private AsyncFunction<Void, BindResult, LdapException> performInitialBindIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<Void, BindResult, LdapException>() {
+            @Override
+            public Promise<BindResult, LdapException> apply(final Void ignored) throws LdapException {
+                if (initialBindRequest != null) {
+                    return connectionHolder.get().bindAsync(initialBindRequest, null);
+                } else {
+                    return newResultPromise(newBindResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<BindResult, Result, LdapException> performInitialHeartBeatIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<BindResult, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(final BindResult ignored) throws LdapException {
+                // Only send an initial heartbeat if we haven't already interacted with the server.
+                if (heartBeatEnabled && sslContext == null && initialBindRequest == null) {
+                    return connectionHolder.get().searchAsync(heartBeatRequest, null, null);
+                } else {
+                    return newResultPromise(newResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<ExtendedResult, Void, LdapException> performSSLHandShakeIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<ExtendedResult, Void, LdapException>() {
+            @Override
+            public Promise<Void, LdapException> apply(final ExtendedResult extendedResult) throws LdapException {
+                if (sslContext != null && !sslUseStartTLS) {
+                    return connectionHolder.get().enableTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites);
+                } else {
+                    return newResultPromise(null);
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException> performStartTLSIfNeeded() {
+        return new AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException>() {
+            @Override
+            public Promise<ExtendedResult, LdapException> apply(final LDAPConnectionImpl connection)
+                    throws LdapException {
+                if (sslContext != null && sslUseStartTLS) {
+                    final StartTLSExtendedRequest startTLS = newStartTLSExtendedRequest(sslContext)
+                            .addEnabledCipherSuite(sslEnabledCipherSuites)
+                            .addEnabledProtocol(sslEnabledProtocols);
+                    return connection.extendedRequestAsync(startTLS, null);
+                } else {
+                    return newResultPromise((ExtendedResult) newGenericExtendedResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private Connection registerConnection(final ConnectionImpl heartBeatConnection) {
+        synchronized (validConnections) {
+            if (heartBeatEnabled && validConnections.isEmpty()) {
+                // This is the first active connection, so start the heart beat.
+                heartBeatFuture = scheduler.get()
+                                           .scheduleWithFixedDelay(sendHeartBeatRunnable,
+                                                                   0,
+                                                                   heartBeatintervalMS,
+                                                                   TimeUnit.MILLISECONDS);
+            }
+            validConnections.add(heartBeatConnection);
+        }
+        return heartBeatConnection;
+    }
+
+    private void releaseScheduler() {
+        if (referenceCount.decrementAndGet() == 0) {
+            scheduler.release();
+        }
+    }
+
+    /**
+     * This synchronizer prevents Bind or StartTLS operations from being processed concurrently with heart-beats. This
+     * is required because the LDAP protocol specifically states that servers receiving a Bind operation should either
+     * wait for existing operations to complete or abandon them. The same presumably applies to StartTLS operations.
+     * Note that concurrent bind/StartTLS operations are not permitted.
+     * <p>
+     * This connection factory only coordinates Bind and StartTLS requests with heart-beats. It does not attempt to
+     * prevent or control attempts to send multiple concurrent Bind or StartTLS operations, etc.
+     * <p>
+     * This synchronizer can be thought of as cross between a read-write lock and a semaphore. Unlike a read-write lock
+     * there is no requirement that a thread releasing a lock must hold it. In addition, this synchronizer does not
+     * support reentrancy. A thread attempting to acquire exclusively more than once will deadlock, and a thread
+     * attempting to acquire shared more than once will succeed and be required to release an equivalent number of
+     * times.
+     * <p>
+     * The synchronizer has three states:
+     * <ul>
+     *     <li> UNLOCKED(0) - the synchronizer may be acquired shared or exclusively
+     *     <li> LOCKED_EXCLUSIVELY(-1) - the synchronizer is held exclusively and cannot be acquired shared or
+     *          exclusively. An exclusive lock is held while a heart beat is in progress
+     *     <li> LOCKED_SHARED(>0) - the synchronizer is held shared and cannot be acquired exclusively. N shared locks
+     *          are held while N Bind or StartTLS operations are in progress.
+     * </ul>
+     */
+    private static final class Sync extends AbstractQueuedSynchronizer {
+        /** Lock states. Positive values indicate that the shared lock is taken. */
+        private static final int LOCKED_EXCLUSIVELY = -1;
+        private static final int UNLOCKED = 0; // initial state
+
+        /** Keep compiler quiet. */
+        private static final long serialVersionUID = -3590428415442668336L;
+
+        boolean isHeld() {
+            return getState() != 0;
+        }
+
+        void lockShared() {
+            acquireShared(1);
+        }
+
+        boolean tryLockExclusively() {
+            return tryAcquire(0 /* unused */);
+        }
+
+        boolean tryLockShared() {
+            return tryAcquireShared(1) > 0;
+        }
+
+        void unlockExclusively() {
+            release(0 /* unused */);
+        }
+
+        void unlockShared() {
+            releaseShared(0 /* unused */);
+        }
+
+        @Override
+        protected boolean isHeldExclusively() {
+            return getState() == LOCKED_EXCLUSIVELY;
+        }
+
+        @Override
+        protected boolean tryAcquire(final int ignored) {
+            if (compareAndSetState(UNLOCKED, LOCKED_EXCLUSIVELY)) {
+                setExclusiveOwnerThread(Thread.currentThread());
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        protected int tryAcquireShared(final int readers) {
+            for (;;) {
+                final int state = getState();
+                if (state == LOCKED_EXCLUSIVELY) {
+                    return LOCKED_EXCLUSIVELY; // failed
+                }
+                final int newState = state + readers;
+                if (compareAndSetState(state, newState)) {
+                    return newState; // succeeded + more readers allowed
+                }
+            }
+        }
+
+        @Override
+        protected boolean tryRelease(final int ignored) {
+            if (getState() != LOCKED_EXCLUSIVELY) {
+                throw new IllegalMonitorStateException();
+            }
+            setExclusiveOwnerThread(null);
+            setState(UNLOCKED);
+            return true;
+        }
+
+        @Override
+        protected boolean tryReleaseShared(final int ignored) {
+            for (;;) {
+                final int state = getState();
+                if (state == UNLOCKED || state == LOCKED_EXCLUSIVELY) {
+                    throw new IllegalMonitorStateException();
+                }
+                final int newState = state - 1;
+                if (compareAndSetState(state, newState)) {
+                    /*
+                     * We could always return true here, but since there cannot be waiting readers we can specialize
+                     * for waiting writers.
+                     */
+                    return newState == UNLOCKED;
+                }
+            }
+        }
+
+    }
+
+    /** A connection that sends heart beats and supports all operations. */
+    private final class ConnectionImpl extends AbstractAsynchronousConnection implements ConnectionEventListener {
+        /** The wrapped connection. */
+        private final LDAPConnectionImpl connectionImpl;
+
+        /** List of pending Bind or StartTLS requests which must be invoked once the current heart beat completes. */
+        private final Queue<Runnable> pendingBindOrStartTLSRequests = new ConcurrentLinkedQueue<>();
+
+        /**
+         * List of pending responses for all active operations. These will be signaled if no heart beat is detected
+         * within the permitted timeout period.
+         */
+        private final Queue<LdapResultHandler<?>> pendingResults = new ConcurrentLinkedQueue<>();
+
+        /** Internal connection state. */
+        private final ConnectionState state = new ConnectionState();
+
+        /** Coordinates heart-beats with Bind and StartTLS requests. */
+        private final Sync sync = new Sync();
+
+        /** Timestamp of last response received (any response, not just heart beats). */
+        private volatile long lastResponseTimestamp = timeService.now();
+
+        private ConnectionImpl(final LDAPConnectionImpl connectionImpl) {
+            this.connectionImpl = connectionImpl;
+            connectionImpl.addConnectionEventListener(this);
+        }
+
+        @Override
+        public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+            return connectionImpl.abandonAsync(request);
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(
+                final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.addAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public void addConnectionEventListener(final ConnectionEventListener listener) {
+            state.addConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(
+                final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            if (sync.tryLockShared()) {
+                // Fast path
+                return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request, intermediateResponseHandler));
+            }
+            return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, BindResult, LdapException>() {
+                @Override
+                public Promise<BindResult, LdapException> apply(Void value) throws LdapException {
+                    return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request,
+                                                                                   intermediateResponseHandler));
+                }
+            });
+        }
+
+        @Override
+        public void close() {
+            handleConnectionClosed();
+            connectionImpl.close();
+        }
+
+        @Override
+        public void close(final UnbindRequest request, final String reason) {
+            handleConnectionClosed();
+            connectionImpl.close(request, reason);
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(
+                final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.compareAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(
+                final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.deleteAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(
+                final ExtendedRequest<R> request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            if (!isStartTLSRequest(request)) {
+                return timestampPromise(connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+            }
+            if (sync.tryLockShared()) {
+                // Fast path
+                return timestampBindOrStartTLSPromise(
+                        connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+            }
+            return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, R, LdapException>() {
+                @Override
+                public Promise<R, LdapException> apply(Void value) throws LdapException {
+                    return timestampBindOrStartTLSPromise(
+                            connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+                }
+            });
+        }
+
+        @Override
+        public void handleConnectionClosed() {
+            if (state.notifyConnectionClosed()) {
+                failPendingResults(newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED,
+                                                    HBCF_CONNECTION_CLOSED_BY_CLIENT.get()));
+                synchronized (validConnections) {
+                    connectionImpl.removeConnectionEventListener(this);
+                    validConnections.remove(this);
+                    if (heartBeatEnabled && validConnections.isEmpty()) {
+                        // This is the last active connection, so stop the heartbeat.
+                        heartBeatFuture.cancel(false);
+                    }
+                }
+                releaseScheduler();
+            }
+        }
+
+        @Override
+        public void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) {
+            if (state.notifyConnectionError(isDisconnectNotification, error)) {
+                failPendingResults(error);
+            }
+        }
+
+        @Override
+        public void handleUnsolicitedNotification(final ExtendedResult notification) {
+            timestamp(notification);
+            state.notifyUnsolicitedNotification(notification);
+        }
+
+        @Override
+        public boolean isClosed() {
+            return state.isClosed();
+        }
+
+        @Override
+        public boolean isValid() {
+            return state.isValid() && connectionImpl.isValid();
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(
+                final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.modifyAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(
+                final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.modifyDNAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public void removeConnectionEventListener(final ConnectionEventListener listener) {
+            state.removeConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(
+                final SearchRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final SearchResultHandler searchHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+
+            final AtomicBoolean searchDone = new AtomicBoolean();
+            final SearchResultHandler entryHandler = new SearchResultHandler() {
+                @Override
+                public synchronized boolean handleEntry(SearchResultEntry entry) {
+                    if (!searchDone.get()) {
+                        timestamp(entry);
+                        if (searchHandler != null) {
+                            searchHandler.handleEntry(entry);
+                        }
+                    }
+                    return true;
+                }
+
+                @Override
+                public synchronized boolean handleReference(SearchResultReference reference) {
+                    if (!searchDone.get()) {
+                        timestamp(reference);
+                        if (searchHandler != null) {
+                            searchHandler.handleReference(reference);
+                        }
+                    }
+                    return true;
+                }
+            };
+            return timestampPromise(connectionImpl.searchAsync(request, intermediateResponseHandler, entryHandler)
+                                                  .thenOnResultOrException(new Runnable() {
+                                                      @Override
+                                                      public void run() {
+                                                          searchDone.getAndSet(true);
+                                                      }
+                                                  }));
+        }
+
+        @Override
+        public String toString() {
+            return connectionImpl.toString();
+        }
+
+        private void checkForHeartBeat() {
+            if (sync.isHeld()) {
+                /*
+                 * A heart beat or bind/startTLS is still in progress, but it should have completed by now. Let's
+                 * avoid aggressively terminating the connection, because the heart beat may simply have been delayed
+                 * by a sudden surge of activity. Therefore, only flag the connection as failed if no activity has been
+                 * seen on the connection since the heart beat was sent.
+                 */
+                final long currentTimeMillis = timeService.now();
+                if (lastResponseTimestamp < (currentTimeMillis - heartBeatTimeoutMS)) {
+                    logger.warn(LocalizableMessage.raw("No heartbeat detected for connection '%s'", connectionImpl));
+                    handleConnectionError(false, newHeartBeatTimeoutError());
+                }
+            }
+        }
+
+        private boolean hasConnectionErrorOccurred() {
+            return state.getConnectionError() != null;
+        }
+
+        private <R extends Result> LdapPromise<R> enqueueBindOrStartTLSPromise(
+                AsyncFunction<Void, R, LdapException> doRequest) {
+            /*
+             * A heart beat must be in progress so create a runnable task which will be executed when the heart beat
+             * completes.
+             */
+            final LdapPromiseImpl<Void> promise = newLdapPromiseImpl();
+            final LdapPromise<R> result = promise.thenAsync(doRequest);
+
+            // Enqueue and flush if the heart beat has completed in the mean time.
+            pendingBindOrStartTLSRequests.offer(new Runnable() {
+                @Override
+                public void run() {
+                    // FIXME: Handle cancel chaining.
+                    if (!result.isCancelled()) {
+                        sync.lockShared(); // Will not block.
+                        promise.handleResult(null);
+                    }
+                }
+            });
+            flushPendingBindOrStartTLSRequests();
+            return result;
+        }
+
+        private void failPendingResults(final LdapException error) {
+            // Peek instead of pool because notification is responsible for removing the element from the queue.
+            LdapResultHandler<?> pendingResult;
+            while ((pendingResult = pendingResults.peek()) != null) {
+                pendingResult.handleException(error);
+            }
+        }
+
+        private void flushPendingBindOrStartTLSRequests() {
+            if (!pendingBindOrStartTLSRequests.isEmpty()) {
+                /*
+                 * The pending requests will acquire the shared lock, but we take it here anyway to ensure that
+                 * pending requests do not get blocked.
+                 */
+                if (sync.tryLockShared()) {
+                    try {
+                        Runnable pendingRequest;
+                        while ((pendingRequest = pendingBindOrStartTLSRequests.poll()) != null) {
+                            // Dispatch the waiting request. This will not block.
+                            pendingRequest.run();
+                        }
+                    } finally {
+                        sync.unlockShared();
+                    }
+                }
+            }
+        }
+
+        private boolean isStartTLSRequest(final ExtendedRequest<?> request) {
+            return request.getOID().equals(StartTLSExtendedRequest.OID);
+        }
+
+        private <R> LdapPromise<R> newConnectionErrorPromise() {
+            return newFailedLdapPromise(state.getConnectionError());
+        }
+
+        private void releaseBindOrStartTLSLock() {
+            sync.unlockShared();
+        }
+
+        private void releaseHeartBeatLock() {
+            sync.unlockExclusively();
+            flushPendingBindOrStartTLSRequests();
+        }
+
+        /**
+         * Sends a heart beat on this connection if required to do so.
+         *
+         * @return {@code true} if a heart beat was sent, otherwise {@code false}.
+         */
+        private boolean sendHeartBeat() {
+            // Don't attempt to send a heart beat if the connection has already failed.
+            if (!state.isValid()) {
+                return false;
+            }
+
+            // Only send the heart beat if the connection has been idle for some time.
+            final long currentTimeMillis = timeService.now();
+            if (currentTimeMillis < (lastResponseTimestamp + heartBeatDelayMS)) {
+                return false;
+            }
+
+            /* Don't send a heart beat if there is already a heart beat, bind, or startTLS in progress. Note that the
+             * bind/startTLS response will update the lastResponseTimestamp as if it were a heart beat.
+             */
+            if (sync.tryLockExclusively()) {
+                try {
+                    connectionImpl.searchAsync(heartBeatRequest, null, new SearchResultHandler() {
+                        @Override
+                        public boolean handleEntry(final SearchResultEntry entry) {
+                            timestamp(entry);
+                            return true;
+                        }
+
+                        @Override
+                        public boolean handleReference(final SearchResultReference reference) {
+                            timestamp(reference);
+                            return true;
+                        }
+                    }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
+                        @Override
+                        public void handleResult(Result result) {
+                            timestamp(result);
+                            releaseHeartBeatLock();
+                        }
+                    }).thenOnException(new ExceptionHandler<LdapException>() {
+                        @Override
+                        public void handleException(LdapException exception) {
+                            /*
+                             * Connection failure will be handled by connection event listener. Ignore cancellation
+                             * errors since these indicate that the heart beat was aborted by a client-side close.
+                             */
+                            if (!(exception instanceof CancelledResultException)) {
+                                /*
+                                 * Log at debug level to avoid polluting the logs with benign password policy related
+                                 * errors. See OPENDJ-1168 and OPENDJ-1167.
+                                 */
+                                logger.debug(LocalizableMessage.raw("Heartbeat failed for connection factory '%s'",
+                                                                    LDAPConnectionFactory.this,
+                                                                    exception));
+                                timestamp(exception);
+                            }
+                            releaseHeartBeatLock();
+                        }
+                    });
+                } catch (final IllegalStateException e) {
+                    /*
+                     * This may happen when we attempt to send the heart beat just after the connection is closed but
+                     * before we are notified. Release the lock because we're never going to get a response.
+                     */
+                    releaseHeartBeatLock();
+                }
+            }
+            /*
+             * Indicate that a the heartbeat should be checked even if a bind/startTLS is in progress, since these
+             * operations will effectively act as the heartbeat.
+             */
+            return true;
+        }
+
+        private <R> R timestamp(final R response) {
+            if (!(response instanceof ConnectionException)) {
+                lastResponseTimestamp = timeService.now();
+            }
+            return response;
+        }
+
+        private <R extends Result> LdapPromise<R> timestampBindOrStartTLSPromise(LdapPromise<R> wrappedPromise) {
+            return timestampPromise(wrappedPromise).thenOnResultOrException(new Runnable() {
+                @Override
+                public void run() {
+                    releaseBindOrStartTLSLock();
+                }
+            });
+        }
+
+        private <R extends Result> LdapPromise<R> timestampPromise(LdapPromise<R> wrappedPromise) {
+            final LdapPromiseImpl<R> outerPromise = new LdapPromiseImplWrapper<>(wrappedPromise);
+            pendingResults.add(outerPromise);
+            wrappedPromise.thenOnResult(new ResultHandler<R>() {
+                @Override
+                public void handleResult(R result) {
+                    outerPromise.handleResult(result);
+                    timestamp(result);
+                }
+            }).thenOnException(new ExceptionHandler<LdapException>() {
+                @Override
+                public void handleException(LdapException exception) {
+                    outerPromise.handleException(exception);
+                    timestamp(exception);
+                }
+            });
+            outerPromise.thenOnResultOrException(new Runnable() {
+                @Override
+                public void run() {
+                    pendingResults.remove(outerPromise);
+                }
+            });
+            if (hasConnectionErrorOccurred()) {
+                outerPromise.handleException(state.getConnectionError());
+            }
+            return outerPromise;
+        }
+
+        private class LdapPromiseImplWrapper<R> extends LdapPromiseImpl<R> {
+            protected LdapPromiseImplWrapper(final LdapPromise<R> wrappedPromise) {
+                super(new PromiseImpl<R, LdapException>() {
+                    @Override
+                    protected LdapException tryCancel(boolean mayInterruptIfRunning) {
+                        /*
+                         * FIXME: if the inner cancel succeeds then this promise will be completed and we can never
+                         * indicate that this cancel request has succeeded.
+                         */
+                        wrappedPromise.cancel(mayInterruptIfRunning);
+                        return null;
+                    }
+                }, wrappedPromise.getRequestID());
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java
new file mode 100644
index 0000000..48ff6bf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java
@@ -0,0 +1,309 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.spi.LDAPListenerImpl;
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+
+/**
+ * An LDAP server connection listener which waits for LDAP connection requests
+ * to come in over the network and binds them to a {@link ServerConnection}
+ * created using the provided {@link ServerConnectionFactory}.
+ * <p>
+ * When processing requests, {@code ServerConnection} implementations are passed
+ * an integer as the first parameter. This integer represents the
+ * {@code requestID} associated with the client request and corresponds to the
+ * {@code requestID} passed as a parameter to abandon and cancel extended
+ * requests. The request ID may also be useful for logging purposes.
+ * <p>
+ * An {@code LDAPListener} does not require {@code ServerConnection}
+ * implementations to return a result when processing requests. More
+ * specifically, an {@code LDAPListener} does not maintain any internal state
+ * information associated with each request which must be released. This is
+ * useful when implementing LDAP abandon operations which may prevent results
+ * being sent for abandoned operations.
+ * <p>
+ * The following code illustrates how to create a simple LDAP server:
+ *
+ * <pre>
+ * class MyClientConnection implements ServerConnection&lt;Integer&gt; {
+ *     private final LDAPClientContext clientContext;
+ *
+ *     private MyClientConnection(LDAPClientContext clientContext) {
+ *         this.clientContext = clientContext;
+ *     }
+ *
+ *     public void add(Integer requestID, AddRequest request, ResultHandler&lt;Result&gt; handler,
+ *             IntermediateResponseHandler intermediateResponseHandler)
+ *             throws UnsupportedOperationException {
+ *         // ...
+ *     }
+ *
+ *     // ...
+ *
+ * }
+ *
+ * class MyServer implements ServerConnectionFactory&lt;LDAPClientContext, RequestContext&gt; {
+ *     public ServerConnection&lt;RequestContext&gt; accept(LDAPClientContext context) {
+ *         System.out.println(&quot;Connection from: &quot; + context.getPeerAddress());
+ *         return new MyClientConnection(context);
+ *     }
+ * }
+ *
+ * public static void main(String[] args) throws Exception {
+ *     LDAPListener listener = new LDAPListener(1389, new MyServer());
+ *
+ *     // ...
+ *
+ *     listener.close();
+ * }
+ * </pre>
+ */
+public final class LDAPListener extends CommonLDAPOptions implements Closeable {
+
+    /**
+     * Specifies the maximum queue length for incoming connections requests. If a
+     * connection request arrives when the queue is full, the connection is refused.
+     */
+    public static final Option<Integer> CONNECT_MAX_BACKLOG = Option.withDefault(50);
+
+    /**
+     * Specifies the maximum request size in bytes for incoming LDAP requests.
+     * If an incoming request exceeds the limit then the connection will be aborted by the listener.
+     * Default value is 5MiB.
+     */
+    public static final Option<Integer> REQUEST_MAX_SIZE_IN_BYTES = Option.withDefault(5 * 1024 * 1024);
+
+    /**
+     * We implement the factory using the pimpl idiom in order have
+     * cleaner Javadoc which does not expose implementation methods.
+     */
+    private final LDAPListenerImpl impl;
+
+    /** Transport provider that provides the implementation of this listener. */
+    private TransportProvider provider;
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param port
+     *            The port to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {code factory} was {@code null}.
+     */
+    public LDAPListener(final int port,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory) throws IOException {
+        this(port, factory, Options.defaultOptions());
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param port
+     *            The port to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {code factory} or {@code options} was {@code null}.
+     */
+    public LDAPListener(final int port,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options) throws IOException {
+        Reject.ifNull(factory, options);
+        this.provider = getTransportProvider(options);
+        this.impl = provider.getLDAPListener(new InetSocketAddress(port), factory, options);
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {@code address} or {code factory} was {@code null}.
+     */
+    public LDAPListener(final InetSocketAddress address,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory) throws IOException {
+        this(address, factory, Options.defaultOptions());
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {@code address}, {code factory}, or {@code options} was
+     *             {@code null}.
+     */
+    public LDAPListener(final InetSocketAddress address,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options) throws IOException {
+        Reject.ifNull(address, factory, options);
+        this.provider = getTransportProvider(options);
+        this.impl = provider.getLDAPListener(address, factory, options);
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param host
+     *            The address to listen on.
+     * @param port
+     *            The port to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {@code host} or {code factory} was {@code null}.
+     */
+    public LDAPListener(final String host, final int port,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory) throws IOException {
+        this(host, port, factory, Options.defaultOptions());
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections at the provided address.
+     *
+     * @param host
+     *            The address to listen on.
+     * @param port
+     *            The port to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     * @throws NullPointerException
+     *             If {@code host}, {code factory}, or {@code options} was
+     *             {@code null}.
+     */
+    public LDAPListener(final String host, final int port,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options) throws IOException {
+        Reject.ifNull(host, factory, options);
+        final InetSocketAddress address = new InetSocketAddress(host, port);
+        this.provider = getTransportProvider(options);
+        this.impl = provider.getLDAPListener(address, factory, options);
+    }
+
+    /** Closes this LDAP connection listener. */
+    @Override
+    public void close() {
+        impl.close();
+    }
+
+    /**
+     * Returns the {@code InetAddress} that this LDAP listener is listening on.
+     *
+     * @return The {@code InetAddress} that this LDAP listener is listening on.
+     */
+    public InetAddress getAddress() {
+        return getSocketAddress().getAddress();
+    }
+
+    /**
+     * Returns the host name that this LDAP listener is listening on. The
+     * returned host name is the same host name that was provided during
+     * construction and may be an IP address. More specifically, this method
+     * will not perform a reverse DNS lookup.
+     *
+     * @return The host name that this LDAP listener is listening on.
+     */
+    public String getHostName() {
+        return Connections.getHostString(getSocketAddress());
+    }
+
+    /**
+     * Returns the port that this LDAP listener is listening on.
+     *
+     * @return The port that this LDAP listener is listening on.
+     */
+    public int getPort() {
+        return getSocketAddress().getPort();
+    }
+
+    /**
+     * Returns the address that this LDAP listener is listening on.
+     *
+     * @return The address that this LDAP listener is listening on.
+     */
+    public InetSocketAddress getSocketAddress() {
+        return impl.getSocketAddress();
+    }
+
+    /**
+     * Returns the name of the transport provider, which provides the implementation
+     * of this factory.
+     *
+     * @return The name of actual transport provider.
+     */
+    public String getProviderName() {
+        return provider.getName();
+    }
+
+    @Override
+    public String toString() {
+        return impl.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java
new file mode 100644
index 0000000..f30d9a0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java
@@ -0,0 +1,743 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * An LDAP URL as defined in RFC 4516. In addition, the secure ldap (ldaps://)
+ * is also supported. LDAP URLs have the following format:
+ *
+ * <PRE>
+ * "ldap[s]://" [ <I>hostName</I> [":" <I>portNumber</I>] ]
+ *          "/" <I>distinguishedName</I>
+ *          ["?" <I>attributeList</I>
+ *              ["?" <I>scope</I> "?" <I>filterString</I> ] ]
+ * </PRE>
+ *
+ * Where:
+ * <UL>
+ * <LI>all text within double-quotes are literal
+ * <LI><CODE><I>hostName</I></CODE> and <CODE><I>portNumber</I></CODE> identify
+ * the location of the LDAP server.
+ * <LI><CODE><I>distinguishedName</I></CODE> is the name of an entry within the
+ * given directory (the entry represents the starting point of the search).
+ * <LI><CODE><I>attributeList</I></CODE> contains a list of attributes to
+ * retrieve (if null, fetch all attributes). This is a comma-delimited list of
+ * attribute names.
+ * <LI><CODE><I>scope</I></CODE> is one of the following:
+ * <UL>
+ * <LI><CODE>base</CODE> indicates that this is a search only for the specified
+ * entry
+ * <LI><CODE>one</CODE> indicates that this is a search for matching entries one
+ * level under the specified entry (and not including the entry itself)
+ * <LI><CODE>sub</CODE> indicates that this is a search for matching entries at
+ * all levels under the specified entry (including the entry itself)
+ * <LI><CODE>subordinates</CODE> indicates that this is a search for matching
+ * entries all levels under the specified entry (excluding the entry itself)
+ * </UL>
+ * If not specified, <CODE><I>scope</I></CODE> is <CODE>base</CODE> by default.
+ * <LI><CODE><I>filterString</I></CODE> is a human-readable representation of
+ * the search criteria. If no filter is provided, then a default of "
+ * {@code (objectClass=*)}" should be assumed.
+ * </UL>
+ * The same encoding rules for other URLs (e.g. HTTP) apply for LDAP URLs.
+ * Specifically, any "illegal" characters are escaped with
+ * <CODE>%<I>HH</I></CODE>, where <CODE><I>HH</I></CODE> represent the two hex
+ * digits which correspond to the ASCII value of the character. This encoding is
+ * only legal (or necessary) on the DN and filter portions of the URL.
+ * <P>
+ * Note that this class does not implement extensions.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc4516">RFC 4516 - Lightweight
+ *      Directory Access Protocol (LDAP): Uniform Resource Locator</a>
+ */
+public final class LDAPUrl {
+    /**
+     * The scheme corresponding to an LDAP URL. RFC 4516 mandates only ldap
+     * scheme but we support "ldaps" too.
+     */
+    private final boolean isSecured;
+
+    /** The host name corresponding to an LDAP URL. */
+    private final String host;
+
+    /** The port number corresponding to an LDAP URL. */
+    private final int port;
+
+    /** The distinguished name corresponding to an LDAP URL. */
+    private final DN name;
+
+    /** The search scope corresponding to an LDAP URL. */
+    private final SearchScope scope;
+
+    /** The search filter corresponding to an LDAP URL. */
+    private final Filter filter;
+
+    /** The attributes that need to be searched. */
+    private final List<String> attributes;
+
+    /** The String value of LDAP URL. */
+    private final String urlString;
+
+    /** Normalized ldap URL. */
+    private String normalizedURL;
+
+    /** The default scheme to be used with LDAP URL. */
+    private static final String DEFAULT_URL_SCHEME = "ldap";
+
+    /** The SSL-based scheme allowed to be used with LDAP URL. */
+    private static final String SSL_URL_SCHEME = "ldaps";
+
+    /** The default host. */
+    private static final String DEFAULT_HOST = "localhost";
+
+    /** The default non-SSL port. */
+    private static final int DEFAULT_PORT = 389;
+
+    /** The default SSL port. */
+    private static final int DEFAULT_SSL_PORT = 636;
+
+    /** The default filter. */
+    private static final Filter DEFAULT_FILTER = Filter.objectClassPresent();
+
+    /** The default search scope. */
+    private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE_OBJECT;
+
+    /** The default distinguished name. */
+    private static final DN DEFAULT_DN = DN.rootDN();
+
+    /** The % encoding character. */
+    private static final char PERCENT_ENCODING_CHAR = '%';
+
+    /** The ? character. */
+    private static final char QUESTION_CHAR = '?';
+
+    /** The slash (/) character. */
+    private static final char SLASH_CHAR = '/';
+
+    /** The comma (,) character. */
+    private static final char COMMA_CHAR = ',';
+
+    /** The colon (:) character. */
+    private static final char COLON_CHAR = ':';
+
+    /** Set containing characters that do not need to be encoded. */
+    private static final Set<Character> VALID_CHARS = new HashSet<>();
+
+    static {
+        // Refer to RFC 3986 for more details.
+        final char[] delims = {
+            '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '.', '-', '_', '~'
+        };
+        for (final char c : delims) {
+            VALID_CHARS.add(c);
+        }
+
+        for (char c = 'a'; c <= 'z'; c++) {
+            VALID_CHARS.add(c);
+        }
+
+        for (char c = 'A'; c <= 'Z'; c++) {
+            VALID_CHARS.add(c);
+        }
+
+        for (char c = '0'; c <= '9'; c++) {
+            VALID_CHARS.add(c);
+        }
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an LDAP URL using the
+     * default schema.
+     *
+     * @param url
+     *            The LDAP string representation of an LDAP URL.
+     * @return The parsed LDAP URL.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code url} is not a valid LDAP string representation of
+     *             an LDAP URL.
+     * @throws NullPointerException
+     *             If {@code url} was {@code null}.
+     */
+    public static LDAPUrl valueOf(final String url) {
+        return valueOf(url, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an LDAP URL using the
+     * provided schema.
+     *
+     * @param url
+     *            The LDAP string representation of an LDAP URL.
+     * @param schema
+     *            The schema to use when parsing the LDAP URL.
+     * @return The parsed LDAP URL.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code url} is not a valid LDAP string representation of
+     *             an LDAP URL.
+     * @throws NullPointerException
+     *             If {@code url} or {@code schema} was {@code null}.
+     */
+    public static LDAPUrl valueOf(final String url, final Schema schema) {
+        Reject.ifNull(url, schema);
+        return new LDAPUrl(url, schema);
+    }
+
+    private static int decodeHex(final String url, final int index, final char hexChar) {
+        if (hexChar >= '0' && hexChar <= '9') {
+            return hexChar - '0';
+        } else if (hexChar >= 'A' && hexChar <= 'F') {
+            return hexChar - 'A' + 10;
+        } else if (hexChar >= 'a' && hexChar <= 'f') {
+            return hexChar - 'a' + 10;
+        }
+
+        final LocalizableMessage msg = ERR_LDAPURL_INVALID_HEX_BYTE.get(url, index);
+        throw new LocalizedIllegalArgumentException(msg);
+    }
+
+    private static void percentDecoder(final String urlString, final int index, final String s,
+            final StringBuilder decoded) {
+        Reject.ifNull(s);
+        Reject.ifNull(decoded);
+        decoded.append(s);
+
+        int srcPos = 0, dstPos = 0;
+
+        while (srcPos < decoded.length()) {
+            if (decoded.charAt(srcPos) != '%') {
+                if (srcPos != dstPos) {
+                    decoded.setCharAt(dstPos, decoded.charAt(srcPos));
+                }
+                srcPos++;
+                dstPos++;
+                continue;
+            }
+            int i = decodeHex(urlString, index + srcPos + 1, decoded.charAt(srcPos + 1)) << 4;
+            int j = decodeHex(urlString, index + srcPos + 2, decoded.charAt(srcPos + 2));
+            decoded.setCharAt(dstPos, (char) (i | j));
+            dstPos++;
+            srcPos += 3;
+        }
+        decoded.setLength(dstPos);
+    }
+
+    /**
+     * This method performs the percent-encoding as defined in section 2.1 of
+     * RFC 3986.
+     *
+     * @param urlElement
+     *            The element of the URL that needs to be percent encoded.
+     * @param encodedBuffer
+     *            The buffer that contains the final percent encoded value.
+     */
+    private static void percentEncoder(final String urlElement, final StringBuilder encodedBuffer) {
+        Reject.ifNull(urlElement);
+        for (int count = 0; count < urlElement.length(); count++) {
+            final char c = urlElement.charAt(count);
+            if (VALID_CHARS.contains(c)) {
+                encodedBuffer.append(c);
+            } else {
+                encodedBuffer.append(PERCENT_ENCODING_CHAR);
+                encodedBuffer.append(Integer.toHexString(c));
+            }
+        }
+    }
+
+    /**
+     * Creates a new LDAP URL referring to a single entry on the specified
+     * server. The LDAP URL with have base object scope and the filter
+     * {@code (objectClass=*)}.
+     *
+     * @param isSecured
+     *            {@code true} if this LDAP URL should use LDAPS or
+     *            {@code false} if it should use LDAP.
+     * @param host
+     *            The name or IP address in dotted format of the LDAP server.
+     *            For example, {@code ldap.server1.com} or
+     *            {@code 192.202.185.90}. Use {@code null} for the local host.
+     * @param port
+     *            The port number of the LDAP server, or {@code null} to use the
+     *            default port (389 for LDAP and 636 for LDAPS).
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed, or {@code null} to specify the root
+     *            DSE.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code port} was less than 1 or greater than 65535.
+     */
+    public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name) {
+        this(isSecured, host, port, name, DEFAULT_SCOPE, DEFAULT_FILTER);
+    }
+
+    /**
+     * Creates a new LDAP URL including the full set of parameters for a search
+     * request.
+     *
+     * @param isSecured
+     *            {@code true} if this LDAP URL should use LDAPS or
+     *            {@code false} if it should use LDAP.
+     * @param host
+     *            The name or IP address in dotted format of the LDAP server.
+     *            For example, {@code ldap.server1.com} or
+     *            {@code 192.202.185.90}. Use {@code null} for the local host.
+     * @param port
+     *            The port number of the LDAP server, or {@code null} to use the
+     *            default port (389 for LDAP and 636 for LDAPS).
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed, or {@code null} to specify the root
+     *            DSE.
+     * @param scope
+     *            The search scope, or {@code null} to specify base scope.
+     * @param filter
+     *            The search filter, or {@code null} to specify the filter
+     *            {@code (objectClass=*)}.
+     * @param attributes
+     *            The list of attributes to be included in the search results.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code port} was less than 1 or greater than 65535.
+     */
+    public LDAPUrl(final boolean isSecured, final String host, final Integer port, final DN name,
+            final SearchScope scope, final Filter filter, final String... attributes) {
+        // The buffer storing the encoded url.
+        final StringBuilder urlBuffer = new StringBuilder();
+
+        // build the scheme.
+        this.isSecured = isSecured;
+        if (this.isSecured) {
+            urlBuffer.append(SSL_URL_SCHEME);
+        } else {
+            urlBuffer.append(DEFAULT_URL_SCHEME);
+        }
+        urlBuffer.append("://");
+
+        if (host == null) {
+            this.host = DEFAULT_HOST;
+        } else {
+            this.host = host;
+            urlBuffer.append(this.host);
+        }
+
+        int listenPort = DEFAULT_PORT;
+        if (port == null) {
+            listenPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT;
+        } else {
+            listenPort = port.intValue();
+            if (listenPort < 1 || listenPort > 65535) {
+                final LocalizableMessage msg = ERR_LDAPURL_BAD_PORT.get(listenPort);
+                throw new LocalizedIllegalArgumentException(msg);
+            }
+            urlBuffer.append(COLON_CHAR);
+            urlBuffer.append(listenPort);
+        }
+
+        this.port = listenPort;
+
+        // We need a slash irrespective of dn is defined or not.
+        urlBuffer.append(SLASH_CHAR);
+        if (name != null) {
+            this.name = name;
+            percentEncoder(name.toString(), urlBuffer);
+        } else {
+            this.name = DEFAULT_DN;
+        }
+
+        // Add attributes.
+        urlBuffer.append(QUESTION_CHAR);
+        switch (attributes.length) {
+        case 0:
+            this.attributes = Collections.emptyList();
+            break;
+        case 1:
+            this.attributes = Collections.singletonList(attributes[0]);
+            urlBuffer.append(attributes[0]);
+            break;
+        default:
+            this.attributes = Collections.unmodifiableList(Arrays.asList(attributes));
+            urlBuffer.append(attributes[0]);
+            for (int i = 1; i < attributes.length; i++) {
+                urlBuffer.append(COMMA_CHAR);
+                urlBuffer.append(attributes[i]);
+            }
+            break;
+        }
+
+        // Add the scope.
+        urlBuffer.append(QUESTION_CHAR);
+        if (scope != null) {
+            this.scope = scope;
+            urlBuffer.append(scope);
+        } else {
+            this.scope = DEFAULT_SCOPE;
+        }
+
+        // Add the search filter.
+        urlBuffer.append(QUESTION_CHAR);
+        if (filter != null) {
+            this.filter = filter;
+            urlBuffer.append(this.filter);
+        } else {
+            this.filter = DEFAULT_FILTER;
+        }
+
+        urlString = urlBuffer.toString();
+    }
+
+    private LDAPUrl(final String urlString, final Schema schema) {
+        this.urlString = urlString;
+
+        // Parse the url and build the LDAP URL.
+        final int schemeIdx = urlString.indexOf("://");
+        if (schemeIdx < 0) {
+            throw new LocalizedIllegalArgumentException(ERR_LDAPURL_NO_SCHEME.get(urlString));
+        }
+
+        final String scheme = StaticUtils.toLowerCase(urlString.substring(0, schemeIdx));
+        if (DEFAULT_URL_SCHEME.equalsIgnoreCase(scheme)) {
+            // Default ldap scheme.
+            isSecured = false;
+        } else if (SSL_URL_SCHEME.equalsIgnoreCase(scheme)) {
+            isSecured = true;
+        } else {
+            throw new LocalizedIllegalArgumentException(ERR_LDAPURL_BAD_SCHEME.get(urlString, scheme));
+        }
+
+        final int urlLength = urlString.length();
+        final int hostPortIdx = urlString.indexOf(SLASH_CHAR, schemeIdx + 3);
+        final StringBuilder builder = new StringBuilder();
+        if (hostPortIdx < 0) {
+            // We got anything here like the host and port?
+            if (urlLength > schemeIdx + 3) {
+                final String hostAndPort = urlString.substring(schemeIdx + 3, urlLength);
+                port = parseHostPort(urlString, hostAndPort, builder);
+                host = builder.toString();
+                builder.setLength(0);
+            } else {
+                // Nothing else is specified apart from the scheme.
+                // Use the default settings and return from here.
+                host = DEFAULT_HOST;
+                port = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT;
+            }
+            name = DEFAULT_DN;
+            scope = DEFAULT_SCOPE;
+            filter = DEFAULT_FILTER;
+            attributes = Collections.emptyList();
+            return;
+        }
+
+        final String hostAndPort = urlString.substring(schemeIdx + 3, hostPortIdx);
+        // assign the host and port.
+        port = parseHostPort(urlString, hostAndPort, builder);
+        host = builder.toString();
+        builder.setLength(0);
+
+        // Parse the dn.
+        DN parsedDN = null;
+        final int dnIdx = urlString.indexOf(QUESTION_CHAR, hostPortIdx + 1);
+
+        if (dnIdx < 0) {
+            // Whatever we have here is the dn.
+            final String dnStr = urlString.substring(hostPortIdx + 1, urlLength);
+            percentDecoder(urlString, hostPortIdx + 1, dnStr, builder);
+            try {
+                parsedDN = DN.valueOf(builder.toString(), schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                final LocalizableMessage msg =
+                        ERR_LDAPURL_INVALID_DN.get(urlString, e.getMessageObject());
+                throw new LocalizedIllegalArgumentException(msg);
+            }
+            builder.setLength(0);
+            name = parsedDN;
+            scope = DEFAULT_SCOPE;
+            filter = DEFAULT_FILTER;
+            attributes = Collections.emptyList();
+            return;
+        }
+
+        final String dnStr = urlString.substring(hostPortIdx + 1, dnIdx);
+        if (dnStr.length() == 0) {
+            parsedDN = DEFAULT_DN;
+        } else {
+            percentDecoder(urlString, hostPortIdx + 1, dnStr, builder);
+            try {
+                parsedDN = DN.valueOf(builder.toString(), schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                final LocalizableMessage msg =
+                        ERR_LDAPURL_INVALID_DN.get(urlString, e.getMessageObject());
+                throw new LocalizedIllegalArgumentException(msg);
+            }
+            builder.setLength(0);
+        }
+        name = parsedDN;
+
+        // Find out the attributes.
+        final int attrIdx = urlString.indexOf(QUESTION_CHAR, dnIdx + 1);
+        if (attrIdx < 0) {
+            attributes = Collections.emptyList();
+            scope = DEFAULT_SCOPE;
+            filter = DEFAULT_FILTER;
+            return;
+        }
+        attributes = parseAttributes(urlString.substring(dnIdx + 1, attrIdx));
+
+        // Find the scope.
+        final int scopeIdx = urlString.indexOf(QUESTION_CHAR, attrIdx + 1);
+        if (scopeIdx < 0) {
+            scope = DEFAULT_SCOPE;
+            filter = DEFAULT_FILTER;
+            return;
+        }
+        scope = parseScope(urlString.substring(attrIdx + 1, scopeIdx));
+
+        // Last one is filter.
+        final String parsedFilter = urlString.substring(scopeIdx + 1, urlLength);
+        if (parsedFilter.length() > 0) {
+            // Clear what we already have.
+            builder.setLength(0);
+            percentDecoder(urlString, scopeIdx + 1, parsedFilter, builder);
+            try {
+                this.filter = Filter.valueOf(builder.toString());
+            } catch (final LocalizedIllegalArgumentException e) {
+                final LocalizableMessage msg =
+                        ERR_LDAPURL_INVALID_FILTER.get(urlString, e.getMessageObject());
+                throw new LocalizedIllegalArgumentException(msg);
+            }
+        } else {
+            this.filter = DEFAULT_FILTER;
+        }
+    }
+
+    private List<String> parseAttributes(final String attrDesc) {
+        final StringTokenizer token = new StringTokenizer(attrDesc, String.valueOf(COMMA_CHAR));
+        final List<String> parsedAttrs = new ArrayList<>(token.countTokens());
+        while (token.hasMoreElements()) {
+            parsedAttrs.add(token.nextToken());
+        }
+        return Collections.unmodifiableList(parsedAttrs);
+    }
+
+    private SearchScope parseScope(String scopeDef) {
+        final String scope = toLowerCase(scopeDef);
+        for (final SearchScope sscope : SearchScope.values()) {
+            if (sscope.toString().equals(scope)) {
+                return sscope;
+            }
+        }
+        return SearchScope.BASE_OBJECT;
+    }
+
+    /**
+     * Creates a new search request containing the parameters of this LDAP URL.
+     *
+     * @return A new search request containing the parameters of this LDAP URL.
+     */
+    public SearchRequest asSearchRequest() {
+        final SearchRequest request = Requests.newSearchRequest(name, scope, filter);
+        for (final String a : attributes) {
+            request.addAttribute(a);
+        }
+        return request;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (o == this) {
+            return true;
+        } else if (o instanceof LDAPUrl) {
+            final String s1 = toNormalizedString();
+            final String s2 = ((LDAPUrl) o).toNormalizedString();
+            return s1.equals(s2);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an unmodifiable list containing the attributes to be included
+     * with each entry that matches the search criteria. Attributes that are
+     * sub-types of listed attributes are implicitly included. If the returned
+     * list is empty then all user attributes will be included by default.
+     *
+     * @return An unmodifiable list containing the attributes to be included
+     *         with each entry that matches the search criteria.
+     */
+    public List<String> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Returns the search filter associated with this LDAP URL.
+     *
+     * @return The search filter associated with this LDAP URL.
+     */
+    public Filter getFilter() {
+        return filter;
+    }
+
+    /**
+     * Returns the name or IP address in dotted format of the LDAP server
+     * referenced by this LDAP URL. For example, {@code ldap.server1.com} or
+     * {@code 192.202.185.90}. Use {@code null} for the local host.
+     *
+     * @return A name or IP address in dotted format of the LDAP server
+     *         referenced by this LDAP URL.
+     */
+    public String getHost() {
+        return host;
+    }
+
+    /**
+     * Returns the distinguished name of the base entry relative to which the
+     * search is to be performed.
+     *
+     * @return The distinguished name of the base entry relative to which the
+     *         search is to be performed.
+     */
+    public DN getName() {
+        return name;
+    }
+
+    /**
+     * Returns the port number of the LDAP server referenced by this LDAP URL.
+     *
+     * @return The port number of the LDAP server referenced by this LDAP URL.
+     */
+    public int getPort() {
+        return port;
+    }
+
+    /**
+     * Returns the search scope associated with this LDAP URL.
+     *
+     * @return The search scope associated with this LDAP URL.
+     */
+    public SearchScope getScope() {
+        return scope;
+    }
+
+    @Override
+    public int hashCode() {
+        final String s = toNormalizedString();
+        return s.hashCode();
+    }
+
+    /**
+     * Returns {@code true} if this LDAP URL should use LDAPS or {@code false}
+     * if it should use LDAP.
+     *
+     * @return {@code true} if this LDAP URL should use LDAPS or {@code false}
+     *         if it should use LDAP.
+     */
+    public boolean isSecure() {
+        return isSecured;
+    }
+
+    @Override
+    public String toString() {
+        return urlString;
+    }
+
+    private int parseHostPort(final String urlString, final String hostAndPort,
+            final StringBuilder host) {
+        Reject.ifNull(urlString);
+        Reject.ifNull(hostAndPort);
+        Reject.ifNull(host);
+        int urlPort = isSecured ? DEFAULT_SSL_PORT : DEFAULT_PORT;
+        if (hostAndPort.length() == 0) {
+            host.append(DEFAULT_HOST);
+            return urlPort;
+        }
+        final int colonIdx = hostAndPort.indexOf(':');
+        if (colonIdx < 0) {
+            // port is not specified.
+            host.append(hostAndPort);
+            return urlPort;
+        }
+
+        String s = hostAndPort.substring(0, colonIdx);
+        if (s.length() == 0) {
+            // Use the default host as we allow only the port to be
+            // specified.
+            host.append(DEFAULT_HOST);
+        } else {
+            host.append(s);
+        }
+        s = hostAndPort.substring(colonIdx + 1, hostAndPort.length());
+        try {
+            urlPort = Integer.parseInt(s);
+        } catch (final NumberFormatException e) {
+            throw new LocalizedIllegalArgumentException(ERR_LDAPURL_CANNOT_DECODE_PORT.get(urlString, s));
+        }
+
+        // Check the validity of the port.
+        if (urlPort < 1 || urlPort > 65535) {
+            throw new LocalizedIllegalArgumentException(ERR_LDAPURL_INVALID_PORT.get(urlString, urlPort));
+        }
+        return urlPort;
+    }
+
+    private String toNormalizedString() {
+        if (normalizedURL == null) {
+            final StringBuilder builder = new StringBuilder();
+            if (this.isSecured) {
+                builder.append(SSL_URL_SCHEME);
+            } else {
+                builder.append(DEFAULT_URL_SCHEME);
+            }
+            builder.append("://");
+            builder.append(host);
+            builder.append(COLON_CHAR);
+            builder.append(port);
+            builder.append(SLASH_CHAR);
+            percentEncoder(name.toString(), builder);
+            builder.append(QUESTION_CHAR);
+            final int sz = attributes.size();
+            for (int i = 0; i < sz; i++) {
+                if (i > 0) {
+                    builder.append(COMMA_CHAR);
+                }
+                builder.append(attributes.get(i));
+            }
+            builder.append(QUESTION_CHAR);
+            builder.append(scope);
+            builder.append(QUESTION_CHAR);
+            percentEncoder(filter.toString(), builder);
+            normalizedURL = builder.toString();
+        }
+        return normalizedURL;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapException.java
new file mode 100644
index 0000000..5dcb641
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapException.java
@@ -0,0 +1,213 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * was unsuccessful. This class can be sub-classed in order to implement
+ * application specific exceptions.
+ */
+@SuppressWarnings("serial")
+public class LdapException extends IOException {
+
+    /**
+     * Creates a new LDAP exception with the provided result code and an
+     * empty diagnostic message.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new LDAP exception.
+     * @throws IllegalArgumentException
+     *             If the provided result code does not represent a failure.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static LdapException newLdapException(ResultCode resultCode) {
+        return newLdapException(resultCode, null, null);
+    }
+
+    /**
+     * Creates a new LDAP exception with the provided result code and
+     * diagnostic message.
+     *
+     * @param resultCode
+     *            The result code.
+     * @param diagnosticMessage
+     *            The diagnostic message, which may be empty or {@code null}
+     *            indicating that none was provided.
+     * @return The new LDAP exception.
+     * @throws IllegalArgumentException
+     *             If the provided result code does not represent a failure.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static LdapException newLdapException(ResultCode resultCode,
+            CharSequence diagnosticMessage) {
+        return newLdapException(resultCode, diagnosticMessage, null);
+    }
+
+    /**
+     * Creates a new LDAP exception with the provided result code and
+     * cause. The diagnostic message will be taken from the cause, if provided.
+     *
+     * @param resultCode
+     *            The result code.
+     * @param cause
+     *            The throwable cause, which may be {@code null} indicating that
+     *            none was provided.
+     * @return The new LDAP exception.
+     * @throws IllegalArgumentException
+     *             If the provided result code does not represent a failure.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static LdapException newLdapException(ResultCode resultCode, Throwable cause) {
+        return newLdapException(resultCode, null, cause);
+    }
+
+    /**
+     * Creates a new LDAP exception with the provided result code,
+     * diagnostic message, and cause.
+     *
+     * @param resultCode
+     *            The result code.
+     * @param diagnosticMessage
+     *            The diagnostic message, which may be empty or {@code null}
+     *            indicating that none was provided.
+     * @param cause
+     *            The throwable cause, which may be {@code null} indicating that
+     *            none was provided.
+     * @return The new LDAP exception.
+     * @throws IllegalArgumentException
+     *             If the provided result code does not represent a failure.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static LdapException newLdapException(ResultCode resultCode,
+            CharSequence diagnosticMessage, Throwable cause) {
+        final Result result = Responses.newResult(resultCode);
+        if (diagnosticMessage != null) {
+            result.setDiagnosticMessage(diagnosticMessage.toString());
+        } else if (cause != null) {
+            result.setDiagnosticMessage(cause.getLocalizedMessage());
+        }
+        result.setCause(cause);
+        return newLdapException(result);
+    }
+
+    /**
+     * Creates a new LDAP exception using the provided result.
+     *
+     * @param result
+     *            The result whose result code indicates a failure.
+     * @return The LDAP exception wrapping the provided result.
+     * @throws IllegalArgumentException
+     *             If the provided result does not represent a failure.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static LdapException newLdapException(final Result result) {
+        if (!result.getResultCode().isExceptional()) {
+            throw new IllegalArgumentException("Attempted to wrap a successful result: " + result);
+        }
+
+        switch (result.getResultCode().asEnum()) {
+        case ASSERTION_FAILED:
+            return new AssertionFailureException(result);
+        case AUTH_METHOD_NOT_SUPPORTED:
+        case CLIENT_SIDE_AUTH_UNKNOWN:
+        case INAPPROPRIATE_AUTHENTICATION:
+        case INVALID_CREDENTIALS:
+            return new AuthenticationException(result);
+        case AUTHORIZATION_DENIED:
+        case CONFIDENTIALITY_REQUIRED:
+        case INSUFFICIENT_ACCESS_RIGHTS:
+        case STRONG_AUTH_REQUIRED:
+            return new AuthorizationException(result);
+        case CLIENT_SIDE_USER_CANCELLED:
+        case CANCELLED:
+            return new CancelledResultException(result);
+        case CLIENT_SIDE_SERVER_DOWN:
+        case CLIENT_SIDE_CONNECT_ERROR:
+        case CLIENT_SIDE_DECODING_ERROR:
+        case CLIENT_SIDE_ENCODING_ERROR:
+            return new ConnectionException(result);
+        case ATTRIBUTE_OR_VALUE_EXISTS:
+        case NO_SUCH_ATTRIBUTE:
+        case CONSTRAINT_VIOLATION:
+        case ENTRY_ALREADY_EXISTS:
+        case INVALID_ATTRIBUTE_SYNTAX:
+        case INVALID_DN_SYNTAX:
+        case NAMING_VIOLATION:
+        case NOT_ALLOWED_ON_NONLEAF:
+        case NOT_ALLOWED_ON_RDN:
+        case OBJECTCLASS_MODS_PROHIBITED:
+        case OBJECTCLASS_VIOLATION:
+        case UNDEFINED_ATTRIBUTE_TYPE:
+            return new ConstraintViolationException(result);
+        case REFERRAL:
+            return new ReferralException(result);
+        case NO_SUCH_OBJECT:
+        case CLIENT_SIDE_NO_RESULTS_RETURNED:
+            return new EntryNotFoundException(result);
+        case CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED:
+            return new MultipleEntriesFoundException(result);
+        case CLIENT_SIDE_TIMEOUT:
+        case TIME_LIMIT_EXCEEDED:
+            return new TimeoutResultException(result);
+        default:
+            return new LdapException(result);
+        }
+    }
+
+    private static String getMessage(final Result result) {
+        if (result.getDiagnosticMessage() == null || result.getDiagnosticMessage().isEmpty()) {
+            return result.getResultCode().toString();
+        } else {
+            return result.getResultCode() + ": " + result.getDiagnosticMessage();
+        }
+    }
+
+    private final Result result;
+
+    /**
+     * Creates a new LDAP exception using the provided result.
+     *
+     * @param result
+     *            The error result.
+     */
+    protected LdapException(final Result result) {
+        super(getMessage(result), result.getCause());
+        this.result = result;
+    }
+
+    /**
+     * Returns the error result which caused this exception to be thrown. The
+     * type of result returned corresponds to the expected result type of the
+     * original request.
+     *
+     * @return The error result which caused this exception to be thrown.
+     */
+    public final Result getResult() {
+        return result;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapPromise.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapPromise.java
new file mode 100644
index 0000000..800fa70
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapPromise.java
@@ -0,0 +1,66 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+/**
+ * A handle which can be used to retrieve the Result of an asynchronous Request.
+ *
+ * @param <S>
+ *            The type of result returned by this promise.
+ */
+public interface LdapPromise<S> extends Promise<S, LdapException> {
+    /**
+     * Returns the request ID of the request if appropriate.
+     *
+     * @return The request ID, or {@code -1} if there is no request ID.
+     */
+    int getRequestID();
+
+    @Override
+    LdapPromise<S> thenOnResult(ResultHandler<? super S> onResult);
+
+    @Override
+    LdapPromise<S> thenOnException(ExceptionHandler<? super LdapException> onException);
+
+    @Override
+    LdapPromise<S> thenOnResultOrException(Runnable onResultOrException);
+
+    @Override
+    // @Checkstyle:ignore
+    <VOUT> LdapPromise<VOUT> then(Function<? super S, VOUT, LdapException> onResult);
+
+    @Override
+    LdapPromise<S> thenOnResultOrException(ResultHandler<? super S> onResult,
+                                           ExceptionHandler<? super LdapException> onException);
+
+    @Override
+    LdapPromise<S> thenAlways(Runnable onResultOrException);
+
+    @Override
+    LdapPromise<S> thenFinally(Runnable onResultOrException);
+
+    @Override
+    // @Checkstyle:ignore
+    <VOUT> LdapPromise<VOUT> thenAsync(AsyncFunction<? super S, VOUT, LdapException> onResult);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapResultHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapResultHandler.java
new file mode 100644
index 0000000..415fcb0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LdapResultHandler.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.ResultHandler;
+
+/**
+ * A completion handler for consuming the result of an asynchronous operation or
+ * connection attempts.
+ * <p>
+ * A result completion handler may be specified when performing asynchronous
+ * operations using a {@link Connection} object or when connecting
+ * asynchronously to a remote Directory Server using an
+ * {@link ConnectionFactory}. The {@link #handleResult} method is invoked when
+ * the operation or connection attempt completes successfully. The
+ * {@link #handleException(LdapException)} method is invoked if the operation or connection
+ * attempt fails.
+ * <p>
+ * Implementations of these methods should complete in a timely manner so as to
+ * avoid keeping the invoking thread from dispatching to other completion
+ * handlers.
+ *
+ * @param <S>
+ *            The type of result handled by this result handler.
+ */
+public interface LdapResultHandler<S> extends ResultHandler<S>, ExceptionHandler<LdapException> {
+    /**
+     * Invoked when the asynchronous operation has failed.
+     *
+     * @param exception
+     *            The error result exception indicating why the asynchronous
+     *            operation has failed.
+     */
+    @Override
+    void handleException(LdapException exception);
+
+    /**
+     * Invoked when the asynchronous operation has completed successfully.
+     *
+     * @param result
+     *            The result of the asynchronous operation.
+     */
+    @Override
+    void handleResult(S result);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedAttribute.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedAttribute.java
new file mode 100644
index 0000000..1f98183
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedAttribute.java
@@ -0,0 +1,694 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An implementation of the {@code Attribute} interface with predictable
+ * iteration order.
+ * <p>
+ * Internally, attribute values are stored in a linked list and it's this list
+ * which defines the iteration ordering, which is the order in which elements
+ * were inserted into the set (insertion-order). This ordering is particularly
+ * useful in LDAP where clients generally appreciate having things returned in
+ * the same order they were presented.
+ * <p>
+ * All operations are supported by this implementation.
+ */
+public final class LinkedAttribute extends AbstractAttribute {
+
+    private static abstract class Impl {
+        abstract boolean add(LinkedAttribute attribute, ByteString value);
+
+        abstract void clear(LinkedAttribute attribute);
+
+        abstract boolean contains(LinkedAttribute attribute, ByteString value);
+
+        boolean containsAll(final LinkedAttribute attribute, final Collection<?> values) {
+            // TODO: could optimize if objects is a LinkedAttribute having the
+            // same equality matching rule.
+            for (final Object value : values) {
+                if (!contains(attribute, ByteString.valueOfObject(value))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        abstract ByteString firstValue(LinkedAttribute attribute);
+
+        abstract Iterator<ByteString> iterator(LinkedAttribute attribute);
+
+        abstract boolean remove(LinkedAttribute attribute, ByteString value);
+
+        abstract <T> boolean retainAll(LinkedAttribute attribute, Collection<T> values,
+                Collection<? super T> missingValues);
+
+        abstract int size(LinkedAttribute attribute);
+    }
+
+    private static final class MultiValueImpl extends Impl {
+        @Override
+        boolean add(final LinkedAttribute attribute, final ByteString value) {
+            final ByteString normalizedValue = normalizeValue(attribute, value);
+            return attribute.multipleValues.put(normalizedValue, value) == null;
+        }
+
+        @Override
+        void clear(final LinkedAttribute attribute) {
+            attribute.multipleValues = null;
+            attribute.pimpl = ZERO_VALUE_IMPL;
+        }
+
+        @Override
+        boolean contains(final LinkedAttribute attribute, final ByteString value) {
+            return attribute.multipleValues.containsKey(normalizeValue(attribute, value));
+        }
+
+        @Override
+        ByteString firstValue(final LinkedAttribute attribute) {
+            return attribute.multipleValues.values().iterator().next();
+        }
+
+        @Override
+        Iterator<ByteString> iterator(final LinkedAttribute attribute) {
+            return new Iterator<ByteString>() {
+                private Impl expectedImpl = MULTI_VALUE_IMPL;
+
+                private Iterator<ByteString> iterator = attribute.multipleValues.values()
+                        .iterator();
+
+                @Override
+                public boolean hasNext() {
+                    return iterator.hasNext();
+                }
+
+                @Override
+                public ByteString next() {
+                    if (attribute.pimpl != expectedImpl) {
+                        throw new ConcurrentModificationException();
+                    } else {
+                        return iterator.next();
+                    }
+                }
+
+                @Override
+                public void remove() {
+                    if (attribute.pimpl != expectedImpl) {
+                        throw new ConcurrentModificationException();
+                    } else {
+                        iterator.remove();
+
+                        // Resize if we have removed the second to last value.
+                        if (attribute.multipleValues != null
+                                && attribute.multipleValues.size() == 1) {
+                            resize(attribute);
+                            iterator = attribute.pimpl.iterator(attribute);
+                        }
+
+                        // Always update since we may change to single or zero
+                        // value
+                        // impl.
+                        expectedImpl = attribute.pimpl;
+                    }
+                }
+
+            };
+        }
+
+        @Override
+        boolean remove(final LinkedAttribute attribute, final ByteString value) {
+            final ByteString normalizedValue = normalizeValue(attribute, value);
+            if (attribute.multipleValues.remove(normalizedValue) != null) {
+                resize(attribute);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        <T> boolean retainAll(final LinkedAttribute attribute, final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            // TODO: could optimize if objects is a LinkedAttribute having the
+            // same equality matching rule.
+            if (values.isEmpty()) {
+                clear(attribute);
+                return true;
+            }
+
+            final Map<ByteString, T> valuesToRetain = new HashMap<>(values.size());
+            for (final T value : values) {
+                valuesToRetain.put(normalizeValue(attribute, ByteString.valueOfObject(value)), value);
+            }
+
+            boolean modified = false;
+            final Iterator<ByteString> iterator = attribute.multipleValues.keySet().iterator();
+            while (iterator.hasNext()) {
+                final ByteString normalizedValue = iterator.next();
+                if (valuesToRetain.remove(normalizedValue) == null) {
+                    modified = true;
+                    iterator.remove();
+                }
+            }
+
+            if (missingValues != null) {
+                missingValues.addAll(valuesToRetain.values());
+            }
+
+            resize(attribute);
+
+            return modified;
+        }
+
+        @Override
+        int size(final LinkedAttribute attribute) {
+            return attribute.multipleValues.size();
+        }
+
+        private void resize(final LinkedAttribute attribute) {
+            // May need to resize if initial size estimate was wrong (e.g. all
+            // values in added collection were the same).
+            switch (attribute.multipleValues.size()) {
+            case 0:
+                attribute.multipleValues = null;
+                attribute.pimpl = ZERO_VALUE_IMPL;
+                break;
+            case 1:
+                final Map.Entry<ByteString, ByteString> e =
+                        attribute.multipleValues.entrySet().iterator().next();
+                attribute.singleValue = e.getValue();
+                attribute.normalizedSingleValue = e.getKey();
+                attribute.multipleValues = null;
+                attribute.pimpl = SINGLE_VALUE_IMPL;
+                break;
+            default:
+                // Nothing to do.
+                break;
+            }
+        }
+    }
+
+    private static final class SingleValueImpl extends Impl {
+        @Override
+        boolean add(final LinkedAttribute attribute, final ByteString value) {
+            final ByteString normalizedValue = normalizeValue(attribute, value);
+            if (attribute.normalizedSingleValue().equals(normalizedValue)) {
+                return false;
+            }
+
+            attribute.multipleValues = new LinkedHashMap<>(2);
+            attribute.multipleValues.put(attribute.normalizedSingleValue, attribute.singleValue);
+            attribute.multipleValues.put(normalizedValue, value);
+            attribute.singleValue = null;
+            attribute.normalizedSingleValue = null;
+            attribute.pimpl = MULTI_VALUE_IMPL;
+
+            return true;
+        }
+
+        @Override
+        void clear(final LinkedAttribute attribute) {
+            attribute.singleValue = null;
+            attribute.normalizedSingleValue = null;
+            attribute.pimpl = ZERO_VALUE_IMPL;
+        }
+
+        @Override
+        boolean contains(final LinkedAttribute attribute, final ByteString value) {
+            final ByteString normalizedValue = normalizeValue(attribute, value);
+            return attribute.normalizedSingleValue().equals(normalizedValue);
+        }
+
+        @Override
+        ByteString firstValue(final LinkedAttribute attribute) {
+            if (attribute.singleValue != null) {
+                return attribute.singleValue;
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
+
+        @Override
+        Iterator<ByteString> iterator(final LinkedAttribute attribute) {
+            return new Iterator<ByteString>() {
+                private Impl expectedImpl = SINGLE_VALUE_IMPL;
+
+                private boolean hasNext = true;
+
+                @Override
+                public boolean hasNext() {
+                    return hasNext;
+                }
+
+                @Override
+                public ByteString next() {
+                    if (attribute.pimpl != expectedImpl) {
+                        throw new ConcurrentModificationException();
+                    } else if (hasNext) {
+                        hasNext = false;
+                        return attribute.singleValue;
+                    } else {
+                        throw new NoSuchElementException();
+                    }
+                }
+
+                @Override
+                public void remove() {
+                    if (attribute.pimpl != expectedImpl) {
+                        throw new ConcurrentModificationException();
+                    } else if (hasNext || attribute.singleValue == null) {
+                        throw new IllegalStateException();
+                    } else {
+                        clear(attribute);
+                        expectedImpl = attribute.pimpl;
+                    }
+                }
+
+            };
+        }
+
+        @Override
+        boolean remove(final LinkedAttribute attribute, final ByteString value) {
+            if (contains(attribute, value)) {
+                clear(attribute);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        <T> boolean retainAll(final LinkedAttribute attribute, final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            // TODO: could optimize if objects is a LinkedAttribute having the
+            // same equality matching rule.
+            if (values.isEmpty()) {
+                clear(attribute);
+                return true;
+            }
+
+            final ByteString normalizedSingleValue = attribute.normalizedSingleValue();
+            boolean retained = false;
+            for (final T value : values) {
+                final ByteString normalizedValue =
+                        normalizeValue(attribute, ByteString.valueOfObject(value));
+                if (normalizedSingleValue.equals(normalizedValue)) {
+                    if (missingValues == null) {
+                        // We can stop now.
+                        return false;
+                    }
+                    retained = true;
+                } else if (missingValues != null) {
+                    missingValues.add(value);
+                }
+            }
+
+            if (!retained) {
+                clear(attribute);
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        int size(final LinkedAttribute attribute) {
+            return 1;
+        }
+    }
+
+    private static final class ZeroValueImpl extends Impl {
+        @Override
+        boolean add(final LinkedAttribute attribute, final ByteString value) {
+            attribute.singleValue = value;
+            attribute.pimpl = SINGLE_VALUE_IMPL;
+            return true;
+        }
+
+        @Override
+        void clear(final LinkedAttribute attribute) {
+            // Nothing to do.
+        }
+
+        @Override
+        boolean contains(final LinkedAttribute attribute, final ByteString value) {
+            return false;
+        }
+
+        @Override
+        boolean containsAll(final LinkedAttribute attribute, final Collection<?> values) {
+            return values.isEmpty();
+        }
+
+        @Override
+        ByteString firstValue(final LinkedAttribute attribute) {
+            throw new NoSuchElementException();
+        }
+
+        @Override
+        Iterator<ByteString> iterator(final LinkedAttribute attribute) {
+            return new Iterator<ByteString>() {
+                @Override
+                public boolean hasNext() {
+                    return false;
+                }
+
+                @Override
+                public ByteString next() {
+                    if (attribute.pimpl != ZERO_VALUE_IMPL) {
+                        throw new ConcurrentModificationException();
+                    } else {
+                        throw new NoSuchElementException();
+                    }
+                }
+
+                @Override
+                public void remove() {
+                    if (attribute.pimpl != ZERO_VALUE_IMPL) {
+                        throw new ConcurrentModificationException();
+                    } else {
+                        throw new IllegalStateException();
+                    }
+                }
+
+            };
+        }
+
+        @Override
+        boolean remove(final LinkedAttribute attribute, final ByteString value) {
+            return false;
+        }
+
+        @Override
+        <T> boolean retainAll(final LinkedAttribute attribute, final Collection<T> values,
+                final Collection<? super T> missingValues) {
+            if (missingValues != null) {
+                missingValues.addAll(values);
+            }
+            return false;
+        }
+
+        @Override
+        int size(final LinkedAttribute attribute) {
+            return 0;
+        }
+    }
+
+    /** An attribute factory which can be used to create new linked attributes. */
+    public static final AttributeFactory FACTORY = new AttributeFactory() {
+        @Override
+        public Attribute newAttribute(final AttributeDescription attributeDescription) {
+            return new LinkedAttribute(attributeDescription);
+        }
+    };
+
+    private static final MultiValueImpl MULTI_VALUE_IMPL = new MultiValueImpl();
+    private static final SingleValueImpl SINGLE_VALUE_IMPL = new SingleValueImpl();
+    private static final ZeroValueImpl ZERO_VALUE_IMPL = new ZeroValueImpl();
+
+    private final AttributeDescription attributeDescription;
+    private Map<ByteString, ByteString> multipleValues;
+    private ByteString normalizedSingleValue;
+    private Impl pimpl = ZERO_VALUE_IMPL;
+    private ByteString singleValue;
+
+    /**
+     * Creates a new attribute having the same attribute description and
+     * attribute values as {@code attribute}.
+     *
+     * @param attribute
+     *            The attribute to be copied.
+     * @throws NullPointerException
+     *             If {@code attribute} was {@code null}.
+     */
+    public LinkedAttribute(final Attribute attribute) {
+        this.attributeDescription = attribute.getAttributeDescription();
+
+        if (attribute instanceof LinkedAttribute) {
+            final LinkedAttribute other = (LinkedAttribute) attribute;
+            this.pimpl = other.pimpl;
+            this.singleValue = other.singleValue;
+            this.normalizedSingleValue = other.normalizedSingleValue;
+            if (other.multipleValues != null) {
+                this.multipleValues = new LinkedHashMap<>(other.multipleValues);
+            }
+        } else {
+            addAll(attribute);
+        }
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and no
+     * attribute values.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public LinkedAttribute(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        this.attributeDescription = attributeDescription;
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * single attribute value.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null} .
+     */
+    public LinkedAttribute(final AttributeDescription attributeDescription, final Object value) {
+        this(attributeDescription);
+        add(value);
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * attribute values.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param values
+     *            The attribute values.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code values} was
+     *             {@code null}.
+     */
+    public LinkedAttribute(final AttributeDescription attributeDescription,
+            final Object... values) {
+        this(attributeDescription);
+        add(values);
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * attribute values.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param values
+     *            The attribute values.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code values} was
+     *             {@code null}.
+     */
+    public LinkedAttribute(final AttributeDescription attributeDescription,
+            final Collection<?> values) {
+        this(attributeDescription);
+        addAll(values, null);
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and no
+     * attribute values. The attribute description will be decoded using the
+     * default schema.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    public LinkedAttribute(final String attributeDescription) {
+        this(AttributeDescription.valueOf(attributeDescription));
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * attribute values. The attribute description will be decoded using the
+     * default schema.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param values
+     *            The attribute values.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code values} was
+     *             {@code null}.
+     */
+    public LinkedAttribute(final String attributeDescription, final Collection<?> values) {
+        this(attributeDescription);
+        addAll(values, null);
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * single attribute value. The attribute description will be decoded using
+     * the default schema.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param value
+     *            The single attribute value.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code value} was
+     *             {@code null} .
+     */
+    public LinkedAttribute(final String attributeDescription, final Object value) {
+        this(attributeDescription);
+        add(ByteString.valueOfObject(value));
+    }
+
+    /**
+     * Creates a new attribute having the specified attribute description and
+     * attribute values. The attribute description will be decoded using the
+     * default schema.
+     * <p>
+     * Any attribute values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeDescription
+     *            The attribute description.
+     * @param values
+     *            The attribute values.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} or {@code values} was
+     *             {@code null}.
+     */
+    public LinkedAttribute(final String attributeDescription, final Object... values) {
+        this(attributeDescription);
+        add(values);
+    }
+
+    @Override
+    public boolean add(final ByteString value) {
+        Reject.ifNull(value);
+        return pimpl.add(this, value);
+    }
+
+    @Override
+    public void clear() {
+        pimpl.clear(this);
+    }
+
+    @Override
+    public boolean contains(final Object value) {
+        Reject.ifNull(value);
+        return pimpl.contains(this, ByteString.valueOfObject(value));
+    }
+
+    @Override
+    public boolean containsAll(final Collection<?> values) {
+        Reject.ifNull(values);
+        return pimpl.containsAll(this, values);
+    }
+
+    @Override
+    public ByteString firstValue() {
+        return pimpl.firstValue(this);
+    }
+
+    @Override
+    public AttributeDescription getAttributeDescription() {
+        return attributeDescription;
+    }
+
+    @Override
+    public Iterator<ByteString> iterator() {
+        return pimpl.iterator(this);
+    }
+
+    @Override
+    public boolean remove(final Object value) {
+        Reject.ifNull(value);
+        return pimpl.remove(this, ByteString.valueOfObject(value));
+    }
+
+    @Override
+    public <T> boolean retainAll(final Collection<T> values,
+            final Collection<? super T> missingValues) {
+        Reject.ifNull(values);
+        return pimpl.retainAll(this, values, missingValues);
+    }
+
+    @Override
+    public int size() {
+        return pimpl.size(this);
+    }
+
+    /** Lazily computes the normalized single value. */
+    private ByteString normalizedSingleValue() {
+        if (normalizedSingleValue == null) {
+            normalizedSingleValue = normalizeValue(this, singleValue);
+        }
+        return normalizedSingleValue;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedHashMapEntry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedHashMapEntry.java
new file mode 100644
index 0000000..0277177
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LinkedHashMapEntry.java
@@ -0,0 +1,164 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.LinkedHashMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.requests.Requests;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An implementation of the {@code Entry} interface which uses a
+ * {@code LinkedHashMap} for storing attributes. Attributes are returned in the
+ * same order that they were added to the entry. All operations are supported by
+ * this implementation. For example, you can build an entry like this:
+ *
+ * <pre>
+ * Entry entry = new LinkedHashMapEntry("cn=Bob,ou=People,dc=example,dc=com")
+ *    .addAttribute("cn", "Bob")
+ *    .addAttribute("objectclass", "top")
+ *    .addAttribute("objectclass", "person")
+ *    .addAttribute("objectclass", "organizationalPerson")
+ *    .addAttribute("objectclass", "inetOrgPerson")
+ *    .addAttribute("mail", "subgenius@example.com")
+ *    .addAttribute("sn", "Dobbs");
+ * </pre>
+ *
+ * A {@code LinkedHashMapEntry} stores references to attributes which have been
+ * added using the {@link #addAttribute} methods. Attributes sharing the same
+ * attribute description are merged by adding the values of the new attribute to
+ * the existing attribute. More specifically, the existing attribute must be
+ * modifiable for the merge to succeed. Similarly, the {@link #removeAttribute}
+ * methods remove the specified values from the existing attribute. The
+ * {@link #replaceAttribute} methods remove the existing attribute (if present)
+ * and store a reference to the new attribute - neither the new or existing
+ * attribute need to be modifiable in this case.
+ */
+public final class LinkedHashMapEntry extends AbstractMapEntry {
+    /**
+     * An entry factory which can be used to create new linked hash map entries.
+     */
+    public static final EntryFactory FACTORY = new EntryFactory() {
+        @Override
+        public Entry newEntry(final DN name) {
+            return new LinkedHashMapEntry(name);
+        }
+    };
+
+    /**
+     * Creates an entry having the same distinguished name, attributes, and
+     * object classes of the provided entry. This constructor performs a deep
+     * copy of {@code entry} and will copy each attribute as a
+     * {@link LinkedAttribute}.
+     * <p>
+     * A shallow copy constructor is provided by
+     * {@link #LinkedHashMapEntry(Entry)}.
+     *
+     * @param entry
+     *            The entry to be copied.
+     * @return A deep copy of {@code entry}.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     * @see #LinkedHashMapEntry(Entry)
+     */
+    public static LinkedHashMapEntry deepCopyOfEntry(final Entry entry) {
+        LinkedHashMapEntry copy = new LinkedHashMapEntry(entry.getName());
+        for (final Attribute attribute : entry.getAllAttributes()) {
+            copy.addAttribute(new LinkedAttribute(attribute));
+        }
+        return copy;
+    }
+
+    /**
+     * Creates an entry with an empty (root) distinguished name and no
+     * attributes.
+     */
+    public LinkedHashMapEntry() {
+        this(DN.rootDN());
+    }
+
+    /**
+     * Creates an empty entry using the provided distinguished name and no
+     * attributes.
+     *
+     * @param name
+     *            The distinguished name of this entry.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public LinkedHashMapEntry(final DN name) {
+        super(Reject.checkNotNull(name), new LinkedHashMap<AttributeDescription, Attribute>());
+    }
+
+    /**
+     * Creates an entry having the same distinguished name, attributes, and
+     * object classes of the provided entry. This constructor performs a shallow
+     * copy of {@code entry} and will not copy the attributes contained in
+     * {@code entry}.
+     * <p>
+     * A deep copy constructor is provided by {@link #deepCopyOfEntry(Entry)}
+     *
+     * @param entry
+     *            The entry to be copied.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     * @see #deepCopyOfEntry(Entry)
+     */
+    public LinkedHashMapEntry(final Entry entry) {
+        this(entry.getName());
+        for (final Attribute attribute : entry.getAllAttributes()) {
+            addAttribute(attribute);
+        }
+    }
+
+    /**
+     * Creates an empty entry using the provided distinguished name decoded
+     * using the default schema.
+     *
+     * @param name
+     *            The distinguished name of this entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public LinkedHashMapEntry(final String name) {
+        this(DN.valueOf(name));
+    }
+
+    /**
+     * Creates a new entry using the provided lines of LDIF decoded using the
+     * default schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing the an LDIF add change record or an
+     *            LDIF entry record.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public LinkedHashMapEntry(final String... ldifLines) {
+        this(Requests.newAddRequest(ldifLines));
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancer.java
new file mode 100644
index 0000000..a7f8e06
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancer.java
@@ -0,0 +1,297 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+import static org.forgerock.opendj.ldap.Connections.*;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.ResultCode.CLIENT_SIDE_CONNECT_ERROR;
+import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.util.Utils.joinAsString;
+import static org.forgerock.util.promise.Promises.*;
+
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * An abstract load balancer providing common monitoring and failover capabilities.
+ * <p>
+ * Implementations should override the {@link ConnectionFactory} methods and use the
+ * {@link #getMonitoredConnectionFactory(int)} method in order to obtain the desired load-balanced connection factory.
+ * If the requested connection factory is unavailable then a linear probe will be performed in order to the next
+ * suitable connection factory.
+ */
+abstract class LoadBalancer implements ConnectionFactory {
+    LoadBalancer(final String loadBalancerName,
+                 final Collection<? extends ConnectionFactory> factories,
+                 final Options options) {
+        Reject.ifNull(loadBalancerName, factories, options);
+
+        this.loadBalancerName = loadBalancerName;
+        this.monitoredFactories = new ArrayList<>(factories.size());
+        int i = 0;
+        for (final ConnectionFactory f : factories) {
+            this.monitoredFactories.add(new MonitoredConnectionFactory(f, i++));
+        }
+        this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(options.get(LOAD_BALANCER_SCHEDULER));
+        this.monitoringIntervalMS = options.get(LOAD_BALANCER_MONITORING_INTERVAL).to(TimeUnit.MILLISECONDS);
+        this.listener = options.get(LOAD_BALANCER_EVENT_LISTENER);
+    }
+
+    @Override
+    public final void close() {
+        if (isClosed.compareAndSet(false, true)) {
+            synchronized (stateLock) {
+                if (monitoringFuture != null) {
+                    monitoringFuture.cancel(false);
+                    monitoringFuture = null;
+                }
+            }
+            closeSilently(monitoredFactories);
+            scheduler.release();
+        }
+    }
+
+    @Override
+    public final String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(loadBalancerName);
+        builder.append('(');
+        joinAsString(builder, ",", monitoredFactories);
+        builder.append(')');
+        return builder.toString();
+    }
+
+    private final class MonitoredConnectionFactory implements ConnectionFactory, LdapResultHandler<Connection> {
+        private final ConnectionFactory factory;
+        private final AtomicBoolean isOperational = new AtomicBoolean(true);
+        private volatile Promise<?, LdapException> pendingConnectPromise;
+        private final int index;
+
+        private MonitoredConnectionFactory(final ConnectionFactory factory, final int index) {
+            this.factory = factory;
+            this.index = index;
+        }
+
+        @Override
+        public void close() {
+            // Should we cancel the promise?
+            factory.close();
+        }
+
+        @Override
+        public Connection getConnection() throws LdapException {
+            final Connection connection;
+            try {
+                connection = factory.getConnection();
+            } catch (LdapException e) {
+                // Attempt failed - try next factory.
+                notifyOffline(e);
+                final int nextIndex = (index + 1) % monitoredFactories.size();
+                return getMonitoredConnectionFactory(nextIndex).getConnection();
+            }
+            notifyOnline();
+            return connection;
+        }
+
+        @Override
+        public Promise<Connection, LdapException> getConnectionAsync() {
+            return factory.getConnectionAsync().thenAsync(
+                new AsyncFunction<Connection, Connection, LdapException>() {
+                    @Override
+                    public Promise<Connection, LdapException> apply(Connection value) throws LdapException {
+                        notifyOnline();
+                        return newResultPromise(value);
+                    }
+                },
+                new AsyncFunction<LdapException, Connection, LdapException>() {
+                    @Override
+                    public Promise<Connection, LdapException> apply(LdapException error) throws LdapException {
+                        // Attempt failed - try next factory.
+                        notifyOffline(error);
+                        final int nextIndex = (index + 1) % monitoredFactories.size();
+                        return getMonitoredConnectionFactory(nextIndex).getConnectionAsync();
+                    }
+                });
+        }
+
+        /** Handle monitoring connection request failure. */
+        @Override
+        public void handleException(final LdapException exception) {
+            notifyOffline(exception);
+        }
+
+        /** Handle monitoring connection request success. */
+        @Override
+        public void handleResult(final Connection connection) {
+            // The connection is not going to be used, so close it immediately.
+            connection.close();
+            notifyOnline();
+        }
+
+        @Override
+        public String toString() {
+            return factory.toString();
+        }
+
+        /** Attempt to connect to the factory if it is offline and there is no pending monitoring request. */
+        private synchronized void checkIfAvailable() {
+            if (!isOperational.get() && (pendingConnectPromise == null || pendingConnectPromise.isDone())) {
+                logger.debug(LocalizableMessage.raw("Attempting reconnect to offline factory '%s'", this));
+                pendingConnectPromise = factory.getConnectionAsync().thenOnResult(this).thenOnException(this);
+            }
+        }
+
+        private void notifyOffline(final LdapException error) {
+            // Save the error in case the load-balancer is exhausted.
+            lastFailure = error;
+            if (isOperational.getAndSet(false)) {
+                // Transition from online to offline.
+                synchronized (listenerLock) {
+                    try {
+                        listener.handleConnectionFactoryOffline(factory, error);
+                    } catch (RuntimeException e) {
+                        handleListenerException(e);
+                    }
+                }
+                synchronized (stateLock) {
+                    offlineFactoriesCount++;
+                    if (offlineFactoriesCount == 1) {
+                        logger.debug(LocalizableMessage.raw("Starting monitoring thread"));
+                        monitoringFuture =
+                                scheduler.get().scheduleWithFixedDelay(new MonitorRunnable(), 0,
+                                        monitoringIntervalMS, TimeUnit.MILLISECONDS);
+                    }
+                }
+            }
+        }
+
+        private void notifyOnline() {
+            if (!isOperational.getAndSet(true)) {
+                // Transition from offline to online.
+                synchronized (listenerLock) {
+                    try {
+                        listener.handleConnectionFactoryOnline(factory);
+                    } catch (RuntimeException e) {
+                        handleListenerException(e);
+                    }
+                }
+                synchronized (stateLock) {
+                    offlineFactoriesCount--;
+                    if (offlineFactoriesCount == 0) {
+                        logger.debug(LocalizableMessage.raw("Stopping monitoring thread"));
+
+                        monitoringFuture.cancel(false);
+                        monitoringFuture = null;
+                    }
+                }
+            }
+        }
+
+        private void handleListenerException(RuntimeException e) {
+            // TODO: I18N
+            logger.error(LocalizableMessage.raw(
+                    "A run-time error occurred while processing a load-balancer event", e));
+        }
+    }
+
+    private final class MonitorRunnable implements Runnable {
+        private MonitorRunnable() {
+            // Nothing to do.
+        }
+
+        @Override
+        public void run() {
+            for (final MonitoredConnectionFactory factory : monitoredFactories) {
+                factory.checkIfAvailable();
+            }
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private final String loadBalancerName;
+    private final List<MonitoredConnectionFactory> monitoredFactories;
+    private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler;
+    private final Object stateLock = new Object();
+
+    /**
+     * The last connection failure which caused a connection factory to be
+     * marked offline. This is used in order to help diagnose problems when the
+     * load-balancer has exhausted all of its factories.
+     */
+    private volatile LdapException lastFailure;
+
+    /** The event listener which should be notified when connection factories go on or off-line. */
+    private final LoadBalancerEventListener listener;
+
+    /** Ensures that events are notified one at a time. */
+    private final Object listenerLock = new Object();
+
+    /** Guarded by stateLock. */
+    private int offlineFactoriesCount;
+    private final long monitoringIntervalMS;
+
+    /** Guarded by stateLock. */
+    private ScheduledFuture<?> monitoringFuture;
+    private final AtomicBoolean isClosed = new AtomicBoolean();
+
+    /**
+     * Return the first available connection factory starting from {@code initialIndex}.
+     *
+     * @param initialIndex The index of the connection factory to be returned if operational.
+     * @return The first available connection factory starting from the initial index.
+     * @throws LdapException If no connection factories are available.
+     */
+    final ConnectionFactory getMonitoredConnectionFactory(final int initialIndex) throws LdapException {
+        final int maxIndex = monitoredFactories.size();
+        int index = initialIndex;
+        do {
+            final MonitoredConnectionFactory factory = monitoredFactories.get(index);
+            if (factory.isOperational.get()) {
+                return factory;
+            }
+            index = (index + 1) % maxIndex;
+        } while (index != initialIndex);
+
+        /*
+         * All factories are offline so give up. We could have a configurable
+         * policy here such as waiting indefinitely, or for a configurable
+         * timeout period.
+         */
+        throw newLdapException(CLIENT_SIDE_CONNECT_ERROR, "No operational connection factories available", lastFailure);
+    }
+
+    final String getLoadBalancerName() {
+        return loadBalancerName;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancerEventListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancerEventListener.java
new file mode 100644
index 0000000..e288bec
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LoadBalancerEventListener.java
@@ -0,0 +1,91 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.LOAD_BALANCER_EVENT_LISTENER_LOG_OFFLINE;
+import static com.forgerock.opendj.ldap.CoreMessages.LOAD_BALANCER_EVENT_LISTENER_LOG_ONLINE;
+
+import java.util.EventListener;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+
+/**
+ * An object that registers to be notified when a connection factory associated
+ * with a load-balancer changes state from offline to online or vice-versa.
+ * <p>
+ * <b>NOTE:</b> load-balancer implementations must ensure that only one event is
+ * sent at a time. Event listener implementations should not need to be thread
+ * safe.
+ *
+ * @see Connections#LOAD_BALANCER_EVENT_LISTENER
+ */
+public interface LoadBalancerEventListener extends EventListener {
+    /**
+     * An event listener implementation which logs events to the LoadBalancingAlgorithm logger. This event listener is
+     * the default implementation configured using the {@link Connections#LOAD_BALANCER_EVENT_LISTENER}
+     * option.
+     */
+    LoadBalancerEventListener LOG_EVENTS = new LoadBalancerEventListener() {
+        private final LocalizedLogger logger = LocalizedLogger.getLocalizedLogger(LoadBalancer.class);
+
+        @Override
+        public void handleConnectionFactoryOnline(final ConnectionFactory factory) {
+            logger.info(LOAD_BALANCER_EVENT_LISTENER_LOG_ONLINE.get(factory));
+        }
+
+        @Override
+        public void handleConnectionFactoryOffline(final ConnectionFactory factory, final LdapException error) {
+            logger.warn(LOAD_BALANCER_EVENT_LISTENER_LOG_OFFLINE.get(factory, error.getMessage()));
+        }
+    };
+
+    /** An event listener implementation which ignores all events. */
+    LoadBalancerEventListener NO_OP = new LoadBalancerEventListener() {
+        @Override
+        public void handleConnectionFactoryOnline(final ConnectionFactory factory) {
+            // Do nothing.
+        }
+
+        @Override
+        public void handleConnectionFactoryOffline(final ConnectionFactory factory, final LdapException error) {
+            // Do nothing.
+        }
+    };
+
+    /**
+     * Invoked when the load-balancer is unable to obtain a connection from the
+     * specified connection factory. The connection factory will be removed from
+     * the load-balancer and monitored periodically in order to determine when
+     * it is available again, at which point an online notification event will
+     * occur.
+     *
+     * @param factory
+     *            The connection factory which has failed.
+     * @param error
+     *            The last error that occurred.
+     */
+    void handleConnectionFactoryOffline(ConnectionFactory factory, LdapException error);
+
+    /**
+     * Invoked when the load-balancer detects that a previously offline
+     * connection factory is available for use again.
+     *
+     * @param factory
+     *            The connection factory which is now available for use.
+     */
+    void handleConnectionFactoryOnline(ConnectionFactory factory);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Matcher.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Matcher.java
new file mode 100644
index 0000000..3f74233
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Matcher.java
@@ -0,0 +1,567 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.MatchingRuleUse;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * An interface for determining whether entries match a {@code Filter}.
+ */
+public final class Matcher {
+    private static final class AndMatcherImpl extends MatcherImpl {
+        private final List<MatcherImpl> subMatchers;
+
+        private AndMatcherImpl(final List<MatcherImpl> subMatchers) {
+            this.subMatchers = subMatchers;
+        }
+
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            ConditionResult r = ConditionResult.TRUE;
+            for (final MatcherImpl m : subMatchers) {
+                final ConditionResult p = m.matches(entry);
+                if (p == ConditionResult.FALSE) {
+                    return p;
+                }
+                r = ConditionResult.and(r, p);
+            }
+            return r;
+        }
+    }
+
+    private static final class AssertionMatcherImpl extends MatcherImpl {
+        private final Assertion assertion;
+
+        private final AttributeDescription attributeDescription;
+
+        private final boolean dnAttributes;
+
+        private final MatchingRule rule;
+
+        private final MatchingRuleUse ruleUse;
+
+        private AssertionMatcherImpl(final AttributeDescription attributeDescription,
+                final MatchingRule rule, final MatchingRuleUse ruleUse, final Assertion assertion,
+                final boolean dnAttributes) {
+            this.attributeDescription = attributeDescription;
+            this.rule = rule;
+            this.ruleUse = ruleUse;
+            this.assertion = assertion;
+            this.dnAttributes = dnAttributes;
+        }
+
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            ConditionResult r = ConditionResult.FALSE;
+            if (attributeDescription != null) {
+                // If the matchingRule field is absent, the type field will be
+                // present and the default equality matching rule is used,
+                // and an equality match is performed for that type.
+
+                // If the type field is present and the matchingRule is present,
+                // the matchValue is compared against the specified attribute
+                // type and its subtypes.
+                final ConditionResult p =
+                        Matcher.matches(entry.getAttribute(attributeDescription), rule, assertion);
+                if (p == ConditionResult.TRUE) {
+                    return p;
+                }
+                r = ConditionResult.or(r, p);
+            } else {
+                // If the type field is absent and the matchingRule is present,
+                // the matchValue is compared against all attributes in an entry
+                // that support that matchingRule.
+                for (final Attribute a : entry.getAllAttributes()) {
+                    if (ruleUse.hasAttribute(a.getAttributeDescription().getAttributeType())) {
+                        final ConditionResult p = Matcher.matches(a, rule, assertion);
+                        if (p == ConditionResult.TRUE) {
+                            return p;
+                        }
+                        r = ConditionResult.or(r, p);
+                    }
+                }
+            }
+
+            if (dnAttributes) {
+                // If the dnAttributes field is set to TRUE, the match is
+                // additionally applied against all the AttributeValueAssertions
+                // in an entry's distinguished name, and it evaluates to TRUE if
+                // there is at least one attribute or subtype in the
+                // distinguished name for which the filter item evaluates to
+                // TRUE.
+                final DN dn = entry.getName();
+                for (final RDN rdn : dn) {
+                    for (final AVA ava : rdn) {
+                        if (ruleUse.hasAttribute(ava.getAttributeType())) {
+                            final ConditionResult p =
+                                    Matcher.matches(ava.getAttributeValue(), rule, assertion);
+                            if (p == ConditionResult.TRUE) {
+                                return p;
+                            }
+                            r = ConditionResult.or(r, p);
+                        }
+                    }
+                }
+            }
+            return r;
+        }
+    }
+
+    private static class FalseMatcherImpl extends MatcherImpl {
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            return ConditionResult.FALSE;
+        }
+    }
+
+    private static abstract class MatcherImpl {
+        public abstract ConditionResult matches(Entry entry);
+    }
+
+    private static final class NotMatcherImpl extends MatcherImpl {
+        private final MatcherImpl subFilter;
+
+        private NotMatcherImpl(final MatcherImpl subFilter) {
+            this.subFilter = subFilter;
+        }
+
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            return ConditionResult.not(subFilter.matches(entry));
+        }
+    }
+
+    private static final class OrMatcherImpl extends MatcherImpl {
+        private final List<MatcherImpl> subMatchers;
+
+        private OrMatcherImpl(final List<MatcherImpl> subMatchers) {
+            this.subMatchers = subMatchers;
+        }
+
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            ConditionResult r = ConditionResult.FALSE;
+            for (final MatcherImpl m : subMatchers) {
+                final ConditionResult p = m.matches(entry);
+                if (p == ConditionResult.TRUE) {
+                    return p;
+                }
+                r = ConditionResult.or(r, p);
+            }
+            return r;
+        }
+    }
+
+    private static final class PresentMatcherImpl extends MatcherImpl {
+        private final AttributeDescription attribute;
+
+        private PresentMatcherImpl(final AttributeDescription attribute) {
+            this.attribute = attribute;
+        }
+
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            return entry.getAttribute(attribute) == null ? ConditionResult.FALSE
+                    : ConditionResult.TRUE;
+        }
+    }
+
+    private static class TrueMatcherImpl extends MatcherImpl {
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            return ConditionResult.TRUE;
+        }
+    }
+
+    private static class UndefinedMatcherImpl extends MatcherImpl {
+        @Override
+        public ConditionResult matches(final Entry entry) {
+            return ConditionResult.UNDEFINED;
+        }
+    }
+
+    /**
+     * A visitor which is used to transform a filter into a matcher.
+     */
+    private static final class Visitor implements FilterVisitor<MatcherImpl, Schema> {
+        @Override
+        public MatcherImpl visitAndFilter(final Schema schema, final List<Filter> subFilters) {
+            if (subFilters.isEmpty()) {
+                logger.trace(LocalizableMessage.raw("Empty add filter component. Will always return TRUE"));
+                return TRUE;
+            }
+
+            final List<MatcherImpl> subMatchers = new ArrayList<>(subFilters.size());
+            for (final Filter f : subFilters) {
+                subMatchers.add(f.accept(this, schema));
+            }
+            return new AndMatcherImpl(subMatchers);
+        }
+
+        @Override
+        public MatcherImpl visitApproxMatchFilter(final Schema schema,
+                final String attributeDescription, final ByteString assertionValue) {
+            final AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw(
+                        "Attribute description %s is not recognized", attributeDescription, e));
+                return UNDEFINED;
+            }
+
+            final MatchingRule rule = ad.getAttributeType().getApproximateMatchingRule();
+            if (rule == null) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The attribute type %s does not define an approximate matching rule",
+                        attributeDescription));
+                return UNDEFINED;
+            }
+
+            final Assertion assertion;
+            try {
+                assertion = rule.getAssertion(assertionValue);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The assertion value %s is invalid", assertionValue, de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, null, assertion, false);
+        }
+
+        @Override
+        public MatcherImpl visitEqualityMatchFilter(final Schema schema,
+                final String attributeDescription, final ByteString assertionValue) {
+            final AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                        attributeDescription, e));
+                return UNDEFINED;
+            }
+
+            final MatchingRule rule = ad.getAttributeType().getEqualityMatchingRule();
+            if (rule == null) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The attribute type %s does not define an equality matching rule",
+                        attributeDescription));
+                return UNDEFINED;
+            }
+
+            final Assertion assertion;
+            try {
+                assertion = rule.getAssertion(assertionValue);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The assertion value %s is invalid", assertionValue, de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, null, assertion, false);
+        }
+
+        @Override
+        public MatcherImpl visitExtensibleMatchFilter(final Schema schema,
+                final String matchingRule, final String attributeDescription,
+                final ByteString assertionValue, final boolean dnAttributes) {
+            AttributeDescription ad = null;
+            MatchingRule rule = null;
+            MatchingRuleUse ruleUse = null;
+            Assertion assertion;
+
+            if (matchingRule != null) {
+                try {
+                    rule = schema.getMatchingRule(matchingRule);
+                } catch (final UnknownSchemaElementException e) {
+                    // TODO: I18N
+                    logger.warn(LocalizableMessage.raw("Matching rule %s is not recognized", matchingRule));
+                    return UNDEFINED;
+                }
+            }
+
+            if (attributeDescription != null) {
+                try {
+                    ad = AttributeDescription.valueOf(attributeDescription, schema);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    // TODO: I18N
+                    logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                            attributeDescription, e));
+                    return UNDEFINED;
+                }
+
+                if (rule == null) {
+                    rule = ad.getAttributeType().getEqualityMatchingRule();
+                    if (rule == null) {
+                        // TODO: I18N
+                        logger.warn(LocalizableMessage.raw(
+                                "The attribute type %s does not define an equality matching rule",
+                                attributeDescription));
+                        return UNDEFINED;
+                    }
+                } else {
+                    try {
+                        ruleUse = schema.getMatchingRuleUse(rule);
+                        if (!ruleUse.hasAttribute(ad.getAttributeType())) {
+                            // TODO: I18N
+                            logger.warn(LocalizableMessage.raw(
+                                    "The matching rule %s is not valid for attribute type %s",
+                                    matchingRule, attributeDescription));
+                            return UNDEFINED;
+                        }
+                    } catch (final UnknownSchemaElementException e) {
+                        // TODO: I18N
+                        logger.warn(LocalizableMessage.raw("No matching rule use is defined for matching rule %s",
+                                matchingRule));
+                        return UNDEFINED;
+                    }
+                }
+            } else {
+                try {
+                    ruleUse = schema.getMatchingRuleUse(rule);
+                } catch (final UnknownSchemaElementException e) {
+                    // TODO: I18N
+                    logger.warn(LocalizableMessage.raw("No matching rule use is defined for matching rule %s",
+                            matchingRule));
+                    return UNDEFINED;
+                }
+            }
+
+            try {
+                assertion = rule.getAssertion(assertionValue);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The assertion value %s is invalid", assertionValue, de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, ruleUse, assertion, dnAttributes);
+        }
+
+        @Override
+        public MatcherImpl visitGreaterOrEqualFilter(final Schema schema,
+                final String attributeDescription, final ByteString assertionValue) {
+            final AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                        attributeDescription, e));
+
+                return UNDEFINED;
+            }
+
+            final MatchingRule rule = ad.getAttributeType().getOrderingMatchingRule();
+            if (rule == null) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The attribute type %s does not define an ordering matching rule",
+                        attributeDescription));
+                return UNDEFINED;
+            }
+
+            final Assertion assertion;
+            try {
+                assertion = rule.getGreaterOrEqualAssertion(assertionValue);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The assertion value %s is invalid", assertionValue, de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, null, assertion, false);
+        }
+
+        @Override
+        public MatcherImpl visitLessOrEqualFilter(final Schema schema,
+                final String attributeDescription, final ByteString assertionValue) {
+            final AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                        attributeDescription, e));
+                return UNDEFINED;
+            }
+
+            final MatchingRule rule = ad.getAttributeType().getOrderingMatchingRule();
+            if (rule == null) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The attribute type %s does not define an ordering matching rule",
+                        attributeDescription));
+                return UNDEFINED;
+            }
+
+            final Assertion assertion;
+            try {
+                assertion = rule.getLessOrEqualAssertion(assertionValue);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The assertion value %s is invalid", assertionValue , de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, null, assertion, false);
+        }
+
+        @Override
+        public MatcherImpl visitNotFilter(final Schema schema, final Filter subFilter) {
+            final MatcherImpl subMatcher = subFilter.accept(this, schema);
+            return new NotMatcherImpl(subMatcher);
+        }
+
+        @Override
+        public MatcherImpl visitOrFilter(final Schema schema, final List<Filter> subFilters) {
+            if (subFilters.isEmpty()) {
+                logger.trace(LocalizableMessage.raw("Empty or filter component. Will always return FALSE"));
+                return FALSE;
+            }
+
+            final List<MatcherImpl> subMatchers = new ArrayList<>(subFilters.size());
+            for (final Filter f : subFilters) {
+                subMatchers.add(f.accept(this, schema));
+            }
+            return new OrMatcherImpl(subMatchers);
+        }
+
+        @Override
+        public MatcherImpl visitPresentFilter(final Schema schema, final String attributeDescription) {
+            AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                        attributeDescription, e));
+                return UNDEFINED;
+            }
+
+            return new PresentMatcherImpl(ad);
+        }
+
+        @Override
+        public MatcherImpl visitSubstringsFilter(final Schema schema,
+                final String attributeDescription, final ByteString initialSubstring,
+                final List<ByteString> anySubstrings, final ByteString finalSubstring) {
+            final AttributeDescription ad;
+            try {
+                ad = AttributeDescription.valueOf(attributeDescription, schema);
+            } catch (final LocalizedIllegalArgumentException e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Attribute description %s is not recognized",
+                        attributeDescription, e));
+                return UNDEFINED;
+            }
+
+            final MatchingRule rule = ad.getAttributeType().getSubstringMatchingRule();
+            if (rule == null) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The attribute type %s does not define an substring matching rule",
+                        attributeDescription));
+                return UNDEFINED;
+            }
+
+            final Assertion assertion;
+            try {
+                assertion = rule.getSubstringAssertion(initialSubstring, anySubstrings, finalSubstring);
+            } catch (final DecodeException de) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("The substring assertion values contain an invalid value", de));
+                return UNDEFINED;
+            }
+            return new AssertionMatcherImpl(ad, rule, null, assertion, false);
+        }
+
+        @Override
+        public MatcherImpl visitUnrecognizedFilter(final Schema schema, final byte filterTag,
+                final ByteString filterBytes) {
+            // TODO: I18N
+            logger.warn(LocalizableMessage.raw("The type of filtering requested with tag %s is not implemented",
+                    StaticUtils.byteToHex(filterTag)));
+            return UNDEFINED;
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private static final MatcherImpl FALSE = new FalseMatcherImpl();
+    private static final MatcherImpl TRUE = new TrueMatcherImpl();
+    private static final MatcherImpl UNDEFINED = new UndefinedMatcherImpl();
+
+    private static final FilterVisitor<MatcherImpl, Schema> VISITOR = new Visitor();
+
+    private static ConditionResult matches(final Attribute a, final MatchingRule rule,
+            final Assertion assertion) {
+        ConditionResult r = ConditionResult.FALSE;
+        if (a != null) {
+            for (final ByteString v : a) {
+                switch (matches(v, rule, assertion)) {
+                case TRUE:
+                    return ConditionResult.TRUE;
+                case FALSE:
+                    continue;
+                case UNDEFINED:
+                    r = ConditionResult.UNDEFINED;
+                }
+            }
+        }
+        return r;
+    }
+
+    private static ConditionResult matches(final ByteString v, final MatchingRule rule,
+            final Assertion assertion) {
+        try {
+            final ByteString normalizedValue = rule.normalizeAttributeValue(v);
+            return assertion.matches(normalizedValue);
+        } catch (final DecodeException de) {
+            // TODO: I18N
+            logger.warn(LocalizableMessage.raw(
+                    "The attribute value %s is invalid for matching rule %s. Possible schema error?",
+                    v, rule.getNameOrOID(), de));
+            return ConditionResult.UNDEFINED;
+        }
+    }
+
+    private final MatcherImpl impl;
+
+    Matcher(final Filter filter, final Schema schema) {
+        this.impl = filter.accept(VISITOR, schema);
+    }
+
+    /**
+     * Indicates whether this filter {@code Matcher} matches the provided
+     * {@code Entry}.
+     *
+     * @param entry
+     *            The entry to be matched.
+     * @return The result of matching the provided {@code Entry} against this
+     *         filter {@code Matcher}.
+     */
+    public ConditionResult matches(final Entry entry) {
+        return impl.matches(entry);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
new file mode 100644
index 0000000..714817d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
@@ -0,0 +1,623 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
+import static org.forgerock.opendj.ldap.Entries.modifyEntry;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.EntryReader;
+
+/**
+ * A simple in memory back-end which can be used for testing. It is not intended
+ * for production use due to various limitations. The back-end implementations
+ * supports the following:
+ * <ul>
+ * <li>add, bind (simple), compare, delete, modify, and search operations, but
+ * not modifyDN nor extended operations
+ * <li>assertion, pre-, and post- read controls, subtree delete control, and
+ * permissive modify control
+ * <li>thread safety - supports concurrent operations
+ * </ul>
+ * It does not support the following:
+ * <ul>
+ * <li>high performance
+ * <li>secure password storage
+ * <li>schema checking
+ * <li>persistence
+ * <li>indexing
+ * </ul>
+ * This class can be used in conjunction with the factories defined in
+ * {@link Connections} to create simple servers as well as mock LDAP
+ * connections. For example, to create a mock LDAP connection factory:
+ *
+ * <pre>
+ * MemoryBackend backend = new MemoryBackend();
+ * Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
+ *         .getConnection();
+ * </pre>
+ *
+ * To create a simple LDAP server listening on 0.0.0.0:1389:
+ *
+ * <pre>
+ * MemoryBackend backend = new MemoryBackend();
+ * LDAPListener listener = new LDAPListener(1389, Connections
+ *         .&lt;LDAPClientContext&gt; newServerConnectionFactory(backend));
+ * </pre>
+ */
+public final class MemoryBackend implements RequestHandler<RequestContext> {
+    private final DecodeOptions decodeOptions;
+    private final ConcurrentSkipListMap<DN, Entry> entries = new ConcurrentSkipListMap<>();
+    private final Schema schema;
+    private final Object writeLock = new Object();
+
+    /**
+     * Creates a new empty memory backend which will use the default schema.
+     */
+    public MemoryBackend() {
+        this(Schema.getDefaultSchema());
+    }
+
+    /**
+     * Creates a new memory backend which will use the default schema, and will
+     * contain the entries read from the provided entry reader.
+     *
+     * @param reader
+     *            The entry reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entries,
+     *             or if duplicate entries are detected.
+     */
+    public MemoryBackend(final EntryReader reader) throws IOException {
+        this(Schema.getDefaultSchema(), reader);
+    }
+
+    /**
+     * Creates a new empty memory backend which will use the provided schema.
+     *
+     * @param schema
+     *            The schema to use for decoding filters, etc.
+     */
+    public MemoryBackend(final Schema schema) {
+        this.schema = schema;
+        this.decodeOptions = new DecodeOptions().setSchema(schema);
+    }
+
+    /**
+     * Creates a new memory backend which will use the provided schema, and will
+     * contain the entries read from the provided entry reader.
+     *
+     * @param schema
+     *            The schema to use for decoding filters, etc.
+     * @param reader
+     *            The entry reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entries,
+     *             or if duplicate entries are detected.
+     */
+    public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
+        this.schema = schema;
+        this.decodeOptions = new DecodeOptions().setSchema(schema);
+        load(reader, false);
+    }
+
+    /**
+     * Clears the contents of this memory backend so that it does not contain
+     * any entries.
+     *
+     * @return This memory backend.
+     */
+    public MemoryBackend clear() {
+        synchronized (writeLock) {
+            entries.clear();
+        }
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if the named entry exists in this memory backend.
+     *
+     * @param dn
+     *            The name of the entry.
+     * @return {@code true} if the named entry exists in this memory backend.
+     */
+    public boolean contains(final DN dn) {
+        return get(dn) != null;
+    }
+
+    /**
+     * Returns {@code true} if the named entry exists in this memory backend.
+     *
+     * @param dn
+     *            The name of the entry.
+     * @return {@code true} if the named entry exists in this memory backend.
+     */
+    public boolean contains(final String dn) {
+        return get(dn) != null;
+    }
+
+    /**
+     * Returns the named entry contained in this memory backend, or {@code null}
+     * if it does not exist.
+     *
+     * @param dn
+     *            The name of the entry to be returned.
+     * @return The named entry.
+     */
+    public Entry get(final DN dn) {
+        return entries.get(dn);
+    }
+
+    /**
+     * Returns the named entry contained in this memory backend, or {@code null}
+     * if it does not exist.
+     *
+     * @param dn
+     *            The name of the entry to be returned.
+     * @return The named entry.
+     */
+    public Entry get(final String dn) {
+        return get(DN.valueOf(dn, schema));
+    }
+
+    /**
+     * Returns a collection containing all of the entries in this memory
+     * backend. The returned collection is backed by this memory backend, so
+     * changes to the collection are reflected in this memory backend and
+     * vice-versa. The returned collection supports entry removal, iteration,
+     * and is thread safe, but it does not support addition of new entries.
+     *
+     * @return A collection containing all of the entries in this memory
+     *         backend.
+     */
+    public Collection<Entry> getAll() {
+        return entries.values();
+    }
+
+    @Override
+    public void handleAdd(final RequestContext requestContext, final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        try {
+            synchronized (writeLock) {
+                final DN dn = request.getName();
+                final DN parent = dn.parent();
+                if (entries.containsKey(dn)) {
+                    throw newLdapException(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '" + dn + "' already exists");
+                } else if (parent != null && !entries.containsKey(parent)) {
+                    noSuchObject(parent);
+                } else {
+                    entries.put(dn, request);
+                }
+            }
+            resultHandler.handleResult(getResult(request, null, request));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    @Override
+    public void handleBind(final RequestContext requestContext, final int version,
+            final BindRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<BindResult> resultHandler) {
+        try {
+            final Entry entry;
+            synchronized (writeLock) {
+                final DN username = DN.valueOf(request.getName(), schema);
+                final byte[] password;
+                if (request instanceof SimpleBindRequest) {
+                    password = ((SimpleBindRequest) request).getPassword();
+                } else if (request instanceof GenericBindRequest
+                        && request.getAuthenticationType() == BindRequest.AUTHENTICATION_TYPE_SIMPLE) {
+                    password = ((GenericBindRequest) request).getAuthenticationValue();
+                } else {
+                    throw newLdapException(ResultCode.PROTOCOL_ERROR,
+                            "non-SIMPLE authentication not supported: " + request.getAuthenticationType());
+                }
+                entry = getRequiredEntry(null, username);
+                if (!entry.containsAttribute("userPassword", password)) {
+                    throw newLdapException(ResultCode.INVALID_CREDENTIALS, "Wrong password");
+                }
+            }
+            resultHandler.handleResult(getBindResult(request, entry, entry));
+        } catch (final LocalizedIllegalArgumentException e) {
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e));
+        } catch (final EntryNotFoundException e) {
+            /*
+             * Usually you would not include a diagnostic message, but we'll add
+             * one here because the memory back-end is not intended for
+             * production use.
+             */
+            resultHandler.handleException(newLdapException(ResultCode.INVALID_CREDENTIALS, "Unknown user"));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    @Override
+    public void handleCompare(final RequestContext requestContext, final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<CompareResult> resultHandler) {
+        try {
+            final Entry entry;
+            final Attribute assertion;
+            synchronized (writeLock) {
+                final DN dn = request.getName();
+                entry = getRequiredEntry(request, dn);
+                assertion =
+                        singletonAttribute(request.getAttributeDescription(), request
+                                .getAssertionValue());
+            }
+            resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute(
+                    assertion, null)));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    @Override
+    public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        try {
+            final Entry entry;
+            synchronized (writeLock) {
+                final DN dn = request.getName();
+                entry = getRequiredEntry(request, dn);
+                if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) {
+                    // Subtree delete.
+                    entries.subMap(dn, dn.child(RDN.maxValue())).clear();
+                } else {
+                    // Must be leaf.
+                    final DN next = entries.higherKey(dn);
+                    if (next == null || !next.isChildOf(dn)) {
+                        entries.remove(dn);
+                    } else {
+                        throw newLdapException(ResultCode.NOT_ALLOWED_ON_NONLEAF);
+                    }
+                }
+            }
+            resultHandler.handleResult(getResult(request, entry, null));
+        } catch (final DecodeException e) {
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    @Override
+    public <R extends ExtendedResult> void handleExtendedRequest(
+            final RequestContext requestContext, final ExtendedRequest<R> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<R> resultHandler) {
+        resultHandler.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM,
+                "Extended request operation not supported"));
+    }
+
+    @Override
+    public void handleModify(final RequestContext requestContext, final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        try {
+            final Entry entry;
+            final Entry newEntry;
+            synchronized (writeLock) {
+                final DN dn = request.getName();
+                entry = getRequiredEntry(request, dn);
+                newEntry = new LinkedHashMapEntry(entry);
+                entries.put(dn, modifyEntry(newEntry, request));
+            }
+            resultHandler.handleResult(getResult(request, entry, newEntry));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    @Override
+    public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        resultHandler.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM,
+                "ModifyDN request operation not supported"));
+    }
+
+    @Override
+    public void handleSearch(final RequestContext requestContext, final SearchRequest request,
+        final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
+        LdapResultHandler<Result> resultHandler) {
+        try {
+            final DN dn = request.getName();
+            final SearchScope scope = request.getScope();
+            final Filter filter = request.getFilter();
+            final Matcher matcher = filter.matcher(schema);
+            final AttributeFilter attributeFilter =
+                new AttributeFilter(request.getAttributes(), schema).typesOnly(request.isTypesOnly());
+            switch (scope.asEnum()) {
+            case BASE_OBJECT:
+                final Entry baseEntry = getRequiredEntry(request, dn);
+                if (matcher.matches(baseEntry).toBoolean()) {
+                    sendEntry(attributeFilter, entryHandler, baseEntry);
+                }
+                resultHandler.handleResult(newResult(ResultCode.SUCCESS));
+                break;
+
+            case SINGLE_LEVEL:
+            case SUBORDINATES:
+            case WHOLE_SUBTREE:
+                searchWithSubordinates(requestContext, entryHandler, resultHandler, dn, matcher, attributeFilter,
+                    request.getSizeLimit(), scope,
+                    request.getControl(SimplePagedResultsControl.DECODER, new DecodeOptions()));
+                break;
+
+            default:
+                throw newLdapException(ResultCode.PROTOCOL_ERROR,
+                        "Search request contains an unsupported search scope");
+            }
+        } catch (DecodeException e) {
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e.getMessage(), e));
+        } catch (final LdapException e) {
+            resultHandler.handleException(e);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this memory backend does not contain any entries.
+     *
+     * @return {@code true} if this memory backend does not contain any entries.
+     */
+    public boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+    /**
+     * Reads all of the entries from the provided entry reader and adds them to
+     * the content of this memory backend.
+     *
+     * @param reader
+     *            The entry reader.
+     * @param overwrite
+     *            {@code true} if existing entries should be replaced, or
+     *            {@code false} if an error should be returned when duplicate
+     *            entries are encountered.
+     * @return This memory backend.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entries,
+     *             or if duplicate entries are detected and {@code overwrite} is
+     *             {@code false}.
+     */
+    public MemoryBackend load(final EntryReader reader, final boolean overwrite) throws IOException {
+        synchronized (writeLock) {
+            if (reader != null) {
+                try {
+                    while (reader.hasNext()) {
+                        final Entry entry = reader.readEntry();
+                        final DN dn = entry.getName();
+                        if (!overwrite && entries.containsKey(dn)) {
+                            throw newLdapException(ResultCode.ENTRY_ALREADY_EXISTS,
+                                    "Attempted to add the entry '" + dn + "' multiple times");
+                        } else {
+                            entries.put(dn, entry);
+                        }
+                    }
+                } finally {
+                    reader.close();
+                }
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Returns the number of entries contained in this memory backend.
+     *
+     * @return The number of entries contained in this memory backend.
+     */
+    public int size() {
+        return entries.size();
+    }
+
+    /**
+     * Perform a search for scope that includes subordinates, i.e., either
+     * <code>SearchScope.SINGLE_LEVEL</code> or <code>SearchScope.WHOLE_SUBTREE</code>.
+     *
+     * @param requestContext context of this request
+     * @param resultHandler handler which should be used to send back the search results to the client.
+     * @param dn distinguished name of the base entry used for this request
+     * @param matcher to filter entries that matches this request
+     * @param attributeFilter to select attributes to return in search results
+     * @param sizeLimit maximum number of entries to return. A value of zero indicates no restriction
+     *          on number of entries.
+     * @param pagedResults The simple paged results control, if present.
+     * @throws CancelledResultException
+     *           If a cancellation request has been received and processing of
+     *           the request should be aborted if possible.
+     * @throws LdapException
+     *           If the request is unsuccessful.
+     */
+    private void searchWithSubordinates(final RequestContext requestContext, final SearchResultHandler entryHandler,
+            final LdapResultHandler<Result> resultHandler, final DN dn, final Matcher matcher,
+            final AttributeFilter attributeFilter, final int sizeLimit, SearchScope scope,
+            SimplePagedResultsControl pagedResults) throws CancelledResultException, LdapException {
+        final int pageSize = pagedResults != null ? pagedResults.getSize() : 0;
+        final int offset = (pagedResults != null && !pagedResults.getCookie().isEmpty())
+                ? Integer.valueOf(pagedResults.getCookie().toString()) : 0;
+        final Map<DN, Entry> subtree = entries.subMap(dn, dn.child(RDN.maxValue()));
+        int numberOfResults = 0;
+        int position = 0;
+        for (final Entry entry : subtree.values()) {
+            requestContext.checkIfCancelled(false);
+            if (scope.equals(SearchScope.WHOLE_SUBTREE) || entry.getName().isChildOf(dn)
+                    || (scope.equals(SearchScope.SUBORDINATES) && !entry.getName().equals(dn))) {
+                if (matcher.matches(entry).toBoolean()) {
+                    /*
+                     * This entry is going to be returned to the client so it
+                     * counts towards the size limit and any paging criteria.
+                     */
+
+                    // Check size limit.
+                    if (sizeLimit > 0 && numberOfResults >= sizeLimit) {
+                        throw newLdapException(newResult(ResultCode.SIZE_LIMIT_EXCEEDED));
+                    }
+
+                    // Ignore this entry if we haven't reached the first page yet.
+                    if (pageSize > 0 && position++ < offset) {
+                        continue;
+                    }
+
+                    // Send the entry back to the client.
+                    if (!sendEntry(attributeFilter, entryHandler, entry)) {
+                        // Client has disconnected or cancelled.
+                        break;
+                    }
+
+                    numberOfResults++;
+
+                    // Stop if we've reached the end of the page.
+                    if (pageSize > 0 && numberOfResults == pageSize) {
+                        break;
+                    }
+                }
+            }
+        }
+        final Result result = newResult(ResultCode.SUCCESS);
+        if (pageSize > 0) {
+            final ByteString cookie = numberOfResults == pageSize ? ByteString.valueOfUtf8(String.valueOf(position))
+                    : ByteString.empty();
+            result.addControl(SimplePagedResultsControl.newControl(true, 0, cookie));
+        }
+        resultHandler.handleResult(result);
+    }
+
+    private <R extends Result> R addResultControls(final Request request, final Entry before,
+            final Entry after, final R result) throws LdapException {
+        try {
+            // Add pre-read response control if requested.
+            final PreReadRequestControl preRead =
+                    request.getControl(PreReadRequestControl.DECODER, decodeOptions);
+            if (preRead != null) {
+                if (preRead.isCritical() && before == null) {
+                    throw newLdapException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+                } else {
+                    final AttributeFilter filter =
+                            new AttributeFilter(preRead.getAttributes(), schema);
+                    result.addControl(PreReadResponseControl.newControl(filter
+                            .filteredViewOf(before)));
+                }
+            }
+
+            // Add post-read response control if requested.
+            final PostReadRequestControl postRead =
+                    request.getControl(PostReadRequestControl.DECODER, decodeOptions);
+            if (postRead != null) {
+                if (postRead.isCritical() && after == null) {
+                    throw newLdapException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+                } else {
+                    final AttributeFilter filter =
+                            new AttributeFilter(postRead.getAttributes(), schema);
+                    result.addControl(PostReadResponseControl.newControl(filter
+                            .filteredViewOf(after)));
+                }
+            }
+            return result;
+        } catch (final DecodeException e) {
+            throw newLdapException(ResultCode.PROTOCOL_ERROR, e);
+        }
+    }
+
+    private BindResult getBindResult(final BindRequest request, final Entry before,
+            final Entry after) throws LdapException {
+        return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
+    }
+
+    private CompareResult getCompareResult(final CompareRequest request, final Entry entry,
+            final boolean compareResult) throws LdapException {
+        return addResultControls(
+                request,
+                entry,
+                entry,
+                newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE));
+    }
+
+    private Entry getRequiredEntry(final Request request, final DN dn) throws LdapException {
+        final Entry entry = entries.get(dn);
+        if (entry == null) {
+            noSuchObject(dn);
+        } else if (request != null) {
+            AssertionRequestControl control;
+            try {
+                control = request.getControl(AssertionRequestControl.DECODER, decodeOptions);
+            } catch (final DecodeException e) {
+                throw newLdapException(ResultCode.PROTOCOL_ERROR, e);
+            }
+            if (control != null) {
+                final Filter filter = control.getFilter();
+                final Matcher matcher = filter.matcher(schema);
+                if (!matcher.matches(entry).toBoolean()) {
+                    throw newLdapException(ResultCode.ASSERTION_FAILED,
+                            "The filter '" + filter + "' did not match the entry '" + entry.getName() + "'");
+                }
+            }
+        }
+        return entry;
+    }
+
+    private Result getResult(final Request request, final Entry before, final Entry after) throws LdapException {
+        return addResultControls(request, before, after, newResult(ResultCode.SUCCESS));
+    }
+
+    private void noSuchObject(final DN dn) throws LdapException {
+        throw newLdapException(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn + "' does not exist");
+    }
+
+    private boolean sendEntry(final AttributeFilter filter,
+            final SearchResultHandler resultHandler, final Entry entry) {
+        return resultHandler.handleEntry(newSearchResultEntry(filter.filteredViewOf(entry)));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Modification.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Modification.java
new file mode 100644
index 0000000..38717d8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Modification.java
@@ -0,0 +1,86 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.util.Reject;
+
+/** A modification to be performed on an entry during a Modify operation. */
+public final class Modification {
+    private final ModificationType modificationType;
+    private final Attribute attribute;
+
+    /**
+     * Creates a new modification having the provided modification type and
+     * attribute values to be updated. Note that while the returned
+     * {@code Modification} is immutable, the underlying attribute may not be.
+     * The following code ensures that the returned {@code Modification} is
+     * fully immutable:
+     *
+     * <pre>
+     * Modification change = new Modification(modificationType, Attributes
+     *         .unmodifiableAttribute(attribute));
+     * </pre>
+     *
+     * @param modificationType
+     *            The type of modification to be performed.
+     * @param attribute
+     *            The the attribute containing the values to be modified.
+     */
+    public Modification(final ModificationType modificationType, final Attribute attribute) {
+        Reject.ifNull(modificationType, attribute);
+        this.modificationType = modificationType;
+        this.attribute = attribute;
+    }
+
+    /**
+     * Returns the attribute containing the values to be modified.
+     *
+     * @return The the attribute containing the values to be modified.
+     */
+    public Attribute getAttribute() {
+        return attribute;
+    }
+
+    /**
+     * Returns the type of modification to be performed.
+     *
+     * @return The type of modification to be performed.
+     */
+    public ModificationType getModificationType() {
+        return modificationType;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Modification(modificationType=");
+        builder.append(modificationType);
+        builder.append(", attributeDescription=");
+        builder.append(attribute.getAttributeDescriptionAsString());
+        builder.append(", attributeValues={");
+        boolean firstValue = true;
+        for (final ByteString value : attribute) {
+            if (!firstValue) {
+                builder.append(", ");
+            }
+            builder.append(value);
+            firstValue = false;
+        }
+        builder.append("})");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ModificationType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ModificationType.java
new file mode 100644
index 0000000..eb530b2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ModificationType.java
@@ -0,0 +1,193 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A Modify operation change type as defined in RFC 4511 section 4.6 is used to
+ * specify the type of modification being performed on an attribute.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511#section-4.6">RFC 4511 -
+ *      Lightweight Directory Access Protocol (LDAP): The Protocol </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4525">RFC 4525 - Lightweight
+ *      Directory Access Protocol (LDAP) Modify-Increment Extension </a>
+ */
+public final class ModificationType {
+    /**
+     * Contains equivalent values for the ModificationType values.
+     * This allows easily using ModificationType values with switch statements.
+     */
+    public static enum Enum {
+        //@Checkstyle:off
+        /** @see ModificationType#ADD */
+        ADD,
+        /** @see ModificationType#DELETE */
+        DELETE,
+        /** @see ModificationType#REPLACE */
+        REPLACE,
+        /** @see ModificationType#INCREMENT */
+        INCREMENT,
+        /** Used for unknown modification types. */
+        UNKNOWN;
+        //@Checkstyle:on
+    }
+
+    private static final ModificationType[] ELEMENTS = new ModificationType[4];
+
+    private static final List<ModificationType> IMMUTABLE_ELEMENTS = Collections
+            .unmodifiableList(Arrays.asList(ELEMENTS));
+
+    /**
+     * Add the values listed in the modification to the attribute, creating the
+     * attribute if necessary.
+     */
+    public static final ModificationType ADD = register(0, "add", Enum.ADD);
+
+    /**
+     * Delete the values listed in the modification from the attribute. If no
+     * values are listed, or if all current values of the attribute are listed,
+     * the entire attribute is removed.
+     */
+    public static final ModificationType DELETE = register(1, "delete", Enum.DELETE);
+
+    /**
+     * Replace all existing values of the attribute with the new values listed
+     * in the modification, creating the attribute if it did not already exist.
+     * A replace with no listed values will delete the entire attribute if it
+     * exists, and it is ignored if the attribute does not exist.
+     */
+    public static final ModificationType REPLACE = register(2, "replace", Enum.REPLACE);
+
+    /**
+     * Increment all existing values of the attribute by the amount specified in
+     * the modification value.
+     */
+    public static final ModificationType INCREMENT = register(3, "increment", Enum.INCREMENT);
+
+    /**
+     * Returns the modification change type having the specified integer value
+     * as defined in RFC 4511 section 4.6.
+     *
+     * @param intValue
+     *            The integer value of the modification change type.
+     * @return The modification change type, or {@code null} if there was no
+     *         modification change type associated with {@code intValue}.
+     */
+    public static ModificationType valueOf(final int intValue) {
+        ModificationType result = null;
+        if (0 <= intValue && intValue < ELEMENTS.length) {
+            result = ELEMENTS[intValue];
+        }
+        if (result == null) {
+            result = new ModificationType(intValue, "unknown(" + intValue + ")", Enum.UNKNOWN);
+        }
+        return result;
+    }
+
+    /**
+     * Returns an unmodifiable list containing the set of available modification
+     * change types indexed on their integer value as defined in RFC 4511
+     * section 4.6.
+     *
+     * @return An unmodifiable list containing the set of available modification
+     *         change types.
+     */
+    public static List<ModificationType> values() {
+        return IMMUTABLE_ELEMENTS;
+    }
+
+    /**
+     * Creates and registers a new modification change type with the
+     * application.
+     *
+     * @param intValue
+     *            The integer value of the modification change type as defined
+     *            in RFC 4511 section 4.6.
+     * @param name
+     *            The name of the modification change type.
+     * @param modificationTypeEnum
+     *            The enum equivalent for this modification type
+     * @return The new modification change type.
+     */
+    private static ModificationType register(final int intValue, final String name, final Enum modificationTypeEnum) {
+        final ModificationType t = new ModificationType(intValue, name, modificationTypeEnum);
+        ELEMENTS[intValue] = t;
+        return t;
+    }
+
+    private final int intValue;
+
+    private final String name;
+
+    private final Enum modificationTypeEnum;
+
+    /** Prevent direct instantiation. */
+    private ModificationType(final int intValue, final String name, final Enum modificationTypeEnum) {
+        this.intValue = intValue;
+        this.name = name;
+        this.modificationTypeEnum = modificationTypeEnum;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof ModificationType) {
+            return this.intValue == ((ModificationType) obj).intValue;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return intValue;
+    }
+
+    /**
+     * Returns the integer value of this modification change type as defined in
+     * RFC 4511 section 4.6.
+     *
+     * @return The integer value of this modification change type.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    /**
+     * Returns the enum equivalent for this modification type.
+     *
+     * @return The enum equivalent for this modification type when a known mapping exists,
+     *         or {@link Enum#UNKNOWN} if this is an unknown modification type.
+     */
+    public Enum asEnum() {
+        return this.modificationTypeEnum;
+    }
+
+    /**
+     * Returns the string representation of this modification change type.
+     *
+     * @return The string representation of this modification change type.
+     */
+    @Override
+    public String toString() {
+        return name;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MultipleEntriesFoundException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MultipleEntriesFoundException.java
new file mode 100644
index 0000000..b81ba27
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/MultipleEntriesFoundException.java
@@ -0,0 +1,35 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the requested
+ * single entry search operation or read operation failed because the Directory
+ * Server returned multiple matching entries (or search references) when only a
+ * single matching entry was expected. More specifically, this exception is used
+ * for the {@link ResultCode#CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED
+ * CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED} error result codes.
+ */
+@SuppressWarnings("serial")
+public class MultipleEntriesFoundException extends LdapException {
+    MultipleEntriesFoundException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ProviderNotFoundException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ProviderNotFoundException.java
new file mode 100644
index 0000000..b16d567
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ProviderNotFoundException.java
@@ -0,0 +1,66 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.spi.Provider;
+
+/**
+ * Exception thrown when a provider of a service can't be found.
+ */
+@SuppressWarnings("serial")
+public class ProviderNotFoundException extends RuntimeException {
+
+    private final Class<? extends Provider> providerType;
+    private final String providerName;
+
+    /**
+     * Creates the exception with a provider type, provider name and a message.
+     *
+     * @param providerClass
+     *            the provider class
+     * @param providerName
+     *            the name of the provider implementation that was requested
+     * @param message
+     *            the detail message
+     */
+    public ProviderNotFoundException(final Class<? extends Provider> providerClass, final String providerName,
+            final String message) {
+        super(message);
+        this.providerType = providerClass;
+        this.providerName = providerName;
+    }
+
+    /**
+     * Returns the type of provider.
+     *
+     * @return the provider class
+     */
+    public Class<?> getProviderType() {
+        return providerType;
+    }
+
+    /**
+     * Returns the name of provider.
+     *
+     * @return the name of the provider implementation that was requested, or
+     *         null if the default provider was requested.
+     */
+    public String getProviderName() {
+        return providerName;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
new file mode 100644
index 0000000..5430c05
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RDN.java
@@ -0,0 +1,576 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_DUPLICATE_AVA_TYPES;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_NO_AVAS;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TRAILING_GARBAGE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_RDN_TYPE_NOT_FOUND;
+import static org.forgerock.opendj.ldap.DN.AVA_CHAR_SEPARATOR;
+import static org.forgerock.opendj.ldap.DN.NORMALIZED_AVA_SEPARATOR;
+import static org.forgerock.opendj.ldap.DN.NORMALIZED_RDN_SEPARATOR;
+import static org.forgerock.opendj.ldap.DN.RDN_CHAR_SEPARATOR;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.Iterators;
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * A relative distinguished name (RDN) as defined in RFC 4512 section 2.3 is the
+ * name of an entry relative to its immediate superior. An RDN is composed of an
+ * unordered set of one or more attribute value assertions (AVA) consisting of
+ * an attribute description with zero options and an attribute value. These AVAs
+ * are chosen to match attribute values (each a distinguished value) of the
+ * entry.
+ * <p>
+ * An entry's relative distinguished name must be unique among all immediate
+ * subordinates of the entry's immediate superior (i.e. all siblings).
+ * <p>
+ * The following are examples of string representations of RDNs:
+ *
+ * <pre>
+ * uid=12345
+ * ou=Engineering
+ * cn=Kurt Zeilenga+L=Redwood Shores
+ * </pre>
+ *
+ * The last is an example of a multi-valued RDN; that is, an RDN composed of
+ * multiple AVAs.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.3">RFC 4512 -
+ *      Lightweight Directory Access Protocol (LDAP): Directory Information
+ *      Models </a>
+ */
+public final class RDN implements Iterable<AVA>, Comparable<RDN> {
+
+    /**
+     * A constant holding a special RDN having zero AVAs
+     * and which sorts before any RDN other than itself.
+     */
+    private static final RDN MIN_VALUE = new RDN();
+    /**
+     * A constant holding a special RDN having zero AVAs
+     * and which sorts after any RDN other than itself.
+     */
+    private static final RDN MAX_VALUE = new RDN();
+
+    /**
+     * Returns a constant containing a special RDN which sorts before any
+     * RDN other than itself. This RDN may be used in order to perform
+     * range queries on DN keyed collections such as {@code SortedSet}s and
+     * {@code SortedMap}s. For example, the following code can be used to
+     * construct a range whose contents is a sub-tree of entries, excluding the base entry:
+     *
+     * <pre>
+     * SortedMap<DN, Entry> entries = ...;
+     * DN baseDN = ...;
+     *
+     * // Returns a map containing the baseDN and all of its subordinates.
+     * SortedMap<DN,Entry> subtree = entries.subMap(
+     *     baseDN.child(RDN.minValue()), baseDN.child(RDN.maxValue()));
+     * </pre>
+     *
+     * @return A constant containing a special RDN which sorts before any
+     *         RDN other than itself.
+     * @see #maxValue()
+     */
+    public static RDN minValue() {
+        return MIN_VALUE;
+    }
+
+    /**
+     * Returns a constant containing a special RDN which sorts after any
+     * RDN other than itself. This RDN may be used in order to perform
+     * range queries on DN keyed collections such as {@code SortedSet}s and
+     * {@code SortedMap}s. For example, the following code can be used to
+     * construct a range whose contents is a sub-tree of entries:
+     *
+     * <pre>
+     * SortedMap<DN, Entry> entries = ...;
+     * DN baseDN = ...;
+     *
+     * // Returns a map containing the baseDN and all of its subordinates.
+     * SortedMap<DN,Entry> subtree = entries.subMap(baseDN, baseDN.child(RDN.maxValue()));
+     * </pre>
+     *
+     * @return A constant containing a special RDN which sorts after any
+     *         RDN other than itself.
+     * @see #minValue()
+     */
+    public static RDN maxValue() {
+        return MAX_VALUE;
+    }
+
+    /**
+     * Parses the provided LDAP string representation of an RDN using the
+     * default schema.
+     *
+     * @param rdn
+     *            The LDAP string representation of a RDN.
+     * @return The parsed RDN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code rdn} is not a valid LDAP string representation of a
+     *             RDN.
+     * @throws NullPointerException
+     *             If {@code rdn} was {@code null}.
+     */
+    public static RDN valueOf(final String rdn) {
+        return valueOf(rdn, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Parses the provided LDAP string representation of a RDN using the
+     * provided schema.
+     *
+     * @param rdn
+     *            The LDAP string representation of a RDN.
+     * @param schema
+     *            The schema to use when parsing the RDN.
+     * @return The parsed RDN.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code rdn} is not a valid LDAP string representation of a
+     *             RDN.
+     * @throws NullPointerException
+     *             If {@code rdn} or {@code schema} was {@code null}.
+     */
+    public static RDN valueOf(final String rdn, final Schema schema) {
+        final SubstringReader reader = new SubstringReader(rdn);
+        final RDN parsedRdn;
+        try {
+            parsedRdn = decode(reader, schema);
+        } catch (final UnknownSchemaElementException e) {
+            throw new LocalizedIllegalArgumentException(ERR_RDN_TYPE_NOT_FOUND.get(rdn, e.getMessageObject()));
+        }
+        if (reader.remaining() > 0) {
+            throw new LocalizedIllegalArgumentException(
+                    ERR_RDN_TRAILING_GARBAGE.get(rdn, reader.read(reader.remaining())));
+        }
+        return parsedRdn;
+    }
+
+    static RDN decode(final SubstringReader reader, final Schema schema) {
+        final AVA firstAVA = AVA.decode(reader, schema);
+
+        // Skip over any spaces that might be after the attribute value.
+        reader.skipWhitespaces();
+
+        reader.mark();
+        if (reader.remaining() > 0 && reader.read() == '+') {
+            final List<AVA> avas = new ArrayList<>();
+            avas.add(firstAVA);
+
+            do {
+                avas.add(AVA.decode(reader, schema));
+
+                // Skip over any spaces that might be after the attribute value.
+                reader.skipWhitespaces();
+
+                reader.mark();
+            } while (reader.remaining() > 0 && reader.read() == '+');
+
+            reader.reset();
+            return new RDN(avas);
+        } else {
+            reader.reset();
+            return new RDN(firstAVA);
+        }
+    }
+
+    /** In original order. */
+    private final AVA[] avas;
+
+    /**
+     * We need to store the original string value if provided in order to
+     * preserve the original whitespace.
+     */
+    private String stringValue;
+
+    /**
+     * Creates a new RDN using the provided attribute type and value.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeValue
+     *            The attribute value.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code attributeValue} was
+     *             {@code null}.
+     */
+    public RDN(final AttributeType attributeType, final Object attributeValue) {
+        this.avas = new AVA[] { new AVA(attributeType, attributeValue) };
+    }
+
+    /**
+     * Creates a new RDN using the provided attribute type and value decoded
+     * using the default schema.
+     * <p>
+     * If {@code attributeValue} is not an instance of {@code ByteString} then
+     * it will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @param attributeValue
+     *            The attribute value.
+     * @throws UnknownSchemaElementException
+     *             If {@code attributeType} was not found in the default schema.
+     * @throws NullPointerException
+     *             If {@code attributeType} or {@code attributeValue} was
+     *             {@code null}.
+     */
+    public RDN(final String attributeType, final Object attributeValue) {
+        this.avas = new AVA[] { new AVA(attributeType, attributeValue) };
+    }
+
+    /**
+     * Creates a new RDN using the provided AVAs.
+     *
+     * @param avas
+     *            The attribute-value assertions used to build this RDN.
+     * @throws NullPointerException
+     *             If {@code avas} is {@code null} or contains a null ava.
+     * @throws IllegalArgumentException
+     *             If {@code avas} is empty.
+     */
+    public RDN(final AVA... avas) {
+        Reject.ifNull(avas);
+        this.avas = validateAvas(avas);
+    }
+
+    private AVA[] validateAvas(final AVA[] avas) {
+        switch (avas.length) {
+        case 0:
+            throw new LocalizedIllegalArgumentException(ERR_RDN_NO_AVAS.get());
+        case 1:
+            // Guaranteed to be valid.
+            break;
+        case 2:
+            if (avas[0].getAttributeType().equals(avas[1].getAttributeType())) {
+                throw new LocalizedIllegalArgumentException(
+                        ERR_RDN_DUPLICATE_AVA_TYPES.get(avas[0].getAttributeName()));
+            }
+            break;
+        default:
+            final AVA[] sortedAVAs = Arrays.copyOf(avas, avas.length);
+            Arrays.sort(sortedAVAs);
+            AttributeType previousAttributeType = null;
+            for (AVA ava : sortedAVAs) {
+                if (ava.getAttributeType().equals(previousAttributeType)) {
+                    throw new LocalizedIllegalArgumentException(
+                            ERR_RDN_DUPLICATE_AVA_TYPES.get(ava.getAttributeName()));
+                }
+                previousAttributeType = ava.getAttributeType();
+            }
+        }
+        return avas;
+    }
+
+    /**
+     * Creates a new RDN using the provided AVAs.
+     *
+     * @param avas
+     *            The attribute-value assertions used to build this RDN.
+     * @throws NullPointerException
+     *             If {@code ava} is {@code null} or contains null ava.
+     * @throws IllegalArgumentException
+     *             If {@code avas} is empty.
+     */
+    public RDN(Collection<AVA> avas) {
+        Reject.ifNull(avas);
+        this.avas = validateAvas(avas.toArray(new AVA[avas.size()]));
+    }
+
+    // Special constructor for min/max RDN values.
+    private RDN() {
+        this.avas = new AVA[0];
+        this.stringValue = "";
+    }
+
+    @Override
+    public int compareTo(final RDN rdn) {
+        // FIXME how about replacing this method body with the following code?
+        // return toNormalizedByteString().compareTo(rdn.toNormalizedByteString())
+
+        // Identity.
+        if (this == rdn) {
+            return 0;
+        }
+
+        // MAX_VALUE is always greater than any other RDN other than itself.
+        if (this == MAX_VALUE) {
+            return 1;
+        }
+        if (rdn == MAX_VALUE) {
+            return -1;
+        }
+
+        // MIN_VALUE is always less than any other RDN other than itself.
+        if (this == MIN_VALUE) {
+            return -1;
+        }
+        if (rdn == MIN_VALUE) {
+            return 1;
+        }
+
+        // Compare number of AVAs first as this is quick and easy.
+        final int sz1 = avas.length;
+        final int sz2 = rdn.avas.length;
+        if (sz1 != sz2) {
+            return sz1 - sz2 > 0 ? 1 : -1;
+        }
+
+        // Fast path for common case.
+        if (sz1 == 1) {
+            return avas[0].compareTo(rdn.avas[0]);
+        }
+
+        // Need to sort the AVAs before comparing.
+        final AVA[] a1 = new AVA[sz1];
+        System.arraycopy(avas, 0, a1, 0, sz1);
+        Arrays.sort(a1);
+
+        final AVA[] a2 = new AVA[sz1];
+        System.arraycopy(rdn.avas, 0, a2, 0, sz1);
+        Arrays.sort(a2);
+
+        for (int i = 0; i < sz1; i++) {
+            final int result = a1[i].compareTo(a2[i]);
+            if (result != 0) {
+                return result;
+            }
+        }
+
+        return 0;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof RDN) {
+            return compareTo((RDN) obj) == 0;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the attribute value contained in this RDN which is associated
+     * with the provided attribute type, or {@code null} if this RDN does not
+     * include such an attribute value.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @return The attribute value.
+     */
+    public ByteString getAttributeValue(final AttributeType attributeType) {
+        for (final AVA ava : avas) {
+            if (ava.getAttributeType().equals(attributeType)) {
+                return ava.getAttributeValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the first AVA contained in this RDN.
+     *
+     * @return The first AVA contained in this RDN.
+     */
+    public AVA getFirstAVA() {
+        return avas[0];
+    }
+
+    @Override
+    public int hashCode() {
+        // Avoid an algorithm that requires the AVAs to be sorted.
+        int hash = 0;
+        for (final AVA ava : avas) {
+            hash += ava.hashCode();
+        }
+        return hash;
+    }
+
+    /**
+     * Returns {@code true} if this RDN contains more than one AVA.
+     *
+     * @return {@code true} if this RDN contains more than one AVA, otherwise
+     *         {@code false}.
+     */
+    public boolean isMultiValued() {
+        return avas.length > 1;
+    }
+
+    /**
+     * Indicates whether this RDN includes the specified attribute type.
+     *
+     * @param attributeType  The attribute type for which to make the determination.
+     * @return {@code true} if the RDN includes the specified attribute type,
+     *         or {@code false} if not.
+     */
+    public boolean hasAttributeType(AttributeType attributeType) {
+        for (AVA ava : avas) {
+            if (ava.getAttributeType().equals(attributeType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns an iterator of the AVAs contained in this RDN. The AVAs will be
+     * returned in the user provided order.
+     * <p>
+     * Attempts to remove AVAs using an iterator's {@code remove()} method are
+     * not permitted and will result in an {@code UnsupportedOperationException}
+     * being thrown.
+     *
+     * @return An iterator of the AVAs contained in this RDN.
+     */
+    @Override
+    public Iterator<AVA> iterator() {
+        return Iterators.arrayIterator(avas);
+    }
+
+    /**
+     * Returns the number of AVAs in this RDN.
+     *
+     * @return The number of AVAs in this RDN.
+     */
+    public int size() {
+        return avas.length;
+    }
+
+    /**
+     * Returns the RFC 4514 string representation of this RDN.
+     *
+     * @return The RFC 4514 string representation of this RDN.
+     * @see <a href="http://tools.ietf.org/html/rfc4514">RFC 4514 - Lightweight
+     *      Directory Access Protocol (LDAP): String Representation of
+     *      Distinguished Names </a>
+     */
+    @Override
+    public String toString() {
+        // We don't care about potential race conditions here.
+        if (stringValue == null) {
+            final StringBuilder builder = new StringBuilder();
+            avas[0].toString(builder);
+            for (int i = 1; i < avas.length; i++) {
+                builder.append(AVA_CHAR_SEPARATOR);
+                avas[i].toString(builder);
+            }
+            stringValue = builder.toString();
+        }
+        return stringValue;
+    }
+
+    StringBuilder toString(final StringBuilder builder) {
+        return builder.append(this);
+    }
+
+    /**
+     * Returns the normalized byte string representation of this RDN.
+     * <p>
+     * The representation is not a valid RDN.
+     *
+     * @param builder
+     *            The builder to use to construct the normalized byte string.
+     * @return The normalized byte string representation.
+     * @see DN#toNormalizedByteString()
+     */
+    ByteStringBuilder toNormalizedByteString(final ByteStringBuilder builder) {
+        switch (size()) {
+        case 0:
+            if (this == MIN_VALUE) {
+                builder.appendByte(NORMALIZED_RDN_SEPARATOR);
+            } else { // can only be MAX_VALUE
+                builder.appendByte(NORMALIZED_AVA_SEPARATOR);
+            }
+            break;
+        case 1:
+            builder.appendByte(NORMALIZED_RDN_SEPARATOR);
+            getFirstAVA().toNormalizedByteString(builder);
+            break;
+        default:
+            builder.appendByte(NORMALIZED_RDN_SEPARATOR);
+            Iterator<AVA> it = getSortedAvas();
+            it.next().toNormalizedByteString(builder);
+            while (it.hasNext()) {
+                builder.appendByte(NORMALIZED_AVA_SEPARATOR);
+                it.next().toNormalizedByteString(builder);
+            }
+            break;
+        }
+        return builder;
+    }
+
+    /**
+     * Retrieves a normalized string representation of this RDN.
+     * <p>
+     * This representation is safe to use in an URL or in a file name.
+     * However, it is not a valid RDN and can't be reverted to a valid RDN.
+     *
+     * @return The normalized string representation of this RDN.
+     * @see DN#toNormalizedUrlSafeString()
+     */
+    StringBuilder toNormalizedUrlSafeString(final StringBuilder builder) {
+        switch (size()) {
+        case 0:
+            // since MIN_VALUE and MAX_VALUE are only used for sorting DNs,
+            // it is strange to call toNormalizedUrlSafeString() on one of them
+            if (this == MIN_VALUE) {
+                builder.append(RDN_CHAR_SEPARATOR);
+            } else { // can only be MAX_VALUE
+                builder.append(AVA_CHAR_SEPARATOR);
+            }
+            break;
+        case 1:
+            getFirstAVA().toNormalizedUrlSafe(builder);
+            break;
+        default:
+            Iterator<AVA> it = getSortedAvas();
+            it.next().toNormalizedUrlSafe(builder);
+            while (it.hasNext()) {
+                builder.append(AVA_CHAR_SEPARATOR);
+                it.next().toNormalizedUrlSafe(builder);
+            }
+            break;
+        }
+        return builder;
+    }
+
+    private Iterator<AVA> getSortedAvas() {
+        TreeSet<AVA> sortedAvas = new TreeSet<>();
+        Collections.addAll(sortedAvas, avas);
+        return sortedAvas.iterator();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ReferralException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ReferralException.java
new file mode 100644
index 0000000..98936b6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ReferralException.java
@@ -0,0 +1,32 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * could not be processed by the Directory Server because the target entry is
+ * located on another server. More specifically, this exception is used for the
+ * {@link ResultCode#REFERRAL REFERRAL} result code.
+ */
+@SuppressWarnings("serial")
+public class ReferralException extends EntryNotFoundException {
+    ReferralException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestContext.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestContext.java
new file mode 100644
index 0000000..cbcdb46
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestContext.java
@@ -0,0 +1,96 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * The context associated with a request currently being processed by a request
+ * handler. A request context can be used to query state information about the
+ * request, such as whether or not it has been cancelled, as well as registering
+ * to be notified if the request has been cancelled. Implementations may provide
+ * additional information, such as the associated schema, time-stamp
+ * information, etc.
+ */
+public interface RequestContext {
+
+    /**
+     * Registers the provided cancellation listener with this request context so
+     * that it can be notified if a cancellation request is received and
+     * processing of the request should be aborted if possible.
+     * <p>
+     * Requests may be cancelled as a result of an abandon request or a cancel
+     * extended request sent from the client, or by the server itself (e.g.
+     * during server shutdown).
+     * <p>
+     * This method provides a event notification mechanism which can be used by
+     * asynchronous request handler implementations to detect cancellation of
+     * requests.
+     *
+     * @param listener
+     *            The listener which wants to be notified if a cancellation
+     *            request is received and processing of the request should be
+     *            aborted if possible.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     * @see #checkIfCancelled
+     */
+    void addCancelRequestListener(CancelRequestListener listener);
+
+    /**
+     * Throws {@link CancelledResultException} if a cancellation request has
+     * been received and processing of the request should be aborted if
+     * possible.
+     * <p>
+     * Requests may be cancelled as a result of an abandon request or a cancel
+     * extended request sent from the client, or by the server itself (e.g.
+     * during server shutdown).
+     * <p>
+     * This method provides a polling mechanism which can be used by synchronous
+     * request handler implementations to detect cancellation of requests.
+     *
+     * @param signalTooLate
+     *            {@code true} to signal that, after this method returns,
+     *            processing of the request will have proceeded too far for it
+     *            to be aborted by subsequent cancellation requests.
+     * @throws CancelledResultException
+     *             If a cancellation request has been received and processing of
+     *             the request should be aborted if possible.
+     * @see #addCancelRequestListener
+     */
+    void checkIfCancelled(boolean signalTooLate) throws CancelledResultException;
+
+    /**
+     * Returns the message ID of the request, if available. Protocols, such as
+     * LDAP and internal connections, include a unique message ID with each
+     * request which may be useful for logging and auditing.
+     *
+     * @return The message ID of the request, or {@code -1} if there is no
+     *         message ID associated with the request.
+     */
+    int getMessageID();
+
+    /**
+     * Removes the provided cancellation listener from this request context so
+     * that it will not be notified if a cancellation request has been received.
+     *
+     * @param listener
+     *            The listener which no longer wants to be notified if a
+     *            cancellation request has been received.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     */
+    void removeCancelRequestListener(CancelRequestListener listener);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandler.java
new file mode 100644
index 0000000..50dd0f2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandler.java
@@ -0,0 +1,218 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * A handler interface for processing client requests.
+ * <p>
+ * Implementations must always return results using the provided
+ * {@link LdapResultHandler} unless explicitly permitted.
+ * <p>
+ * For example, an {@link LDAPListener} does not require {@code RequestHandler}
+ * implementations to return results, which may be useful when implementing
+ * abandon operation functionality. Conversely, an access logger implemented as
+ * a {@code RequestHandler} wrapper will require wrapped {@code RequestHandler}s
+ * to always return results, even abandoned results, in order for it to log the
+ * result status.
+ *
+ * @param <C>
+ *            The type of request context.
+ * @see ServerConnectionFactory
+ */
+public interface RequestHandler<C> {
+
+    /**
+     * Invoked when an add request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The add request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle add requests.
+     */
+    void handleAdd(C requestContext, AddRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<Result> resultHandler);
+
+    /**
+     * Invoked when a bind request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param version
+     *            The protocol version included with the bind request.
+     * @param request
+     *            The bind request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle bind requests.
+     */
+    void handleBind(C requestContext, int version, BindRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<BindResult> resultHandler);
+
+    /**
+     * Invoked when a compare request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The compare request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle compare requests.
+     */
+    void handleCompare(C requestContext, CompareRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<CompareResult> resultHandler);
+
+    /**
+     * Invoked when a delete request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The delete request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle delete requests.
+     */
+    void handleDelete(C requestContext, DeleteRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<Result> resultHandler);
+
+    /**
+     * Invoked when an extended request is received from a client.
+     *
+     * @param <R>
+     *            The type of result returned by the extended request.
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The extended request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle extended requests.
+     */
+    <R extends ExtendedResult> void handleExtendedRequest(C requestContext,
+            ExtendedRequest<R> request, IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<R> resultHandler);
+
+    /**
+     * Invoked when a modify request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The modify request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle modify requests.
+     */
+    void handleModify(C requestContext, ModifyRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<Result> resultHandler);
+
+    /**
+     * Invoked when a modify DN request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The modify DN request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle modify DN requests.
+     */
+    void handleModifyDN(C requestContext, ModifyDNRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            LdapResultHandler<Result> resultHandler);
+
+    /**
+     * Invoked when a search request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The search request.
+     * @param intermediateResponseHandler
+     *            The handler which should be used to send back any intermediate
+     *            responses to the client.
+     * @param entryHandler
+     *            The entry handler which should be used to send back the search
+     *            entries results to the client.
+     * @param resultHandler
+     *            The handler which should be used to send back the result to
+     *            the client.
+     * @throws UnsupportedOperationException
+     *             If this request handler does not handle search requests.
+     */
+    void handleSearch(C requestContext, SearchRequest request,
+        IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler,
+        LdapResultHandler<Result> resultHandler);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactory.java
new file mode 100644
index 0000000..525e115
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactory.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * A handler interface for accepting new connections from clients.
+ *
+ * @param <C>
+ *            The type of client context.
+ * @param <R>
+ *            The type of request context.
+ */
+public interface RequestHandlerFactory<C, R extends RequestContext> {
+    /**
+     * Invoked when a new client connection is accepted by the associated
+     * listener. Implementations should return a {@code RequestHandler} which
+     * will be used to handle requests from the client connection.
+     *
+     * @param clientContext
+     *            The protocol dependent context information associated with the
+     *            client connection. Depending on the protocol this may contain
+     *            information about the client such as their address and level
+     *            connection security. It may also be used to manage the state
+     *            of the client's connection.
+     * @return A {@code RequestHandler} which will be used to handle requests
+     *         from a client connection.
+     * @throws LdapException
+     *             If this request handler factory cannot accept the client
+     *             connection.
+     */
+    RequestHandler<R> handleAccept(C clientContext) throws LdapException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactoryAdapter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactoryAdapter.java
new file mode 100644
index 0000000..0100d3d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestHandlerFactoryAdapter.java
@@ -0,0 +1,682 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CANCELED_BY_ABANDON_REQUEST;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CANCELED_BY_CANCEL_REQUEST;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CANCELED_BY_CLIENT_DISCONNECT;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CANCELED_BY_CLIENT_ERROR;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CANCELED_BY_SERVER_DISCONNECT;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_CLIENT_CONNECTION_CLOSING;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_CLIENT_DUPLICATE_MESSAGE_ID;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CancelExtendedRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An adapter which converts a {@code RequestHandlerFactory} into a
+ * {@code ServerConnectionFactory}.
+ *
+ * @param <C>
+ *            The type of client context.
+ */
+final class RequestHandlerFactoryAdapter<C> implements ServerConnectionFactory<C, Integer> {
+    /** Request context implementation. */
+    private static class RequestContextImpl<S extends Result, H extends LdapResultHandler<S>>
+            implements RequestContext, LdapResultHandler<S> {
+        /** Adapter class which invokes cancel result handlers with correct result type. */
+        private static final class ExtendedResultHandlerHolder<R extends ExtendedResult> {
+            private final ExtendedRequest<R> request;
+            private final LdapResultHandler<R> resultHandler;
+
+            private ExtendedResultHandlerHolder(final ExtendedRequest<R> request,
+                    final LdapResultHandler<R> resultHandler) {
+                this.request = request;
+                this.resultHandler = resultHandler;
+            }
+
+            private void handleSuccess() {
+                final R cancelResult =
+                        request.getResultDecoder().newExtendedErrorResult(ResultCode.SUCCESS, "",
+                                "");
+                resultHandler.handleResult(cancelResult);
+            }
+
+            private void handleTooLate() {
+                final R cancelResult =
+                        request.getResultDecoder().newExtendedErrorResult(ResultCode.TOO_LATE, "",
+                                "");
+                resultHandler.handleException(newLdapException(cancelResult));
+            }
+        }
+
+        private static enum RequestState {
+            /** Request active, cancel requested. */
+            CANCEL_REQUESTED,
+
+            /** Result sent, was cancelled. */
+            CANCELLED,
+
+            /** Request active. */
+            PENDING,
+
+            /** Result sent, not cancelled. */
+            RESULT_SENT,
+
+            /** Request active, too late to cancel. */
+            TOO_LATE;
+        }
+
+        protected final H resultHandler;
+        /** These should be notified when a cancel request arrives, at most once. */
+        private List<CancelRequestListener> cancelRequestListeners;
+        private LocalizableMessage cancelRequestReason;
+        /** These should be notified when the result is set. */
+        private List<ExtendedResultHandlerHolder<?>> cancelResultHandlers;
+        private final ServerConnectionImpl clientConnection;
+        private final boolean isCancelSupported;
+        private final int messageID;
+        private boolean sendResult = true;
+        private RequestState state = RequestState.PENDING;
+        /** Cancellation state guarded by lock. */
+        private final Object stateLock = new Object();
+
+        protected RequestContextImpl(final ServerConnectionImpl clientConnection,
+                final H resultHandler, final int messageID, final boolean isCancelSupported) {
+            this.clientConnection = clientConnection;
+            this.resultHandler = resultHandler;
+            this.messageID = messageID;
+            this.isCancelSupported = isCancelSupported;
+        }
+
+        @Override
+        public void addCancelRequestListener(final CancelRequestListener listener) {
+            Reject.ifNull(listener);
+
+            boolean invokeImmediately = false;
+            synchronized (stateLock) {
+                switch (state) {
+                case PENDING:
+                    if (cancelRequestListeners == null) {
+                        cancelRequestListeners = new LinkedList<>();
+                    }
+                    cancelRequestListeners.add(listener);
+                    break;
+                case CANCEL_REQUESTED:
+                    // Signal immediately outside lock.
+                    invokeImmediately = true;
+                    break;
+                case TOO_LATE:
+                case RESULT_SENT:
+                case CANCELLED:
+                    /* No point in registering the callback since the request can never be cancelled now. */
+                    break;
+                }
+            }
+
+            if (invokeImmediately) {
+                listener.handleCancelRequest(cancelRequestReason);
+            }
+        }
+
+        @Override
+        public void checkIfCancelled(final boolean signalTooLate) throws CancelledResultException {
+            synchronized (stateLock) {
+                switch (state) {
+                case PENDING:
+                    /* No cancel request, so no handlers, just switch state. */
+                    if (signalTooLate) {
+                        cancelRequestListeners = null;
+                        state = RequestState.TOO_LATE;
+                    }
+                    break;
+                case CANCEL_REQUESTED:
+                    /* Don't change state: let the handler ack the cancellation request. */
+                    throw (CancelledResultException) newLdapException(ResultCode.CANCELLED,
+                            cancelRequestReason.toString());
+                case TOO_LATE:
+                    /* Already too late. Nothing to do. */
+                    break;
+                case RESULT_SENT:
+                case CANCELLED:
+                    /* This should not happen - could throw an illegal state exception? */
+                    break;
+                }
+            }
+        }
+
+        @Override
+        public int getMessageID() {
+            return messageID;
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            if (clientConnection.removePendingRequest(this)) {
+                if (setResult(error.getResult())) {
+                    /*
+                     * FIXME: we must invoke the result handler even when
+                     * abandoned so that chained result handlers may clean up,
+                     * log, etc. We really need to signal that the result must
+                     * not be sent to the client.
+                     */
+                }
+                resultHandler.handleException(error);
+            }
+        }
+
+        @Override
+        public void handleResult(final S result) {
+            if (clientConnection.removePendingRequest(this)) {
+                if (setResult(result)) {
+                    /*
+                     * FIXME: we must invoke the result handler even when
+                     * abandoned so that chained result handlers may clean up,
+                     * log, etc. We really need to signal that the result must
+                     * not be sent to the client.
+                     */
+                }
+                resultHandler.handleResult(result);
+            }
+        }
+
+        @Override
+        public void removeCancelRequestListener(final CancelRequestListener listener) {
+            Reject.ifNull(listener);
+
+            synchronized (stateLock) {
+                if (cancelRequestListeners != null) {
+                    cancelRequestListeners.remove(listener);
+                }
+            }
+        }
+
+        private <R extends ExtendedResult> void cancel(final LocalizableMessage reason,
+                final ExtendedRequest<R> cancelRequest, final LdapResultHandler<R> cancelResultHandler,
+                final boolean sendResult) {
+            Reject.ifNull(reason);
+
+            if (!isCancelSupported) {
+                if (cancelResultHandler != null) {
+                    final Result result =
+                            Responses.newGenericExtendedResult(ResultCode.CANNOT_CANCEL);
+                    cancelResultHandler.handleException(newLdapException(result));
+                }
+                return;
+            }
+
+            List<CancelRequestListener> tmpListeners = null;
+            boolean invokeResultHandler = false;
+            boolean resultHandlerIsSuccess = false;
+
+            synchronized (stateLock) {
+                switch (state) {
+                case PENDING:
+                    /* Switch to CANCEL_REQUESTED state. */
+                    cancelRequestReason = reason;
+                    if (cancelResultHandler != null) {
+                        cancelResultHandlers = new LinkedList<>();
+                        cancelResultHandlers.add(new ExtendedResultHandlerHolder<R>(cancelRequest,
+                                cancelResultHandler));
+                    }
+                    tmpListeners = cancelRequestListeners;
+                    cancelRequestListeners = null;
+                    state = RequestState.CANCEL_REQUESTED;
+                    this.sendResult &= sendResult;
+                    break;
+                case CANCEL_REQUESTED:
+                    /* Cancel already request so listeners already invoked. */
+                    if (cancelResultHandler != null) {
+                        if (cancelResultHandlers == null) {
+                            cancelResultHandlers = new LinkedList<>();
+                        }
+                        cancelResultHandlers.add(new ExtendedResultHandlerHolder<R>(cancelRequest,
+                                cancelResultHandler));
+                    }
+                    break;
+                case TOO_LATE:
+                case RESULT_SENT:
+                    /* Cannot cancel, so invoke result handler immediately outside of lock. */
+                    if (cancelResultHandler != null) {
+                        invokeResultHandler = true;
+                        resultHandlerIsSuccess = false;
+                    }
+                    break;
+                case CANCELLED:
+                    /*
+                     * Multiple cancellation attempts. Clients should not do
+                     * this, but the cancel will effectively succeed
+                     * immediately, so invoke result handler immediately outside
+                     * of lock.
+                     */
+                    if (cancelResultHandler != null) {
+                        invokeResultHandler = true;
+                        resultHandlerIsSuccess = true;
+                    }
+                    break;
+                }
+            }
+
+            /* Invoke listeners outside of lock. */
+            if (tmpListeners != null) {
+                for (final CancelRequestListener listener : tmpListeners) {
+                    listener.handleCancelRequest(reason);
+                }
+            }
+
+            if (invokeResultHandler) {
+                if (resultHandlerIsSuccess) {
+                    final R result =
+                            cancelRequest.getResultDecoder().newExtendedErrorResult(
+                                    ResultCode.SUCCESS, "", "");
+                    cancelResultHandler.handleResult(result);
+                } else {
+                    final Result result = Responses.newGenericExtendedResult(ResultCode.TOO_LATE);
+                    cancelResultHandler.handleException(newLdapException(result));
+                }
+            }
+        }
+
+        /**
+         * Sets the result associated with this request context and updates the
+         * state accordingly.
+         *
+         * @param result
+         *            The result.
+         */
+        private boolean setResult(final Result result) {
+            List<ExtendedResultHandlerHolder<?>> tmpHandlers = null;
+            boolean isCancelled = false;
+            boolean maySendResult;
+
+            synchronized (stateLock) {
+                maySendResult = sendResult;
+
+                switch (state) {
+                case PENDING:
+                case TOO_LATE:
+                    /* Switch to appropriate final state. */
+                    if (!result.getResultCode().equals(ResultCode.CANCELLED)) {
+                        state = RequestState.RESULT_SENT;
+                    } else {
+                        state = RequestState.CANCELLED;
+                    }
+                    break;
+                case CANCEL_REQUESTED:
+                    /* Switch to appropriate final state and invoke any cancel request handlers. */
+                    if (!result.getResultCode().equals(ResultCode.CANCELLED)) {
+                        state = RequestState.RESULT_SENT;
+                    } else {
+                        state = RequestState.CANCELLED;
+                    }
+
+                    isCancelled = (state == RequestState.CANCELLED);
+                    tmpHandlers = cancelResultHandlers;
+                    cancelResultHandlers = null;
+                    break;
+                case RESULT_SENT:
+                case CANCELLED:
+                    /* This should not happen - could throw an illegal state exception? */
+                    maySendResult = false; // Prevent sending multiple results.
+                    break;
+                }
+            }
+
+            /* Invoke handlers outside of lock. */
+            if (tmpHandlers != null) {
+                for (final ExtendedResultHandlerHolder<?> handler : tmpHandlers) {
+                    if (isCancelled) {
+                        handler.handleSuccess();
+                    } else {
+                        handler.handleTooLate();
+                    }
+                }
+            }
+
+            return maySendResult;
+        }
+    }
+
+    /** Search request context implementation. */
+    private static final class SearchRequestContextImpl extends RequestContextImpl<Result, LdapResultHandler<Result>>
+        implements SearchResultHandler {
+
+        private final SearchResultHandler entryHandler;
+
+        private SearchRequestContextImpl(final ServerConnectionImpl clientConnection,
+            final SearchResultHandler entryHandler, final LdapResultHandler<Result> resultHandler, final int messageID,
+            final boolean isCancelSupported) {
+            super(clientConnection, resultHandler, messageID, isCancelSupported);
+            this.entryHandler = entryHandler;
+        }
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            return entryHandler.handleEntry(entry);
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            return entryHandler.handleReference(reference);
+        }
+    }
+
+    private static final class ServerConnectionImpl implements ServerConnection<Integer> {
+        private final AtomicBoolean isClosed = new AtomicBoolean();
+        private final ConcurrentHashMap<Integer, RequestContextImpl<?, ?>> pendingRequests = new ConcurrentHashMap<>();
+        private final RequestHandler<RequestContext> requestHandler;
+
+        private ServerConnectionImpl(final RequestHandler<RequestContext> requestHandler) {
+            this.requestHandler = requestHandler;
+        }
+
+        @Override
+        public void handleAbandon(final Integer messageID, final AbandonRequest request) {
+            final RequestContextImpl<?, ?> abandonedRequest =
+                    getPendingRequest(request.getRequestID());
+            if (abandonedRequest != null) {
+                final LocalizableMessage abandonReason =
+                        INFO_CANCELED_BY_ABANDON_REQUEST.get(messageID);
+                abandonedRequest.cancel(abandonReason, null, null, false);
+            }
+        }
+
+        @Override
+        public void handleAdd(final Integer messageID, final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            final RequestContextImpl<Result, LdapResultHandler<Result>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleAdd(requestContext, request, intermediateResponseHandler,
+                        requestContext);
+            }
+        }
+
+        @Override
+        public void handleBind(final Integer messageID, final int version,
+                final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<BindResult> resultHandler) {
+            final RequestContextImpl<BindResult, LdapResultHandler<BindResult>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, false);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleBind(requestContext, version, request,
+                        intermediateResponseHandler, requestContext);
+            }
+        }
+
+        @Override
+        public void handleCompare(final Integer messageID, final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<CompareResult> resultHandler) {
+            final RequestContextImpl<CompareResult, LdapResultHandler<CompareResult>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleCompare(requestContext, request, intermediateResponseHandler,
+                        requestContext);
+            }
+        }
+
+        @Override
+        public void handleConnectionClosed(final Integer messageID, final UnbindRequest request) {
+            final LocalizableMessage cancelReason = INFO_CANCELED_BY_CLIENT_DISCONNECT.get();
+            doClose(cancelReason);
+        }
+
+        @Override
+        public void handleConnectionDisconnected(final ResultCode resultCode, final String message) {
+            final LocalizableMessage cancelReason = INFO_CANCELED_BY_SERVER_DISCONNECT.get();
+            doClose(cancelReason);
+        }
+
+        @Override
+        public void handleConnectionError(final Throwable error) {
+            final LocalizableMessage cancelReason = INFO_CANCELED_BY_CLIENT_ERROR.get();
+            doClose(cancelReason);
+        }
+
+        @Override
+        public void handleDelete(final Integer messageID, final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            final RequestContextImpl<Result, LdapResultHandler<Result>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleDelete(requestContext, request, intermediateResponseHandler,
+                        requestContext);
+            }
+        }
+
+        @Override
+        public <R extends ExtendedResult> void handleExtendedRequest(final Integer messageID,
+                final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<R> resultHandler) {
+            if (request.getOID().equals(CancelExtendedRequest.OID)) {
+                // Decode the request as a cancel request.
+                CancelExtendedRequest cancelRequest;
+                try {
+                    cancelRequest =
+                            CancelExtendedRequest.DECODER.decodeExtendedRequest(request,
+                                    new DecodeOptions());
+                } catch (final DecodeException e) {
+                    // Couldn't decode a cancel request.
+                    resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e
+                            .getLocalizedMessage()));
+                    return;
+                }
+
+                /*
+                 * Register the request in the pending requests table. Even
+                 * though this request cannot be cancelled, it is important to
+                 * do this in order to monitor the number of pending operations.
+                 */
+                final RequestContextImpl<R, LdapResultHandler<R>> requestContext =
+                        new RequestContextImpl<>(this, resultHandler, messageID, false);
+                if (addPendingRequest(requestContext)) {
+                    // Find and cancel the request.
+                    final RequestContextImpl<?, ?> cancelledRequest =
+                            getPendingRequest(cancelRequest.getRequestID());
+                    if (cancelledRequest != null) {
+                        final LocalizableMessage cancelReason =
+                                INFO_CANCELED_BY_CANCEL_REQUEST.get(messageID);
+                        cancelledRequest.cancel(cancelReason, request, requestContext, true);
+                    } else {
+                        /* Couldn't find the request. Invoke on context in order to remove pending request. */
+                        requestContext.handleException(newLdapException(ResultCode.NO_SUCH_OPERATION));
+                    }
+                }
+            } else {
+                // StartTLS requests cannot be cancelled.
+                boolean isCancelSupported = !request.getOID().equals(StartTLSExtendedRequest.OID);
+                final RequestContextImpl<R, LdapResultHandler<R>> requestContext =
+                        new RequestContextImpl<>(this, resultHandler, messageID, isCancelSupported);
+
+                if (addPendingRequest(requestContext)) {
+                    requestHandler.handleExtendedRequest(requestContext, request,
+                            intermediateResponseHandler, requestContext);
+                }
+            }
+        }
+
+        @Override
+        public void handleModify(final Integer messageID, final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            final RequestContextImpl<Result, LdapResultHandler<Result>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleModify(requestContext, request, intermediateResponseHandler,
+                        requestContext);
+            }
+        }
+
+        @Override
+        public void handleModifyDN(final Integer messageID, final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            final RequestContextImpl<Result, LdapResultHandler<Result>> requestContext =
+                    new RequestContextImpl<>(this, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleModifyDN(requestContext, request, intermediateResponseHandler,
+                        requestContext);
+            }
+        }
+
+        @Override
+        public void handleSearch(final Integer messageID, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
+            final LdapResultHandler<Result> resultHandler) {
+            final SearchRequestContextImpl requestContext =
+                new SearchRequestContextImpl(this, entryHandler, resultHandler, messageID, true);
+            if (addPendingRequest(requestContext)) {
+                requestHandler.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler,
+                    requestContext);
+            }
+        }
+
+        private boolean addPendingRequest(final RequestContextImpl<?, ?> requestContext) {
+            final Integer messageID = requestContext.getMessageID();
+
+            if (isClosed.get()) {
+                final LocalizableMessage message = INFO_CLIENT_CONNECTION_CLOSING.get();
+                requestContext.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM,
+                        message.toString()));
+                return false;
+            } else if (pendingRequests.putIfAbsent(messageID, requestContext) != null) {
+                final LocalizableMessage message =
+                        WARN_CLIENT_DUPLICATE_MESSAGE_ID.get(requestContext.getMessageID());
+                requestContext.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, message.toString()));
+                return false;
+            } else if (isClosed.get()) {
+                /*
+                 * A concurrent close may have already removed the pending
+                 * request but it will have only been notified for cancellation.
+                 */
+                pendingRequests.remove(messageID);
+
+                final LocalizableMessage message = INFO_CLIENT_CONNECTION_CLOSING.get();
+                requestContext.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM, message.toString()));
+                return false;
+            } else {
+                /*
+                 * If the connection is closed now then we just have to pay the
+                 * cost of invoking the request in the request handler.
+                 */
+                return true;
+            }
+        }
+
+        private void doClose(final LocalizableMessage cancelReason) {
+            if (!isClosed.getAndSet(true)) {
+                /*
+                 * At this point if any pending requests are added then we may
+                 * end up cancelling them, but this does not matter since
+                 * addPendingRequest will fail the request immediately.
+                 */
+                final Iterator<RequestContextImpl<?, ?>> iterator =
+                        pendingRequests.values().iterator();
+                while (iterator.hasNext()) {
+                    final RequestContextImpl<?, ?> pendingRequest = iterator.next();
+                    pendingRequest.cancel(cancelReason, null, null, false);
+                    iterator.remove();
+                }
+            }
+        }
+
+        /**
+         * Returns the pending request context having the specified message ID.
+         *
+         * @param messageID
+         *            The message ID associated with the request context.
+         * @return The pending request context.
+         */
+        private RequestContextImpl<?, ?> getPendingRequest(final Integer messageID) {
+            return pendingRequests.get(messageID);
+        }
+
+        /**
+         * Deregister a request context once it has completed.
+         *
+         * @param requestContext
+         *            The request context.
+         * @return {@code true} if the request context was found and removed.
+         */
+        private boolean removePendingRequest(final RequestContextImpl<?, ?> requestContext) {
+            return pendingRequests.remove(requestContext.getMessageID()) != null;
+        }
+    }
+
+    /**
+     * Adapts the provided request handler as a {@code ServerConnection}.
+     *
+     * @param requestHandler
+     *            The request handler.
+     * @return The server connection which will forward requests to the provided
+     *         request handler.
+     */
+    static ServerConnection<Integer> adaptRequestHandler(
+            final RequestHandler<RequestContext> requestHandler) {
+        return new ServerConnectionImpl(requestHandler);
+    }
+
+    private final RequestHandlerFactory<C, RequestContext> factory;
+
+    /**
+     * Creates a new server connection factory using the provided request
+     * handler factory.
+     *
+     * @param factory
+     *            The request handler factory to be adapted into a server
+     *            connection factory.
+     */
+    RequestHandlerFactoryAdapter(final RequestHandlerFactory<C, RequestContext> factory) {
+        this.factory = factory;
+    }
+
+    @Override
+    public ServerConnection<Integer> handleAccept(final C clientContext) throws LdapException {
+        return adaptRequestHandler(factory.handleAccept(clientContext));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestLoadBalancer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestLoadBalancer.java
new file mode 100644
index 0000000..3e12e3c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RequestLoadBalancer.java
@@ -0,0 +1,251 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise;
+import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.spi.ConnectionState;
+import org.forgerock.opendj.ldap.spi.LdapPromises;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+/**
+ * A request based load balancer which load balances individual requests based on properties of the request, such as
+ * the target DN.
+ * <p>
+ * Implementations should override the method {@code getInitialConnectionFactoryIndex()} in order to provide the policy
+ * for selecting the first connection factory to use for each request.
+ */
+final class RequestLoadBalancer extends LoadBalancer {
+    /**
+     * A function which returns the index of the first connection factory which should be used in order to satisfy the
+     * next request. Implementations may base the decision on properties of the provided request, such as the target DN,
+     * whether the request is a read or update request, etc.
+     */
+    private final Function<Request, Integer, NeverThrowsException> nextFactoryFunction;
+
+    RequestLoadBalancer(final String loadBalancerName,
+                        final Collection<? extends ConnectionFactory> factories,
+                        final Options options,
+                        final Function<Request, Integer, NeverThrowsException> nextFactoryFunction) {
+        super(loadBalancerName, factories, options);
+        this.nextFactoryFunction = nextFactoryFunction;
+    }
+
+    @Override
+    public final Connection getConnection() throws LdapException {
+        return new ConnectionImpl();
+    }
+
+    @Override
+    public final Promise<Connection, LdapException> getConnectionAsync() {
+        return newResultPromise((Connection) new ConnectionImpl());
+    }
+
+    private class ConnectionImpl extends AbstractAsynchronousConnection {
+        private final ConnectionState state = new ConnectionState();
+
+        @Override
+        public String toString() {
+            return getLoadBalancerName() + "Connection";
+        }
+
+        @Override
+        public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+            // We cannot possibly route these correctly, so just drop them.
+            return LdapPromises.newSuccessfulLdapPromise(null);
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(
+                final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, Result, LdapException>() {
+                @Override
+                public Promise<Result, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.addAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public void addConnectionEventListener(final ConnectionEventListener listener) {
+            state.addConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(
+                final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, BindResult, LdapException>() {
+                @Override
+                public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.bindAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public void close(final UnbindRequest request, final String reason) {
+            state.notifyConnectionClosed();
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(
+                final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, CompareResult, LdapException>() {
+                @Override
+                public Promise<CompareResult, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.compareAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(
+                final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, Result, LdapException>() {
+                @Override
+                public Promise<Result, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.deleteAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(
+                final ExtendedRequest<R> request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, R, LdapException>() {
+                @Override
+                public Promise<R, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.extendedRequestAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public boolean isClosed() {
+            return state.isClosed();
+        }
+
+        @Override
+        public boolean isValid() {
+            return state.isValid();
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(
+                final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, Result, LdapException>() {
+                @Override
+                public Promise<Result, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.modifyAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(
+                final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, Result, LdapException>() {
+                @Override
+                public Promise<Result, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.modifyDNAsync(request, intermediateResponseHandler);
+                }
+            });
+        }
+
+        @Override
+        public void removeConnectionEventListener(final ConnectionEventListener listener) {
+            state.removeConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(
+                final SearchRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final SearchResultHandler entryHandler) {
+            return getConnectionAndSendRequest(request, new AsyncFunction<Connection, Result, LdapException>() {
+                @Override
+                public Promise<Result, LdapException> apply(final Connection connection) throws LdapException {
+                    return connection.searchAsync(request, intermediateResponseHandler, entryHandler);
+                }
+            });
+        }
+
+        private <R> LdapPromise<R> getConnectionAndSendRequest(
+                final Request request, final AsyncFunction<Connection, R, LdapException> sendRequest) {
+            if (state.isClosed()) {
+                throw new IllegalStateException();
+            }
+            final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+            return getConnectionAsync(request)
+                    .thenOnResult(new ResultHandler<Connection>() {
+                        @Override
+                        public void handleResult(final Connection connection) {
+                            connectionHolder.set(connection);
+                        }
+                    })
+                    .thenAsync(sendRequest)
+                    .thenFinally(new Runnable() {
+                        @Override
+                        public void run() {
+                            closeSilently(connectionHolder.get());
+                        }
+                    });
+        }
+
+        private LdapPromise<Connection> getConnectionAsync(final Request request) {
+            try {
+                final int index = nextFactoryFunction.apply(request);
+                final ConnectionFactory factory = getMonitoredConnectionFactory(index);
+                return LdapPromises.asPromise(factory.getConnectionAsync()
+                                                     .thenOnException(new ExceptionHandler<LdapException>() {
+                                                         @Override
+                                                         public void handleException(final LdapException e) {
+                                                             state.notifyConnectionError(false, e);
+                                                         }
+                                                     }));
+            } catch (final LdapException e) {
+                state.notifyConnectionError(false, e);
+                return newFailedLdapPromise(e);
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ResultCode.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ResultCode.java
new file mode 100644
index 0000000..f60c5ea
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ResultCode.java
@@ -0,0 +1,1000 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * An operation result code as defined in RFC 4511 section 4.1.9 is used to
+ * indicate the final status of an operation. If a server detects multiple
+ * errors for an operation, only one result code is returned. The server should
+ * return the result code that best indicates the nature of the error
+ * encountered. Servers may return substituted result codes to prevent
+ * unauthorized disclosures.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511#section-4.1.9">RFC 4511 -
+ *      Lightweight Directory Access Protocol (LDAP): The Protocol </a>
+ */
+public final class ResultCode {
+    /**
+     * Contains equivalent values for the ResultCode values.
+     * This allows easily using ResultCode values with switch statements.
+     */
+    public static enum Enum {
+        //@Checkstyle:off
+        /** @see ResultCode#UNDEFINED */
+        UNDEFINED,
+        /** @see ResultCode#SUCCESS */
+        SUCCESS,
+        /** @see ResultCode#OPERATIONS_ERROR */
+        OPERATIONS_ERROR,
+        /** @see ResultCode#PROTOCOL_ERROR */
+        PROTOCOL_ERROR,
+        /** @see ResultCode#TIME_LIMIT_EXCEEDED */
+        TIME_LIMIT_EXCEEDED,
+        /** @see ResultCode#SIZE_LIMIT_EXCEEDED */
+        SIZE_LIMIT_EXCEEDED,
+        /** @see ResultCode#COMPARE_FALSE */
+        COMPARE_FALSE,
+        /** @see ResultCode#COMPARE_TRUE */
+        COMPARE_TRUE,
+        /** @see ResultCode#AUTH_METHOD_NOT_SUPPORTED */
+        AUTH_METHOD_NOT_SUPPORTED,
+        /** @see ResultCode#STRONG_AUTH_REQUIRED */
+        STRONG_AUTH_REQUIRED,
+        /** @see ResultCode#REFERRAL */
+        REFERRAL,
+        /** @see ResultCode#ADMIN_LIMIT_EXCEEDED */
+        ADMIN_LIMIT_EXCEEDED,
+        /** @see ResultCode#UNAVAILABLE_CRITICAL_EXTENSION */
+        UNAVAILABLE_CRITICAL_EXTENSION,
+        /** @see ResultCode#CONFIDENTIALITY_REQUIRED */
+        CONFIDENTIALITY_REQUIRED,
+        /** @see ResultCode#SASL_BIND_IN_PROGRESS */
+        SASL_BIND_IN_PROGRESS,
+        /** @see ResultCode#NO_SUCH_ATTRIBUTE */
+        NO_SUCH_ATTRIBUTE,
+        /** @see ResultCode#UNDEFINED_ATTRIBUTE_TYPE */
+        UNDEFINED_ATTRIBUTE_TYPE,
+        /** @see ResultCode#INAPPROPRIATE_MATCHING */
+        INAPPROPRIATE_MATCHING,
+        /** @see ResultCode#CONSTRAINT_VIOLATION */
+        CONSTRAINT_VIOLATION,
+        /** @see ResultCode#ATTRIBUTE_OR_VALUE_EXISTS */
+        ATTRIBUTE_OR_VALUE_EXISTS,
+        /** @see ResultCode#INVALID_ATTRIBUTE_SYNTAX */
+        INVALID_ATTRIBUTE_SYNTAX,
+        /** @see ResultCode#NO_SUCH_OBJECT */
+        NO_SUCH_OBJECT,
+        /** @see ResultCode#ALIAS_PROBLEM */
+        ALIAS_PROBLEM,
+        /** @see ResultCode#INVALID_DN_SYNTAX */
+        INVALID_DN_SYNTAX,
+        /** @see ResultCode#ALIAS_DEREFERENCING_PROBLEM */
+        ALIAS_DEREFERENCING_PROBLEM,
+        /** @see ResultCode#INAPPROPRIATE_AUTHENTICATION */
+        INAPPROPRIATE_AUTHENTICATION,
+        /** @see ResultCode#INVALID_CREDENTIALS */
+        INVALID_CREDENTIALS,
+        /** @see ResultCode#INSUFFICIENT_ACCESS_RIGHTS */
+        INSUFFICIENT_ACCESS_RIGHTS,
+        /** @see ResultCode#BUSY */
+        BUSY,
+        /** @see ResultCode#UNAVAILABLE */
+        UNAVAILABLE,
+        /** @see ResultCode#UNWILLING_TO_PERFORM */
+        UNWILLING_TO_PERFORM,
+        /** @see ResultCode#LOOP_DETECT */
+        LOOP_DETECT,
+        /** @see ResultCode#SORT_CONTROL_MISSING */
+        SORT_CONTROL_MISSING,
+        /** @see ResultCode#OFFSET_RANGE_ERROR */
+        OFFSET_RANGE_ERROR,
+        /** @see ResultCode#NAMING_VIOLATION */
+        NAMING_VIOLATION,
+        /** @see ResultCode#OBJECTCLASS_VIOLATION */
+        OBJECTCLASS_VIOLATION,
+        /** @see ResultCode#NOT_ALLOWED_ON_NONLEAF */
+        NOT_ALLOWED_ON_NONLEAF,
+        /** @see ResultCode#NOT_ALLOWED_ON_RDN */
+        NOT_ALLOWED_ON_RDN,
+        /** @see ResultCode#ENTRY_ALREADY_EXISTS */
+        ENTRY_ALREADY_EXISTS,
+        /** @see ResultCode#OBJECTCLASS_MODS_PROHIBITED */
+        OBJECTCLASS_MODS_PROHIBITED,
+        /** @see ResultCode#AFFECTS_MULTIPLE_DSAS */
+        AFFECTS_MULTIPLE_DSAS,
+        /** @see ResultCode#VIRTUAL_LIST_VIEW_ERROR */
+        VIRTUAL_LIST_VIEW_ERROR,
+        /** @see ResultCode#OTHER */
+        OTHER,
+        /** @see ResultCode#CLIENT_SIDE_SERVER_DOWN */
+        CLIENT_SIDE_SERVER_DOWN,
+        /** @see ResultCode#CLIENT_SIDE_LOCAL_ERROR */
+        CLIENT_SIDE_LOCAL_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_ENCODING_ERROR */
+        CLIENT_SIDE_ENCODING_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_DECODING_ERROR */
+        CLIENT_SIDE_DECODING_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_TIMEOUT */
+        CLIENT_SIDE_TIMEOUT,
+        /** @see ResultCode#CLIENT_SIDE_AUTH_UNKNOWN */
+        CLIENT_SIDE_AUTH_UNKNOWN,
+        /** @see ResultCode#CLIENT_SIDE_FILTER_ERROR */
+        CLIENT_SIDE_FILTER_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_USER_CANCELLED */
+        CLIENT_SIDE_USER_CANCELLED,
+        /** @see ResultCode#CLIENT_SIDE_PARAM_ERROR */
+        CLIENT_SIDE_PARAM_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_NO_MEMORY */
+        CLIENT_SIDE_NO_MEMORY,
+        /** @see ResultCode#CLIENT_SIDE_CONNECT_ERROR */
+        CLIENT_SIDE_CONNECT_ERROR,
+        /** @see ResultCode#CLIENT_SIDE_NOT_SUPPORTED */
+        CLIENT_SIDE_NOT_SUPPORTED,
+        /** @see ResultCode#CLIENT_SIDE_CONTROL_NOT_FOUND */
+        CLIENT_SIDE_CONTROL_NOT_FOUND,
+        /** @see ResultCode#CLIENT_SIDE_NO_RESULTS_RETURNED */
+        CLIENT_SIDE_NO_RESULTS_RETURNED,
+        /** @see ResultCode#CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED */
+        CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+        /** @see ResultCode#CLIENT_SIDE_CLIENT_LOOP */
+        CLIENT_SIDE_CLIENT_LOOP,
+        /** @see ResultCode#CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED */
+        CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED,
+        /** @see ResultCode#CANCELLED */
+        CANCELLED,
+        /** @see ResultCode#NO_SUCH_OPERATION */
+        NO_SUCH_OPERATION,
+        /** @see ResultCode#TOO_LATE */
+        TOO_LATE,
+        /** @see ResultCode#CANNOT_CANCEL */
+        CANNOT_CANCEL,
+        /** @see ResultCode#ASSERTION_FAILED */
+        ASSERTION_FAILED,
+        /** @see ResultCode#AUTHORIZATION_DENIED */
+        AUTHORIZATION_DENIED,
+        /** @see ResultCode#NO_OPERATION */
+        NO_OPERATION,
+        /** Used for unknown search scopes. */
+        UNKNOWN;
+        //@Checkstyle:on
+    }
+
+    private static final Map<Integer, ResultCode> ELEMENTS = new LinkedHashMap<>();
+
+    /**
+     * The result code that should only be used if the actual result code has
+     * not yet been determined.
+     * <p>
+     * Despite not being a standard result code, it is an implementation of the
+     * null object design pattern for this type.
+     */
+    public static final ResultCode UNDEFINED = registerErrorResultCode(-1,
+            INFO_RESULT_UNDEFINED.get(), Enum.UNDEFINED);
+
+    /**
+     * The result code that indicates that the operation completed successfully.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 0}.
+     */
+    public static final ResultCode SUCCESS =
+            registerSuccessResultCode(0, INFO_RESULT_SUCCESS.get(), Enum.SUCCESS);
+
+    /**
+     * The result code that indicates that an internal error prevented the
+     * operation from being processed properly.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 1}.
+     */
+    public static final ResultCode OPERATIONS_ERROR = registerErrorResultCode(1,
+            INFO_RESULT_OPERATIONS_ERROR.get(), Enum.OPERATIONS_ERROR);
+
+    /**
+     * The result code that indicates that the client sent a malformed or
+     * illegal request to the server.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 2}.
+     */
+    public static final ResultCode PROTOCOL_ERROR = registerErrorResultCode(2,
+            INFO_RESULT_PROTOCOL_ERROR.get(), Enum.PROTOCOL_ERROR);
+
+    /**
+     * The result code that indicates that a time limit was exceeded while
+     * attempting to process the request.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 3}.
+     */
+    public static final ResultCode TIME_LIMIT_EXCEEDED = registerErrorResultCode(3,
+            INFO_RESULT_TIME_LIMIT_EXCEEDED.get(), Enum.TIME_LIMIT_EXCEEDED);
+
+    /**
+     * The result code that indicates that a size limit was exceeded while
+     * attempting to process the request.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 4}.
+     */
+    public static final ResultCode SIZE_LIMIT_EXCEEDED = registerErrorResultCode(4,
+            INFO_RESULT_SIZE_LIMIT_EXCEEDED.get(), Enum.SIZE_LIMIT_EXCEEDED);
+
+    /**
+     * The result code that indicates that the attribute value assertion
+     * included in a compare request did not match the targeted entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 5}.
+     */
+    public static final ResultCode COMPARE_FALSE = registerSuccessResultCode(5,
+            INFO_RESULT_COMPARE_FALSE.get(), Enum.COMPARE_FALSE);
+
+    /**
+     * The result code that indicates that the attribute value assertion
+     * included in a compare request did match the targeted entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 6}.
+     */
+    public static final ResultCode COMPARE_TRUE = registerSuccessResultCode(6,
+            INFO_RESULT_COMPARE_TRUE.get(), Enum.COMPARE_TRUE);
+
+    /**
+     * The result code that indicates that the requested authentication attempt
+     * failed because it referenced an invalid SASL mechanism.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 7}.
+     */
+    public static final ResultCode AUTH_METHOD_NOT_SUPPORTED = registerErrorResultCode(7,
+            INFO_RESULT_AUTH_METHOD_NOT_SUPPORTED.get(), Enum.AUTH_METHOD_NOT_SUPPORTED);
+
+    /**
+     * The result code that indicates that the requested operation could not be
+     * processed because it requires that the client has completed a strong form
+     * of authentication.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 8}.
+     */
+    public static final ResultCode STRONG_AUTH_REQUIRED = registerErrorResultCode(8,
+            INFO_RESULT_STRONG_AUTH_REQUIRED.get(), Enum.STRONG_AUTH_REQUIRED);
+
+    /**
+     * The result code that indicates that a referral was encountered.
+     * <p>
+     * Strictly speaking this result code should not be exceptional since it is
+     * considered as a "success" response. However, referrals should occur
+     * rarely in practice and, when they do occur, should not be ignored since
+     * the application may believe that a request has succeeded when, in fact,
+     * nothing was done.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 10}.
+     */
+    public static final ResultCode REFERRAL = registerErrorResultCode(10, INFO_RESULT_REFERRAL
+            .get(), Enum.REFERRAL);
+
+    /**
+     * The result code that indicates that processing on the requested operation
+     * could not continue because an administrative limit was exceeded.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 11}.
+     */
+    public static final ResultCode ADMIN_LIMIT_EXCEEDED = registerErrorResultCode(11,
+            INFO_RESULT_ADMIN_LIMIT_EXCEEDED.get(), Enum.ADMIN_LIMIT_EXCEEDED);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it included a critical extension that is unsupported or
+     * inappropriate for that request.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 12}.
+     */
+    public static final ResultCode UNAVAILABLE_CRITICAL_EXTENSION = registerErrorResultCode(12,
+            INFO_RESULT_UNAVAILABLE_CRITICAL_EXTENSION.get(), Enum.UNAVAILABLE_CRITICAL_EXTENSION);
+
+    /**
+     * The result code that indicates that the requested operation could not be
+     * processed because it requires confidentiality for the communication
+     * between the client and the server.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 13}.
+     */
+    public static final ResultCode CONFIDENTIALITY_REQUIRED = registerErrorResultCode(13,
+            INFO_RESULT_CONFIDENTIALITY_REQUIRED.get(), Enum.CONFIDENTIALITY_REQUIRED);
+
+    /**
+     * The result code that should be used for intermediate responses in
+     * multi-stage SASL bind operations.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 14}.
+     */
+    public static final ResultCode SASL_BIND_IN_PROGRESS = registerSuccessResultCode(14,
+            INFO_RESULT_SASL_BIND_IN_PROGRESS.get(), Enum.SASL_BIND_IN_PROGRESS);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it targeted an attribute or attribute value that did not exist in
+     * the specified entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 16}.
+     */
+    public static final ResultCode NO_SUCH_ATTRIBUTE = registerErrorResultCode(16,
+            INFO_RESULT_NO_SUCH_ATTRIBUTE.get(), Enum.NO_SUCH_ATTRIBUTE);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it referenced an attribute that is not defined in the server
+     * schema.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 17}.
+     */
+    public static final ResultCode UNDEFINED_ATTRIBUTE_TYPE = registerErrorResultCode(17,
+            INFO_RESULT_UNDEFINED_ATTRIBUTE_TYPE.get(), Enum.UNDEFINED_ATTRIBUTE_TYPE);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it attempted to perform an inappropriate type of matching against
+     * an attribute.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 18}.
+     */
+    public static final ResultCode INAPPROPRIATE_MATCHING = registerErrorResultCode(18,
+            INFO_RESULT_INAPPROPRIATE_MATCHING.get(), Enum.INAPPROPRIATE_MATCHING);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have violated some constraint defined in the server.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 19}.
+     */
+    public static final ResultCode CONSTRAINT_VIOLATION = registerErrorResultCode(19,
+            INFO_RESULT_CONSTRAINT_VIOLATION.get(), Enum.CONSTRAINT_VIOLATION);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have resulted in a conflict with an existing attribute
+     * or attribute value in the target entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 20}.
+     */
+    public static final ResultCode ATTRIBUTE_OR_VALUE_EXISTS = registerErrorResultCode(20,
+            INFO_RESULT_ATTRIBUTE_OR_VALUE_EXISTS.get(), Enum.ATTRIBUTE_OR_VALUE_EXISTS);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it violated the syntax for a specified attribute.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 21}.
+     */
+    public static final ResultCode INVALID_ATTRIBUTE_SYNTAX = registerErrorResultCode(21,
+            INFO_RESULT_INVALID_ATTRIBUTE_SYNTAX.get(), Enum.INVALID_ATTRIBUTE_SYNTAX);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it referenced an entry that does not exist.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 32}.
+     */
+    public static final ResultCode NO_SUCH_OBJECT = registerErrorResultCode(32,
+            INFO_RESULT_NO_SUCH_OBJECT.get(), Enum.NO_SUCH_OBJECT);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it attempted to perform an illegal operation on an alias.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 33}.
+     */
+    public static final ResultCode ALIAS_PROBLEM = registerErrorResultCode(33,
+            INFO_RESULT_ALIAS_PROBLEM.get(), Enum.ALIAS_PROBLEM);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have resulted in an entry with an invalid or malformed
+     * DN.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 34}.
+     */
+    public static final ResultCode INVALID_DN_SYNTAX = registerErrorResultCode(34,
+            INFO_RESULT_INVALID_DN_SYNTAX.get(), Enum.INVALID_DN_SYNTAX);
+
+    /**
+     * The result code that indicates that a problem was encountered while
+     * attempting to dereference an alias for a search operation.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 36}.
+     */
+    public static final ResultCode ALIAS_DEREFERENCING_PROBLEM = registerErrorResultCode(36,
+            INFO_RESULT_ALIAS_DEREFERENCING_PROBLEM.get(), Enum.ALIAS_DEREFERENCING_PROBLEM);
+
+    /**
+     * The result code that indicates that an authentication attempt failed
+     * because the requested type of authentication was not appropriate for the
+     * targeted entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 48}.
+     */
+    public static final ResultCode INAPPROPRIATE_AUTHENTICATION = registerErrorResultCode(48,
+            INFO_RESULT_INAPPROPRIATE_AUTHENTICATION.get(), Enum.INAPPROPRIATE_AUTHENTICATION);
+
+    /**
+     * The result code that indicates that an authentication attempt failed
+     * because the user did not provide a valid set of credentials.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 49}.
+     */
+    public static final ResultCode INVALID_CREDENTIALS = registerErrorResultCode(49,
+            INFO_RESULT_INVALID_CREDENTIALS.get(), Enum.INVALID_CREDENTIALS);
+
+    /**
+     * The result code that indicates that the client does not have sufficient
+     * permission to perform the requested operation.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 50}.
+     */
+    public static final ResultCode INSUFFICIENT_ACCESS_RIGHTS = registerErrorResultCode(50,
+            INFO_RESULT_INSUFFICIENT_ACCESS_RIGHTS.get(), Enum.INSUFFICIENT_ACCESS_RIGHTS);
+
+    /**
+     * The result code that indicates that the server is too busy to process the
+     * requested operation.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 51}.
+     */
+    public static final ResultCode BUSY = registerErrorResultCode(51, INFO_RESULT_BUSY.get(), Enum.BUSY);
+
+    /**
+     * The result code that indicates that either the entire server or one or
+     * more required resources were not available for use in processing the
+     * request.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 52}.
+     */
+    public static final ResultCode UNAVAILABLE = registerErrorResultCode(52,
+            INFO_RESULT_UNAVAILABLE.get(), Enum.UNAVAILABLE);
+
+    /**
+     * The result code that indicates that the server is unwilling to perform
+     * the requested operation.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 53}.
+     */
+    public static final ResultCode UNWILLING_TO_PERFORM = registerErrorResultCode(53,
+            INFO_RESULT_UNWILLING_TO_PERFORM.get(), Enum.UNWILLING_TO_PERFORM);
+
+    /**
+     * The result code that indicates that a referral or chaining loop was
+     * detected while processing the request.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 54}.
+     */
+    public static final ResultCode LOOP_DETECT = registerErrorResultCode(54,
+            INFO_RESULT_LOOP_DETECT.get(), Enum.LOOP_DETECT);
+
+    /**
+     * The result code that indicates that a search request included a VLV
+     * request control without a server-side sort control.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 60}.
+     */
+    public static final ResultCode SORT_CONTROL_MISSING = registerErrorResultCode(60,
+            INFO_RESULT_SORT_CONTROL_MISSING.get(), Enum.SORT_CONTROL_MISSING);
+
+    /**
+     * The result code that indicates that a search request included a VLV
+     * request control with an invalid offset.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 61}.
+     */
+    public static final ResultCode OFFSET_RANGE_ERROR = registerErrorResultCode(61,
+            INFO_RESULT_OFFSET_RANGE_ERROR.get(), Enum.OFFSET_RANGE_ERROR);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have violated the server's naming configuration.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 64}.
+     */
+    public static final ResultCode NAMING_VIOLATION = registerErrorResultCode(64,
+            INFO_RESULT_NAMING_VIOLATION.get(), Enum.NAMING_VIOLATION);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have resulted in an entry that violated the server
+     * schema.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 65}.
+     */
+    public static final ResultCode OBJECTCLASS_VIOLATION = registerErrorResultCode(65,
+            INFO_RESULT_OBJECTCLASS_VIOLATION.get(), Enum.OBJECTCLASS_VIOLATION);
+
+    /**
+     * The result code that indicates that the requested operation is not
+     * allowed for non-leaf entries.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 66}.
+     */
+    public static final ResultCode NOT_ALLOWED_ON_NONLEAF = registerErrorResultCode(66,
+            INFO_RESULT_NOT_ALLOWED_ON_NONLEAF.get(), Enum.NOT_ALLOWED_ON_NONLEAF);
+
+    /**
+     * The result code that indicates that the requested operation is not
+     * allowed on an RDN attribute.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 67}.
+     */
+    public static final ResultCode NOT_ALLOWED_ON_RDN = registerErrorResultCode(67,
+            INFO_RESULT_NOT_ALLOWED_ON_RDN.get(), Enum.NOT_ALLOWED_ON_RDN);
+
+    /**
+     * The result code that indicates that the requested operation failed
+     * because it would have resulted in an entry that conflicts with an entry
+     * that already exists.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 68}.
+     */
+    public static final ResultCode ENTRY_ALREADY_EXISTS = registerErrorResultCode(68,
+            INFO_RESULT_ENTRY_ALREADY_EXISTS.get(), Enum.ENTRY_ALREADY_EXISTS);
+
+    /**
+     * The result code that indicates that the operation could not be processed
+     * because it would have modified the objectclasses associated with an entry
+     * in an illegal manner.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 69}.
+     */
+    public static final ResultCode OBJECTCLASS_MODS_PROHIBITED = registerErrorResultCode(69,
+            INFO_RESULT_OBJECTCLASS_MODS_PROHIBITED.get(), Enum.OBJECTCLASS_MODS_PROHIBITED);
+
+    /**
+     * The result code that indicates that the operation could not be processed
+     * because it would impact multiple DSAs or other repositories.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 71}.
+     */
+    public static final ResultCode AFFECTS_MULTIPLE_DSAS = registerErrorResultCode(71,
+            INFO_RESULT_AFFECTS_MULTIPLE_DSAS.get(), Enum.AFFECTS_MULTIPLE_DSAS);
+
+    /**
+     * The result code that indicates that the operation could not be processed
+     * because there was an error while processing the virtual list view
+     * control.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 76}.
+     */
+    public static final ResultCode VIRTUAL_LIST_VIEW_ERROR = registerErrorResultCode(76,
+            INFO_RESULT_VIRTUAL_LIST_VIEW_ERROR.get(), Enum.VIRTUAL_LIST_VIEW_ERROR);
+
+    /**
+     * The result code that should be used if no other result code is
+     * appropriate.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 80}.
+     */
+    public static final ResultCode OTHER = registerErrorResultCode(80, INFO_RESULT_OTHER.get(), Enum.OTHER);
+
+    /**
+     * The client-side result code that indicates that a previously-established
+     * connection to the server was lost. This is for client-side use only and
+     * should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 81}.
+     */
+    public static final ResultCode CLIENT_SIDE_SERVER_DOWN = registerErrorResultCode(81,
+            INFO_RESULT_CLIENT_SIDE_SERVER_DOWN.get(), Enum.CLIENT_SIDE_SERVER_DOWN);
+
+    /**
+     * The client-side result code that indicates that a local error occurred
+     * that had nothing to do with interaction with the server. This is for
+     * client-side use only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 82}.
+     */
+    public static final ResultCode CLIENT_SIDE_LOCAL_ERROR = registerErrorResultCode(82,
+            INFO_RESULT_CLIENT_SIDE_LOCAL_ERROR.get(), Enum.CLIENT_SIDE_LOCAL_ERROR);
+
+    /**
+     * The client-side result code that indicates that an error occurred while
+     * encoding a request to send to the server. This is for client-side use
+     * only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 83}.
+     */
+    public static final ResultCode CLIENT_SIDE_ENCODING_ERROR = registerErrorResultCode(83,
+            INFO_RESULT_CLIENT_SIDE_ENCODING_ERROR.get(), Enum.CLIENT_SIDE_ENCODING_ERROR);
+
+    /**
+     * The client-side result code that indicates that an error occurred while
+     * decoding a response from the server. This is for client-side use only and
+     * should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 84}.
+     */
+    public static final ResultCode CLIENT_SIDE_DECODING_ERROR = registerErrorResultCode(84,
+            INFO_RESULT_CLIENT_SIDE_DECODING_ERROR.get(), Enum.CLIENT_SIDE_DECODING_ERROR);
+
+    /**
+     * The client-side result code that indicates that the client did not
+     * receive an expected response in a timely manner. This is for client-side
+     * use only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 85}.
+     */
+    public static final ResultCode CLIENT_SIDE_TIMEOUT = registerErrorResultCode(85,
+            INFO_RESULT_CLIENT_SIDE_TIMEOUT.get(), Enum.CLIENT_SIDE_TIMEOUT);
+
+    /**
+     * The client-side result code that indicates that the user requested an
+     * unknown or unsupported authentication mechanism. This is for client-side
+     * use only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 86}.
+     */
+    public static final ResultCode CLIENT_SIDE_AUTH_UNKNOWN = registerErrorResultCode(86,
+            INFO_RESULT_CLIENT_SIDE_AUTH_UNKNOWN.get(), Enum.CLIENT_SIDE_AUTH_UNKNOWN);
+
+    /**
+     * The client-side result code that indicates that the filter provided by
+     * the user was malformed and could not be parsed. This is for client-side
+     * use only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 87}.
+     */
+    public static final ResultCode CLIENT_SIDE_FILTER_ERROR = registerErrorResultCode(87,
+            INFO_RESULT_CLIENT_SIDE_FILTER_ERROR.get(), Enum.CLIENT_SIDE_FILTER_ERROR);
+
+    /**
+     * The client-side result code that indicates that the user cancelled an
+     * operation. This is for client-side use only and should never be
+     * transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 88}.
+     */
+    public static final ResultCode CLIENT_SIDE_USER_CANCELLED = registerErrorResultCode(88,
+            INFO_RESULT_CLIENT_SIDE_USER_CANCELLED.get(), Enum.CLIENT_SIDE_USER_CANCELLED);
+
+    /**
+     * The client-side result code that indicates that there was a problem with
+     * one or more of the parameters provided by the user. This is for
+     * client-side use only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 89}.
+     */
+    public static final ResultCode CLIENT_SIDE_PARAM_ERROR = registerErrorResultCode(89,
+            INFO_RESULT_CLIENT_SIDE_PARAM_ERROR.get(), Enum.CLIENT_SIDE_PARAM_ERROR);
+
+    /**
+     * The client-side result code that indicates that the client application
+     * was not able to allocate enough memory for the requested operation. This
+     * is for client-side use only and should never be transferred over
+     * protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 90}.
+     */
+    public static final ResultCode CLIENT_SIDE_NO_MEMORY = registerErrorResultCode(90,
+            INFO_RESULT_CLIENT_SIDE_NO_MEMORY.get(), Enum.CLIENT_SIDE_NO_MEMORY);
+
+    /**
+     * The client-side result code that indicates that the client was not able
+     * to establish a connection to the server. This is for client-side use only
+     * and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 91}.
+     */
+    public static final ResultCode CLIENT_SIDE_CONNECT_ERROR = registerErrorResultCode(91,
+            INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(), Enum.CLIENT_SIDE_CONNECT_ERROR);
+
+    /**
+     * The client-side result code that indicates that the user requested an
+     * operation that is not supported. This is for client-side use only and
+     * should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 92}.
+     */
+    public static final ResultCode CLIENT_SIDE_NOT_SUPPORTED = registerErrorResultCode(92,
+            INFO_RESULT_CLIENT_SIDE_NOT_SUPPORTED.get(), Enum.CLIENT_SIDE_NOT_SUPPORTED);
+
+    /**
+     * The client-side result code that indicates that the client expected a
+     * control to be present in the response from the server but it was not
+     * included. This is for client-side use only and should never be
+     * transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 93}.
+     */
+    public static final ResultCode CLIENT_SIDE_CONTROL_NOT_FOUND = registerErrorResultCode(93,
+            INFO_RESULT_CLIENT_SIDE_CONTROL_NOT_FOUND.get(), Enum.CLIENT_SIDE_CONTROL_NOT_FOUND);
+
+    /**
+     * The client-side result code that indicates that the requested single
+     * entry search operation or read operation failed because the Directory
+     * Server did not return any matching entries. This is for client-side use
+     * only and should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 94}.
+     */
+    public static final ResultCode CLIENT_SIDE_NO_RESULTS_RETURNED = registerErrorResultCode(94,
+            INFO_RESULT_CLIENT_SIDE_NO_RESULTS_RETURNED.get(), Enum.CLIENT_SIDE_NO_RESULTS_RETURNED);
+
+    /**
+     * The client-side result code that the requested single entry search
+     * operation or read operation failed because the Directory Server returned
+     * multiple matching entries (or search references) when only a single
+     * matching entry was expected. This is for client-side use only and should
+     * never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 95}.
+     */
+    public static final ResultCode CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED = registerErrorResultCode(95,
+            INFO_RESULT_CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.get(), Enum.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+
+    /**
+     * The client-side result code that indicates that the client detected a
+     * referral loop caused by servers referencing each other in a circular
+     * manner. This is for client-side use only and should never be transferred
+     * over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 96}.
+     */
+    public static final ResultCode CLIENT_SIDE_CLIENT_LOOP = registerErrorResultCode(96,
+            INFO_RESULT_CLIENT_SIDE_CLIENT_LOOP.get(), Enum.CLIENT_SIDE_CLIENT_LOOP);
+
+    /**
+     * The client-side result code that indicates that the client reached the
+     * maximum number of hops allowed when attempting to follow a referral
+     * (i.e., following one referral resulted in another referral which resulted
+     * in another referral and so on). This is for client-side use only and
+     * should never be transferred over protocol.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 97}.
+     */
+    public static final ResultCode CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED = registerErrorResultCode(
+            97, INFO_RESULT_CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED.get(), Enum.CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED);
+
+    /**
+     * The result code that indicates that a cancel request was successful, or
+     * that the specified operation was canceled.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 118}.
+     */
+    public static final ResultCode CANCELLED = registerErrorResultCode(118, INFO_RESULT_CANCELED
+            .get(), Enum.CANCELLED);
+
+    /**
+     * The result code that indicates that a cancel request was unsuccessful
+     * because the targeted operation did not exist or had already completed.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 119}.
+     */
+    public static final ResultCode NO_SUCH_OPERATION = registerErrorResultCode(119,
+            INFO_RESULT_NO_SUCH_OPERATION.get(), Enum.NO_SUCH_OPERATION);
+
+    /**
+     * The result code that indicates that a cancel request was unsuccessful
+     * because processing on the targeted operation had already reached a point
+     * at which it could not be canceled.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 120}.
+     */
+    public static final ResultCode TOO_LATE = registerErrorResultCode(120, INFO_RESULT_TOO_LATE
+            .get(), Enum.TOO_LATE);
+
+    /**
+     * The result code that indicates that a cancel request was unsuccessful
+     * because the targeted operation was one that could not be canceled.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 121}.
+     */
+    public static final ResultCode CANNOT_CANCEL = registerErrorResultCode(121,
+            INFO_RESULT_CANNOT_CANCEL.get(), Enum.CANNOT_CANCEL);
+
+    /**
+     * The result code that indicates that the filter contained in an assertion
+     * control failed to match the target entry.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 122}.
+     */
+    public static final ResultCode ASSERTION_FAILED = registerErrorResultCode(122,
+            INFO_RESULT_ASSERTION_FAILED.get(), Enum.ASSERTION_FAILED);
+
+    /**
+     * The result code that should be used if the server will not allow the
+     * client to use the requested authorization.
+     * <p>
+     * This result code corresponds to the LDAP result code value of {@code 123}.
+     */
+    public static final ResultCode AUTHORIZATION_DENIED = registerErrorResultCode(123,
+            INFO_RESULT_AUTHORIZATION_DENIED.get(), Enum.AUTHORIZATION_DENIED);
+
+    /**
+     * The result code that should be used if the server did not actually
+     * complete processing on the associated operation because the request
+     * included the LDAP No-Op control.
+     * <p>
+     * This result code corresponds to the LDAP result code value of
+     * {@code 16654}.
+     */
+    public static final ResultCode NO_OPERATION = registerSuccessResultCode(16654,
+            INFO_RESULT_NO_OPERATION.get(), Enum.NO_OPERATION);
+
+    /**
+     * Returns the result code having the specified integer value as defined in
+     * RFC 4511 section 4.1.9. If there is no result code associated with the
+     * specified integer value then a temporary result code is automatically
+     * created in order to handle cases where unexpected result codes are
+     * returned from the server.
+     *
+     * @param intValue
+     *            The integer value of the result code.
+     * @return The result code.
+     */
+    public static ResultCode valueOf(final int intValue) {
+        ResultCode result = ELEMENTS.get(intValue);
+        if (result == null) {
+            result = new ResultCode(
+                intValue, LocalizableMessage.raw("unknown(" + intValue + ")"), true, Enum.UNKNOWN);
+        }
+        return result;
+    }
+
+    private static final List<ResultCode> IMMUTABLE_ELEMENTS = Collections.unmodifiableList(new ArrayList<ResultCode>(
+            ELEMENTS.values()));
+
+    /**
+     * Returns an unmodifiable list containing the set of available result codes
+     * indexed on their integer value as defined in RFC 4511 section 4.1.9.
+     *
+     * @return An unmodifiable list containing the set of available result
+     *         codes.
+     */
+    public static List<ResultCode> values() {
+        return IMMUTABLE_ELEMENTS;
+    }
+
+    /**
+     * Creates and registers a new error result code with the application.
+     *
+     * @param intValue
+     *            The integer value of the error result code as defined in RFC
+     *            4511 section 4.1.9.
+     * @param name
+     *            The name of the error result code.
+     * @param resultCodeEnum
+     *            The enum equivalent for this result code
+     * @return The new error result code.
+     */
+    private static ResultCode registerErrorResultCode(final int intValue,
+            final LocalizableMessage name, final Enum resultCodeEnum) {
+        final ResultCode t = new ResultCode(intValue, name, true, resultCodeEnum);
+        ELEMENTS.put(intValue, t);
+        return t;
+    }
+
+    /**
+     * Creates and registers a new success result code with the application.
+     *
+     * @param intValue
+     *            The integer value of the success result code as defined in RFC
+     *            4511 section 4.1.9.
+     * @param name
+     *            The name of the success result code.
+     * @param resultCodeEnum
+     *            The enum equivalent for this result code
+     * @return The new success result code.
+     */
+    private static ResultCode registerSuccessResultCode(final int intValue,
+            final LocalizableMessage name, final Enum resultCodeEnum) {
+        final ResultCode t = new ResultCode(intValue, name, false, resultCodeEnum);
+        ELEMENTS.put(intValue, t);
+        return t;
+    }
+
+    private final int intValue;
+
+    private final LocalizableMessage name;
+
+    private final boolean exceptional;
+
+    private final Enum resultCodeEnum;
+
+    /** Prevent direct instantiation. */
+    private ResultCode(final int intValue, final LocalizableMessage name, final boolean exceptional,
+            final Enum resultCodeEnum) {
+        this.intValue = intValue;
+        this.name = name;
+        this.exceptional = exceptional;
+        this.resultCodeEnum = resultCodeEnum;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof ResultCode) {
+            return this.intValue == ((ResultCode) obj).intValue;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the short human-readable name of this result code.
+     *
+     * @return The short human-readable name of this result code.
+     */
+    public LocalizableMessage getName() {
+        return name;
+    }
+
+    @Override
+    public int hashCode() {
+        return intValue;
+    }
+
+    /**
+     * Returns the integer value of this result code.
+     *
+     * @return The integer value of this result code.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    /**
+     * Returns the enum equivalent for this result code.
+     *
+     * @return The enum equivalent for this result code when a known mapping exists,
+     *         or {@link Enum#UNKNOWN} if this is an unknown result code.
+     */
+    public Enum asEnum() {
+        return this.resultCodeEnum;
+    }
+
+    /**
+     * Indicates whether or not this result code represents an error result.
+     * <p>
+     * The following result codes are NOT interpreted as error results:
+     * <ul>
+     * <li>{@link #SUCCESS}
+     * <li>{@link #COMPARE_FALSE}
+     * <li>{@link #COMPARE_TRUE}
+     * <li>{@link #SASL_BIND_IN_PROGRESS}
+     * <li>{@link #NO_OPERATION}
+     * </ul>
+     * In order to make it easier for application to detect referrals, the
+     * {@link #REFERRAL} result code is interpreted as an error result (the LDAP
+     * RFCs treat referrals as a success response).
+     *
+     * @return {@code true} if this result code represents an error result,
+     *         otherwise {@code false}.
+     */
+    public boolean isExceptional() {
+        return exceptional;
+    }
+
+    /**
+     * Returns the string representation of this result code.
+     *
+     * @return The string representation of this result code.
+     */
+    @Override
+    public String toString() {
+        return name.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RootDSE.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RootDSE.java
new file mode 100644
index 0000000..5f73484
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/RootDSE.java
@@ -0,0 +1,425 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.CoreSchema;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Collections2;
+
+/**
+ * The root DSE is a DSA-specific Entry (DSE) and not part of any naming context
+ * (or any subtree), and which is uniquely identified by the empty DN.
+ * <p>
+ * A Directory Server uses the root DSE to provide information about itself
+ * using the following set of attributes:
+ * <ul>
+ * <li>{@code altServer}: alternative Directory Servers
+ * <li>{@code namingContexts}: naming contexts
+ * <li>{@code supportedControl}: recognized LDAP controls
+ * <li>{@code supportedExtension}: recognized LDAP extended operations
+ * <li>{@code supportedFeatures}: recognized LDAP features
+ * <li>{@code supportedLDAPVersion}: LDAP versions supported
+ * <li>{@code supportedSASLMechanisms}: recognized SASL authentication
+ * mechanisms
+ * <li>{@code supportedAuthPasswordSchemes}: recognized authentication password
+ * schemes
+ * <li>{@code subschemaSubentry}: the name of the subschema subentry holding the
+ * schema controlling the Root DSE
+ * <li>{@code vendorName}: the name of the Directory Server implementer
+ * <li>{@code vendorVersion}: the version of the Directory Server
+ * implementation.
+ * </ul>
+ * The values provided for these attributes may depend on session- specific and
+ * other factors. For example, a server supporting the SASL EXTERNAL mechanism
+ * might only list "EXTERNAL" when the client's identity has been established by
+ * a lower level.
+ * <p>
+ * The root DSE may also include a {@code subschemaSubentry} attribute. If it
+ * does, the attribute refers to the subschema (sub)entry holding the schema
+ * controlling the root DSE. Clients SHOULD NOT assume that this subschema
+ * (sub)entry controls other entries held by the server.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4512">RFC 4512 - Lightweight
+ *      Directory Access Protocol (LDAP): Directory Information Models </a>
+ * @see <a href="http://tools.ietf.org/html/rfc3045">RFC 3045 - Storing Vendor
+ *      Information in the LDAP Root DSE </a>
+ * @see <a href="http://tools.ietf.org/html/rfc3112">RFC 3112 - LDAP
+ *      Authentication Password Schema </a>
+ */
+public final class RootDSE {
+    private static final AttributeDescription ATTR_ALT_SERVER = AttributeDescription
+            .create(CoreSchema.getAltServerAttributeType());
+
+    private static final AttributeDescription ATTR_NAMING_CONTEXTS = AttributeDescription
+            .create(CoreSchema.getNamingContextsAttributeType());
+
+    private static final AttributeDescription ATTR_SUBSCHEMA_SUBENTRY = AttributeDescription
+            .create(CoreSchema.getSubschemaSubentryAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_AUTH_PASSWORD_SCHEMES =
+            AttributeDescription.create(CoreSchema.getSupportedAuthPasswordSchemesAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_CONTROL = AttributeDescription
+            .create(CoreSchema.getSupportedControlAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_EXTENSION = AttributeDescription
+            .create(CoreSchema.getSupportedExtensionAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_FEATURE = AttributeDescription
+            .create(CoreSchema.getSupportedFeaturesAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_LDAP_VERSION = AttributeDescription
+            .create(CoreSchema.getSupportedLDAPVersionAttributeType());
+
+    private static final AttributeDescription ATTR_SUPPORTED_SASL_MECHANISMS = AttributeDescription
+            .create(CoreSchema.getSupportedSASLMechanismsAttributeType());
+
+    private static final AttributeDescription ATTR_VENDOR_NAME = AttributeDescription
+            .create(CoreSchema.getVendorNameAttributeType());
+
+    private static final AttributeDescription ATTR_VENDOR_VERSION = AttributeDescription
+            .create(CoreSchema.getVendorNameAttributeType());
+
+    private static final AttributeDescription ATTR_FULL_VENDOR_VERSION = AttributeDescription
+            .create(CoreSchema.getFullVendorVersionAttributeType());
+
+    private static final SearchRequest SEARCH_REQUEST = Requests.newSearchRequest(DN.rootDN(),
+            SearchScope.BASE_OBJECT, Filter.objectClassPresent(), ATTR_ALT_SERVER.toString(),
+            ATTR_NAMING_CONTEXTS.toString(), ATTR_SUPPORTED_CONTROL.toString(),
+            ATTR_SUPPORTED_EXTENSION.toString(), ATTR_SUPPORTED_FEATURE.toString(),
+            ATTR_SUPPORTED_LDAP_VERSION.toString(), ATTR_SUPPORTED_SASL_MECHANISMS.toString(),
+            ATTR_FULL_VENDOR_VERSION.toString(),
+            ATTR_VENDOR_NAME.toString(), ATTR_VENDOR_VERSION.toString(),
+            ATTR_SUPPORTED_AUTH_PASSWORD_SCHEMES.toString(), ATTR_SUBSCHEMA_SUBENTRY.toString(),
+            "*");
+
+    /**
+     * Asynchronously reads the Root DSE from the Directory Server using the
+     * provided connection.
+     * <p>
+     * If the Root DSE is not returned by the Directory Server then the request
+     * will fail with an {@link EntryNotFoundException}. More specifically, the
+     * returned promise will never return {@code null}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose Root DSE is to be
+     *            read.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} was {@code null}.
+     */
+    public static LdapPromise<RootDSE> readRootDSEAsync(final Connection connection) {
+        return connection.searchSingleEntryAsync(SEARCH_REQUEST).then(
+            new Function<SearchResultEntry, RootDSE, LdapException>() {
+                @Override
+                public RootDSE apply(SearchResultEntry result) {
+                    return valueOf(result);
+                }
+            });
+    }
+
+    /**
+     * Reads the Root DSE from the Directory Server using the provided
+     * connection.
+     * <p>
+     * If the Root DSE is not returned by the Directory Server then the request
+     * will fail with an {@link EntryNotFoundException}. More specifically, this
+     * method will never return {@code null}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose Root DSE is to be
+     *            read.
+     * @return The Directory Server's Root DSE.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} was {@code null}.
+     */
+    public static RootDSE readRootDSE(final Connection connection) throws LdapException {
+        final Entry entry = connection.searchSingleEntry(SEARCH_REQUEST);
+        return valueOf(entry);
+    }
+
+    /**
+     * Creates a new Root DSE instance backed by the provided entry.
+     * Modifications made to {@code entry} will be reflected in the returned
+     * Root DSE. The returned Root DSE instance is unmodifiable and attempts to
+     * use modify any of the returned collections will result in a
+     * {@code UnsupportedOperationException}.
+     *
+     * @param entry
+     *            The Root DSE entry.
+     * @return A Root DSE instance backed by the provided entry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null} .
+     */
+    public static RootDSE valueOf(Entry entry) {
+        Reject.ifNull(entry);
+        return new RootDSE(entry);
+    }
+
+    private final Entry entry;
+
+    /** Prevent direct instantiation. */
+    private RootDSE(final Entry entry) {
+        this.entry = entry;
+    }
+
+    /**
+     * Returns an unmodifiable list of URIs referring to alternative Directory
+     * Servers that may be contacted when the Directory Server becomes
+     * unavailable.
+     * <p>
+     * URIs for Directory Servers implementing the LDAP protocol are written
+     * according to RFC 4516. Other kinds of URIs may be provided.
+     * <p>
+     * If the Directory Server does not know of any other Directory Servers that
+     * could be used, the returned list will be empty.
+     *
+     * @return An unmodifiable list of URIs referring to alternative Directory
+     *         Servers, which may be empty.
+     * @see <a href="http://tools.ietf.org/html/rfc4516">RFC 4516 - Lightweight
+     *      Directory Access Protocol (LDAP): Uniform Resource Locator </a>
+     */
+    public Collection<String> getAlternativeServers() {
+        return getMultiValuedAttribute(ATTR_ALT_SERVER, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns the entry which backs this Root DSE instance. Modifications made
+     * to the returned entry will be reflected in this Root DSE.
+     *
+     * @return The underlying Root DSE entry.
+     */
+    public Entry getEntry() {
+        return entry;
+    }
+
+    /**
+     * Returns an unmodifiable list of DNs identifying the context prefixes of
+     * the naming contexts that the Directory Server masters or shadows (in part
+     * or in whole).
+     * <p>
+     * If the Directory Server does not master or shadow any naming contexts,
+     * the returned list will be empty.
+     *
+     * @return An unmodifiable list of DNs identifying the context prefixes of
+     *         the naming contexts, which may be empty.
+     */
+    public Collection<DN> getNamingContexts() {
+        return getMultiValuedAttribute(ATTR_NAMING_CONTEXTS, Functions.byteStringToDN());
+    }
+
+    /**
+     * Returns a string which represents the DN of the subschema subentry
+     * holding the schema controlling the Root DSE.
+     * <p>
+     * Clients SHOULD NOT assume that this subschema (sub)entry controls other
+     * entries held by the Directory Server.
+     *
+     * @return The DN of the subschema subentry holding the schema controlling
+     *         the Root DSE, or {@code null} if the DN is not provided.
+     */
+    public DN getSubschemaSubentry() {
+        return getSingleValuedAttribute(ATTR_SUBSCHEMA_SUBENTRY, Functions.byteStringToDN());
+    }
+
+    /**
+     * Returns an unmodifiable list of supported authentication password schemes
+     * which the Directory Server supports.
+     * <p>
+     * If the Directory Server does not support any authentication password
+     * schemes, the returned list will be empty.
+     *
+     * @return An unmodifiable list of supported authentication password
+     *         schemes, which may be empty.
+     * @see <a href="http://tools.ietf.org/html/rfc3112">RFC 3112 - LDAP
+     *      Authentication Password Schema </a>
+     */
+    public Collection<String> getSupportedAuthenticationPasswordSchemes() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_AUTH_PASSWORD_SCHEMES, Functions
+                .byteStringToString());
+    }
+
+    /**
+     * Returns an unmodifiable list of object identifiers identifying the
+     * request controls that the Directory Server supports.
+     * <p>
+     * If the Directory Server does not support any request controls, the
+     * returned list will be empty. Object identifiers identifying response
+     * controls may not be listed.
+     *
+     * @return An unmodifiable list of object identifiers identifying the
+     *         request controls, which may be empty.
+     */
+    public Collection<String> getSupportedControls() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_CONTROL, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns an unmodifiable list of object identifiers identifying the
+     * extended operations that the Directory Server supports.
+     * <p>
+     * If the Directory Server does not support any extended operations, the
+     * returned list will be empty.
+     * <p>
+     * An extended operation generally consists of an extended request and an
+     * extended response but may also include other protocol data units (such as
+     * intermediate responses). The object identifier assigned to the extended
+     * request is used to identify the extended operation. Other object
+     * identifiers used in the extended operation may not be listed as values of
+     * this attribute.
+     *
+     * @return An unmodifiable list of object identifiers identifying the
+     *         extended operations, which may be empty.
+     */
+    public Collection<String> getSupportedExtendedOperations() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_EXTENSION, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns an unmodifiable list of object identifiers identifying elective
+     * features that the Directory Server supports.
+     * <p>
+     * If the server does not support any discoverable elective features, the
+     * returned list will be empty.
+     *
+     * @return An unmodifiable list of object identifiers identifying the
+     *         elective features, which may be empty.
+     */
+    public Collection<String> getSupportedFeatures() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_FEATURE, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns an unmodifiable list of the versions of LDAP that the Directory
+     * Server supports.
+     *
+     * @return An unmodifiable list of the versions.
+     */
+    public Collection<Integer> getSupportedLDAPVersions() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_LDAP_VERSION, Functions.byteStringToInteger());
+    }
+
+    /**
+     * Returns an unmodifiable list of the SASL mechanisms that the Directory
+     * Server recognizes and/or supports.
+     * <p>
+     * The contents of the returned list may depend on the current session state
+     * and may be empty if the Directory Server does not support any SASL
+     * mechanisms.
+     *
+     * @return An unmodifiable list of the SASL mechanisms, which may be empty.
+     * @see <a href="http://tools.ietf.org/html/rfc4513">RFC 4513 - Lightweight
+     *      Directory Access Protocol (LDAP): Authentication Methods and
+     *      Security Mechanisms </a>
+     * @see <a href="http://tools.ietf.org/html/rfc4422">RFC 4422 - Simple
+     *      Authentication and Security Layer (SASL) </a>
+     */
+    public Collection<String> getSupportedSASLMechanisms() {
+        return getMultiValuedAttribute(ATTR_SUPPORTED_SASL_MECHANISMS, Functions
+                .byteStringToString());
+    }
+
+    /**
+     * Returns a string which represents the name of the Directory Server
+     * implementer.
+     *
+     * @return The name of the Directory Server implementer, or {@code null} if
+     *         the vendor name is not provided.
+     * @see <a href="http://tools.ietf.org/html/rfc3045">RFC 3045 - Storing
+     *      Vendor Information in the LDAP Root DSE </a>
+     */
+    public String getVendorName() {
+        return getSingleValuedAttribute(ATTR_VENDOR_NAME, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns a string which represents the version of the Directory Server
+     * implementation.
+     * <p>
+     * Note that this value is typically a release value comprised of a string
+     * and/or a string of numbers used by the developer of the LDAP server
+     * product. The returned string will be unique between two versions of the
+     * Directory Server, but there are no other syntactic restrictions on the
+     * value or the way it is formatted.
+     *
+     * @return The version of the Directory Server implementation, or
+     *         {@code null} if the vendor version is not provided.
+     * @see <a href="http://tools.ietf.org/html/rfc3045">RFC 3045 - Storing
+     *      Vendor Information in the LDAP Root DSE </a>
+     */
+    public String getVendorVersion() {
+        return getSingleValuedAttribute(ATTR_VENDOR_VERSION, Functions.byteStringToString());
+    }
+
+    /**
+     * Returns a string which represents the full version of the Directory Server
+     * implementation.
+     *
+     * @return The full version of the Directory Server implementation, or
+     *         {@code null} if the vendor version is not provided.
+     */
+    public String getFullVendorVersion() {
+        return getSingleValuedAttribute(ATTR_FULL_VENDOR_VERSION, Functions.byteStringToString());
+    }
+
+    private <N> Collection<N> getMultiValuedAttribute(
+            final AttributeDescription attributeDescription,
+        final Function<ByteString, N, NeverThrowsException> function) {
+        // The returned collection is unmodifiable because we may need to
+        // return an empty collection if the attribute does not exist in the
+        // underlying entry. If a value is then added to the returned empty
+        // collection it would require that an attribute is created in the
+        // underlying entry in order to maintain consistency.
+        final Attribute attr = entry.getAttribute(attributeDescription);
+        if (attr != null) {
+            return Collections.unmodifiableCollection(Collections2.transformedCollection(attr,
+                    function, Functions.objectToByteString()));
+        }
+        return Collections.emptySet();
+    }
+
+    private <N> N getSingleValuedAttribute(final AttributeDescription attributeDescription,
+        final Function<ByteString, N, NeverThrowsException> function) {
+        final Attribute attr = entry.getAttribute(attributeDescription);
+        if (attr != null && !attr.isEmpty()) {
+            return function.apply(attr.firstValue());
+        }
+        return null;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SSLContextBuilder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SSLContextBuilder.java
new file mode 100644
index 0000000..169d966
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SSLContextBuilder.java
@@ -0,0 +1,212 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.security.GeneralSecurityException;
+import java.security.Provider;
+import java.security.SecureRandom;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+
+/**
+ * An SSL context builder provides an interface for incrementally constructing
+ * {@link SSLContext} instances for use when securing connections with SSL or
+ * the StartTLS extended operation. The {@link #getSSLContext()} should be
+ * called in order to obtain the {@code SSLContext}.
+ * <p>
+ * For example, use the SSL context builder when setting up LDAP options needed
+ * to use StartTLS. {@link org.forgerock.opendj.ldap.TrustManagers
+ * TrustManagers} has methods you can use to set the trust manager for the SSL
+ * context builder.
+ *
+ * <pre>
+ * LDAPOptions options = new LDAPOptions();
+ * SSLContext sslContext =
+ *         new SSLContextBuilder().setTrustManager(...).getSSLContext();
+ * options.setSSLContext(sslContext);
+ * options.setUseStartTLS(true);
+ *
+ * String host = ...;
+ * int port = ...;
+ * LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
+ * Connection connection = factory.getConnection();
+ * // Connection uses StartTLS...
+ * </pre>
+ */
+public final class SSLContextBuilder {
+    /** SSL protocol: supports some version of SSL; may support other versions. */
+    public static final String PROTOCOL_SSL = "SSL";
+    /** SSL protocol: supports SSL version 2 or higher; may support other versions. */
+    public static final String PROTOCOL_SSL2 = "SSLv2";
+    /** SSL protocol: supports SSL version 3; may support other versions. */
+    public static final String PROTOCOL_SSL3 = "SSLv3";
+    /** SSL protocol: supports some version of TLS; may support other versions. */
+    public static final String PROTOCOL_TLS = "TLS";
+    /**
+     * SSL protocol: supports RFC 2246: TLS version 1.0 ; may support other versions.
+     * <p>
+     * This is the default version.
+     */
+    public static final String PROTOCOL_TLS1 = "TLSv1";
+    /** SSL protocol: supports RFC 4346: TLS version 1.1 ; may support other versions. */
+    public static final String PROTOCOL_TLS1_1 = "TLSv1.1";
+    /** SSL protocol: supports RFC 5246: TLS version 1.2 ; may support other versions. */
+    public static final String PROTOCOL_TLS1_2 = "TLSv1.2";
+
+    private TrustManager trustManager;
+    private KeyManager keyManager;
+    private String protocol = PROTOCOL_TLS1;
+    private SecureRandom random;
+
+    /** These are mutually exclusive. */
+    private Provider provider;
+    private String providerName;
+
+    /** Creates a new SSL context builder using default parameters. */
+    public SSLContextBuilder() {
+        // Do nothing.
+    }
+
+    /**
+     * Creates a {@code SSLContext} using the parameters of this SSL context
+     * builder.
+     *
+     * @return A {@code SSLContext} using the parameters of this SSL context
+     *         builder.
+     * @throws GeneralSecurityException
+     *             If the SSL context could not be created, perhaps due to
+     *             missing algorithms.
+     */
+    public SSLContext getSSLContext() throws GeneralSecurityException {
+        TrustManager[] tm = null;
+        if (trustManager != null) {
+            tm = new TrustManager[] { trustManager };
+        }
+
+        KeyManager[] km = null;
+        if (keyManager != null) {
+            km = new KeyManager[] { keyManager };
+        }
+
+        SSLContext sslContext;
+        if (provider != null) {
+            sslContext = SSLContext.getInstance(protocol, provider);
+        } else if (providerName != null) {
+            sslContext = SSLContext.getInstance(protocol, providerName);
+        } else {
+            sslContext = SSLContext.getInstance(protocol);
+        }
+        sslContext.init(km, tm, random);
+
+        return sslContext;
+    }
+
+    /**
+     * Sets the key manager which the SSL context should use. By default, no key
+     * manager is specified indicating that no certificates will be used.
+     *
+     * @param keyManager
+     *            The key manager which the SSL context should use, which may be
+     *            {@code null} indicating that no certificates will be used.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setKeyManager(final KeyManager keyManager) {
+        this.keyManager = keyManager;
+        return this;
+    }
+
+    /**
+     * Sets the protocol which the SSL context should use. By default, TLSv1
+     * will be used.
+     *
+     * @param protocol
+     *            The protocol which the SSL context should use, which may be
+     *            {@code null} indicating that TLSv1 will be used.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setProtocol(final String protocol) {
+        this.protocol = protocol;
+        return this;
+    }
+
+    /**
+     * Sets the provider which the SSL context should use. By default, the
+     * default provider associated with this JVM will be used.
+     *
+     * @param provider
+     *            The provider which the SSL context should use, which may be
+     *            {@code null} indicating that the default provider associated
+     *            with this JVM will be used.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setProvider(final Provider provider) {
+        this.provider = provider;
+        this.providerName = null;
+        return this;
+    }
+
+    /**
+     * Sets the provider which the SSL context should use. By default, the
+     * default provider associated with this JVM will be used.
+     *
+     * @param providerName
+     *            The name of the provider which the SSL context should use,
+     *            which may be {@code null} indicating that the default provider
+     *            associated with this JVM will be used.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setProvider(final String providerName) {
+        this.provider = null;
+        this.providerName = providerName;
+        return this;
+    }
+
+    /**
+     * Sets the secure random number generator which the SSL context should use.
+     * By default, the default secure random number generator associated with
+     * this JVM will be used.
+     *
+     * @param random
+     *            The secure random number generator which the SSL context
+     *            should use, which may be {@code null} indicating that the
+     *            default secure random number generator associated with this
+     *            JVM will be used.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setSecureRandom(final SecureRandom random) {
+        this.random = random;
+        return this;
+    }
+
+    /**
+     * Sets the trust manager which the SSL context should use. By default, no
+     * trust manager is specified indicating that only certificates signed by
+     * the authorities associated with this JVM will be accepted.
+     *
+     * @param trustManager
+     *            The trust manager which the SSL context should use, which may
+     *            be {@code null} indicating that only certificates signed by
+     *            the authorities associated with this JVM will be accepted.
+     * @return This SSL context builder.
+     */
+    public SSLContextBuilder setTrustManager(final TrustManager trustManager) {
+        this.trustManager = trustManager;
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SchemaResolver.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SchemaResolver.java
new file mode 100644
index 0000000..2e285da
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SchemaResolver.java
@@ -0,0 +1,60 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * Schema resolvers are included with a set of {@code DecodeOptions} in order to
+ * allow application to control how {@code Schema} instances are selected when
+ * decoding requests and responses.
+ * <p>
+ * Implementations must be thread safe. More specifically, any schema caching
+ * performed by the implementation must be capable of handling multiple
+ * concurrent schema requests.
+ *
+ * @see Schema
+ * @see DecodeOptions
+ */
+public interface SchemaResolver {
+    /**
+     * A schema resolver which always returns the current default schema as
+     * returned by {@link Schema#getDefaultSchema()}.
+     */
+    SchemaResolver DEFAULT = new SchemaResolver() {
+        @Override
+        public Schema resolveSchema(String dn) {
+            return Schema.getDefaultSchema();
+        }
+    };
+
+    /**
+     * Finds the appropriate schema for use with the provided distinguished
+     * name.
+     * <p>
+     * Schema resolution must always succeed regardless of any errors that
+     * occur.
+     *
+     * @param dn
+     *            The string representation of a distinguished name associated
+     *            with an entry whose schema is to be located.
+     * @return The appropriate schema for use with the provided distinguished
+     *         name.
+     */
+    Schema resolveSchema(String dn);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultHandler.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultHandler.java
new file mode 100644
index 0000000..c9be04c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultHandler.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+/**
+ * A completion handler for consuming the results of a Search operation.
+ * <p>
+ * {@link Connection} and {@link Connection} objects allow a search result
+ * completion handler to be specified when sending Search operation requests to
+ * a Directory Server. The {@link #handleEntry} method is invoked each time a
+ * Search Result Entry is returned from the Directory Server. The
+ * {@link #handleReference} method is invoked for each Search Result Reference
+ * returned from the Directory Server.
+ * <p>
+ * Implementations of these methods should complete in a timely manner so as to
+ * avoid keeping the invoking thread from dispatching to other completion
+ * handlers.
+ */
+public interface SearchResultHandler {
+    /**
+     * Invoked each time a search result entry is returned from an asynchronous
+     * search operation.
+     *
+     * @param entry
+     *            The search result entry.
+     * @return {@code true} if this handler should continue to be notified of
+     *         any remaining entries and references, or {@code false} if the
+     *         remaining entries and references should be skipped for some
+     *         reason (e.g. a client side size limit has been reached).
+     */
+    boolean handleEntry(SearchResultEntry entry);
+
+    /**
+     * Invoked each time a search result reference is returned from an
+     * asynchronous search operation.
+     *
+     * @param reference
+     *            The search result reference.
+     * @return {@code true} if this handler should continue to be notified of
+     *         any remaining entries and references, or {@code false} if the
+     *         remaining entries and references should be skipped for some
+     *         reason (e.g. a client side size limit has been reached).
+     */
+    boolean handleReference(SearchResultReference reference);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultReferenceIOException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultReferenceIOException.java
new file mode 100644
index 0000000..7952b33
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchResultReferenceIOException.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Thrown when an iteration over a set of search results using a
+ * {@code ConnectionEntryReader} encounters a {@code SearchResultReference}.
+ */
+@SuppressWarnings("serial")
+public final class SearchResultReferenceIOException extends IOException {
+    private final SearchResultReference reference;
+
+    /**
+     * Creates a new referral result IO exception with the provided
+     * {@code SearchResultReference}.
+     *
+     * @param reference
+     *            The {@code SearchResultReference} which may be later retrieved
+     *            by the {@link #getReference} method.
+     * @throws NullPointerException
+     *             If {@code reference} was {@code null}.
+     */
+    public SearchResultReferenceIOException(final SearchResultReference reference) {
+        super(Reject.checkNotNull(reference).toString());
+        this.reference = reference;
+    }
+
+    /**
+     * Returns the {@code SearchResultReference} which was encountered while
+     * processing the search results.
+     *
+     * @return The {@code SearchResultReference} which was encountered while
+     *         processing the search results.
+     */
+    public SearchResultReference getReference() {
+        return reference;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchScope.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchScope.java
new file mode 100644
index 0000000..d3727ca
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SearchScope.java
@@ -0,0 +1,204 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A Search operation search scope as defined in RFC 4511 section 4.5.1.2 is
+ * used to specify the scope of a Search operation.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511#section-4.5.1.2">RFC 4511 -
+ *      Lightweight Directory Access Protocol (LDAP): The Protocol </a>
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-sermersheim-ldap-subordinate-scope">
+ *      draft-sermersheim-ldap-subordinate-scope - Subordinate Subtree Search
+ *      Scope for LDAP </a>
+ */
+public final class SearchScope {
+    /**
+     * Contains equivalent values for the SearchScope values.
+     * This allows easily using SearchScope values with switch statements.
+     */
+    public static enum Enum {
+        //@Checkstyle:off
+        /** @see SearchScope#BASE_OBJECT */
+        BASE_OBJECT,
+        /** @see SearchScope#SINGLE_LEVEL */
+        SINGLE_LEVEL,
+        /** @see SearchScope#WHOLE_SUBTREE */
+        WHOLE_SUBTREE,
+        /** @see SearchScope#SUBORDINATES */
+        SUBORDINATES,
+        /** Used for unknown search scopes. */
+        UNKNOWN;
+        //@Checkstyle:on
+    }
+
+    private static final SearchScope[] ELEMENTS = new SearchScope[4];
+
+    private static final List<SearchScope> IMMUTABLE_ELEMENTS = Collections.unmodifiableList(Arrays
+            .asList(ELEMENTS));
+
+    /** The scope is constrained to the search base entry. */
+    public static final SearchScope BASE_OBJECT = register(0, "base", Enum.BASE_OBJECT);
+
+    /** The scope is constrained to the immediate subordinates of the search base entry. */
+    public static final SearchScope SINGLE_LEVEL = register(1, "one", Enum.SINGLE_LEVEL);
+
+    /** The scope is constrained to the search base entry and to all its subordinates. */
+    public static final SearchScope WHOLE_SUBTREE = register(2, "sub", Enum.WHOLE_SUBTREE);
+
+    /**
+     * The scope is constrained to all the subordinates of the search base
+     * entry, but does not include the search base entry itself (as wholeSubtree
+     * does).
+     */
+    public static final SearchScope SUBORDINATES = register(3, "subordinates", Enum.SUBORDINATES);
+
+    /**
+     * Returns the search scope having the specified integer value as defined in
+     * RFC 4511 section 4.5.1.2.
+     *
+     * @param intValue
+     *            The integer value of the search scope.
+     * @return The search scope, or {@code null} if there was no search scope
+     *         associated with {@code intValue}.
+     */
+    public static SearchScope valueOf(final int intValue) {
+        SearchScope result = null;
+        if (0 <= intValue && intValue < ELEMENTS.length) {
+            result = ELEMENTS[intValue];
+        }
+        if (result == null) {
+            result = new SearchScope(intValue, "unknown(" + intValue + ")", Enum.UNKNOWN);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the search scope having the specified name as defined in RFC 4511
+     * section 4.5.1.2.
+     *
+     * @param name
+     *          the name of the search scope to return
+     * @return The search scope, or {@code null} if there was no search scope
+     *         associated with {@code name}.
+     * @throws NullPointerException
+     *           if name is null
+     */
+    public static SearchScope valueOf(String name) {
+        for (SearchScope searchScope : ELEMENTS) {
+            if (searchScope.name.equals(name)) {
+                return searchScope;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns an unmodifiable list containing the set of available search
+     * scopes indexed on their integer value as defined in RFC 4511 section
+     * 4.5.1.2.
+     *
+     * @return An unmodifiable list containing the set of available search
+     *         scopes.
+     */
+    public static List<SearchScope> values() {
+        return IMMUTABLE_ELEMENTS;
+    }
+
+    /**
+     * Creates and registers a new search scope with the application.
+     *
+     * @param intValue
+     *            The integer value of the search scope as defined in RFC 4511
+     *            section 4.5.1.2.
+     * @param name
+     *            The name of the search scope as defined in RFC 4516.
+     * @param searchScopeEnum
+     *            The enum equivalent for this search scope
+     * @return The new search scope.
+     */
+    private static SearchScope register(final int intValue, final String name, Enum searchScopeEnum) {
+        final SearchScope t = new SearchScope(intValue, name, searchScopeEnum);
+        ELEMENTS[intValue] = t;
+        return t;
+    }
+
+    private final int intValue;
+
+    private final String name;
+
+    private final Enum searchScopeEnum;
+
+    /** Prevent direct instantiation. */
+    private SearchScope(final int intValue, final String name, Enum searchScopeEnum) {
+        this.intValue = intValue;
+        this.name = name;
+        this.searchScopeEnum = searchScopeEnum;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof SearchScope) {
+            return this.intValue == ((SearchScope) obj).intValue;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return intValue;
+    }
+
+    /**
+     * Returns the integer value of this search scope as defined in RFC 4511
+     * section 4.5.1.2.
+     *
+     * @return The integer value of this search scope.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    /**
+     * Returns the enum equivalent for this search scope.
+     *
+     * @return The enum equivalent for this search scope when a known mapping exists,
+     *         or {@link Enum#UNKNOWN} if this is an unknown search scope.
+     */
+    public Enum asEnum() {
+        return this.searchScopeEnum;
+    }
+
+    /**
+     * Returns the string representation of this search scope as defined in RFC
+     * 4516.
+     *
+     * @return The string representation of this search scope.
+     */
+    @Override
+    public String toString() {
+        return name;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnection.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnection.java
new file mode 100644
index 0000000..a8eab85
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnection.java
@@ -0,0 +1,88 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+
+/**
+ * A handler interface for interacting with client connections. A
+ * {@code ServerConnection} is associated with a client connection when the
+ * {@link ServerConnectionFactory#handleAccept(Object) handleAccept} method is
+ * invoked against a {@code ServerConnectionFactory}.
+ * <p>
+ * Implementations are responsible for handling connection life-cycle as well as
+ * request life-cycle. In particular, a {@code ServerConnection} is responsible
+ * for processing abandon and unbind requests, as well as extended operations
+ * such as {@code StartTLS} and {@code Cancel} operations.
+ *
+ * @param <C>
+ *            The type of request context.
+ * @see ServerConnectionFactory
+ */
+public interface ServerConnection<C> extends RequestHandler<C> {
+
+    /**
+     * Invoked when an abandon request is received from a client.
+     *
+     * @param requestContext
+     *            The request context.
+     * @param request
+     *            The abandon request.
+     * @throws UnsupportedOperationException
+     *             If this server connection does not handle abandon requests.
+     */
+    void handleAbandon(C requestContext, AbandonRequest request);
+
+    /**
+     * Invoked when the client closes the connection, possibly using an unbind
+     * request.
+     *
+     * @param requestContext
+     *            The request context which should be ignored if there was no
+     *            associated unbind request.
+     * @param request
+     *            The unbind request, which may be {@code null} if one was not
+     *            sent before the connection was closed.
+     */
+    void handleConnectionClosed(C requestContext, UnbindRequest request);
+
+    /**
+     * Invoked when the server disconnects the client connection, possibly using
+     * a disconnect notification.
+     *
+     * @param resultCode
+     *            The result code which was included with the disconnect
+     *            notification, or {@code null} if no disconnect notification
+     *            was sent.
+     * @param message
+     *            The diagnostic message, which may be empty or {@code null}
+     *            indicating that none was provided.
+     */
+    void handleConnectionDisconnected(ResultCode resultCode, String message);
+
+    /**
+     * Invoked when an error occurs on the connection and it is no longer
+     * usable.
+     *
+     * @param error
+     *            The exception describing the problem that occurred.
+     */
+    void handleConnectionError(Throwable error);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnectionFactory.java
new file mode 100644
index 0000000..996b216
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/ServerConnectionFactory.java
@@ -0,0 +1,55 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+/**
+ * A handler interface for accepting new connections from clients.
+ * <p>
+ * A connection listener implementation, such as {@link LDAPListener} or
+ * {@link Connections#newInternalConnectionFactory newInternalConnectionFactory}
+ * , invoke the method {@link #handleAccept(Object) handleAccept} whenever a new
+ * client connection is accepted.
+ *
+ * @param <C>
+ *            The type of client context.
+ * @param <R>
+ *            The type of request context.
+ * @see LDAPListener
+ * @see Connections#newInternalConnectionFactory(ServerConnectionFactory,
+ *      Object) newInternalConnectionFactory
+ */
+public interface ServerConnectionFactory<C, R> {
+    /**
+     * Invoked when a new client connection is accepted by the associated
+     * listener. Implementations should return a {@code ServerConnection} which
+     * will be used to handle requests from the client connection.
+     *
+     * @param clientContext
+     *            The protocol dependent context information associated with the
+     *            client connection. Depending on the protocol this may contain
+     *            information about the client such as their address and level
+     *            connection security. It may also be used to manage the state
+     *            of the client's connection.
+     * @return A {@code ServerConnection} which will be used to handle requests
+     *         from a client connection.
+     * @throws LdapException
+     *             If this server connection factory cannot accept the client
+     *             connection.
+     */
+    ServerConnection<R> handleAccept(C clientContext) throws LdapException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SortKey.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SortKey.java
new file mode 100644
index 0000000..30555eb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/SortKey.java
@@ -0,0 +1,538 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/**
+ * A search result sort key as defined in RFC 2891 is used to specify how search
+ * result entries should be ordered. Sort keys are used with the server side
+ * sort request control
+ * {@link org.forgerock.opendj.ldap.controls.ServerSideSortRequestControl}, but
+ * could also be used for performing client side sorting as well.
+ * <p>
+ * The following example illustrates how a single sort key may be used to sort
+ * entries as they are returned from a search operation using the {@code cn}
+ * attribute as the sort key:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * SearchRequest request = ...;
+ *
+ * Comparator&lt;Entry> comparator = SortKey.comparator("cn");
+ * Set&lt;SearchResultEntry>; results = new TreeSet&lt;SearchResultEntry>(comparator);
+ *
+ * connection.search(request, results);
+ * </pre>
+ *
+ * A sort key includes an attribute description and a boolean value that
+ * indicates whether the sort should be ascending or descending. It may also
+ * contain a specific ordering matching rule that should be used for the sorting
+ * process, although if none is provided it will use the default ordering
+ * matching rule for the attribute type.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control
+ *      Extension for Server Side Sorting of Search Results </a>
+ */
+public final class SortKey {
+    private static final class CompositeEntryComparator implements Comparator<Entry> {
+        private final List<Comparator<Entry>> comparators;
+
+        private CompositeEntryComparator(final List<Comparator<Entry>> comparators) {
+            this.comparators = comparators;
+        }
+
+        @Override
+        public int compare(final Entry entry1, final Entry entry2) {
+            for (final Comparator<Entry> comparator : comparators) {
+                final int result = comparator.compare(entry1, entry2);
+                if (result != 0) {
+                    return result;
+                }
+            }
+            return 0;
+        }
+
+    }
+
+    /**
+     * A comparator which can be used to compare entries using a sort key.
+     */
+    private static final class EntryComparator implements Comparator<Entry> {
+        private final AttributeDescription attributeDescription;
+        private final MatchingRule matchingRule;
+        private final boolean isReverseOrder;
+
+        private EntryComparator(final AttributeDescription attributeDescription,
+                final MatchingRule matchingRule, final boolean isReverseOrder) {
+            this.attributeDescription = attributeDescription;
+            this.matchingRule = matchingRule;
+            this.isReverseOrder = isReverseOrder;
+        }
+
+        /**
+         * We must use the lowest available value in both entries and missing
+         * attributes sort last.
+         */
+        @Override
+        public int compare(final Entry entry1, final Entry entry2) {
+            // Find and normalize the lowest value attribute in each entry.
+            final ByteString normalizedValue1 = lowestValueOf(entry1);
+            final ByteString normalizedValue2 = lowestValueOf(entry2);
+
+            // Entries with missing attributes always sort after (regardless of
+            // order).
+            if (normalizedValue1 == null) {
+                return normalizedValue2 != null ? 1 : 0;
+            } else if (normalizedValue2 == null) {
+                return -1;
+            } else if (isReverseOrder) {
+                return normalizedValue2.compareTo(normalizedValue1);
+            } else {
+                return normalizedValue1.compareTo(normalizedValue2);
+            }
+        }
+
+        private ByteString lowestValueOf(final Entry entry) {
+            ByteString normalizedValue = null;
+            for (final Attribute attribute : entry.getAllAttributes(attributeDescription)) {
+                for (final ByteString value : attribute) {
+                    try {
+                        final ByteString tmp = matchingRule.normalizeAttributeValue(value);
+                        if (normalizedValue == null || tmp.compareTo(normalizedValue) < 0) {
+                            normalizedValue = tmp;
+                        }
+                    } catch (final DecodeException ignored) {
+                        // Ignore the error - treat the value as missing.
+                    }
+                }
+            }
+            return normalizedValue;
+        }
+
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * the provided list of sort keys. The sort keys will be decoded using the
+     * default schema.
+     *
+     * @param keys
+     *            The list of sort keys.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If one of the sort keys could not be converted to a
+     *             comparator.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code keys} was {@code null}.
+     */
+    public static Comparator<Entry> comparator(final Collection<SortKey> keys) {
+        return comparator(Schema.getDefaultSchema(), keys);
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * the provided list of sort keys. The sort keys will be decoded using the
+     * provided schema.
+     *
+     * @param schema
+     *            The schema which should be used for decoding the sort keys.
+     * @param keys
+     *            The list of sort keys.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If one of the sort keys could not be converted to a
+     *             comparator.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code schema} or {@code keys} was {@code null}.
+     */
+    public static Comparator<Entry> comparator(final Schema schema, final Collection<SortKey> keys) {
+        Reject.ifNull(schema, keys);
+        Reject.ifFalse(!keys.isEmpty(), "keys must not be empty");
+
+        final List<Comparator<Entry>> comparators = new ArrayList<>(keys.size());
+        for (final SortKey key : keys) {
+            comparators.add(key.comparator(schema));
+        }
+        return new CompositeEntryComparator(comparators);
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * the provided list of sort keys. The sort keys will be decoded using the
+     * provided schema.
+     *
+     * @param schema
+     *            The schema which should be used for decoding the sort keys.
+     * @param keys
+     *            The list of sort keys.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If one of the sort keys could not be converted to a
+     *             comparator.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code schema} or {@code keys} was {@code null}.
+     */
+    public static Comparator<Entry> comparator(final Schema schema, final SortKey... keys) {
+        return comparator(schema, Arrays.asList(keys));
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * the provided list of sort keys. The sort keys will be decoded using the
+     * default schema.
+     *
+     * @param keys
+     *            The list of sort keys.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If one of the sort keys could not be converted to a
+     *             comparator.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code keys} was {@code null}.
+     */
+    public static Comparator<Entry> comparator(final SortKey... keys) {
+        return comparator(Schema.getDefaultSchema(), keys);
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * the provided string representation of a list of sort keys. The sort keys
+     * will be decoded using the default schema. The string representation is
+     * comprised of a comma separate list of sort keys as defined in
+     * {@link #valueOf(String)}. There must be at least one sort key present in
+     * the string representation.
+     *
+     * @param sortKeys
+     *            The list of sort keys.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code sortKeys} is not a valid string representation of a
+     *             list of sort keys, or if one of the sort keys could not be
+     *             converted to a comparator.
+     * @throws NullPointerException
+     *             If {@code sortKeys} was {@code null}.
+     */
+    public static Comparator<Entry> comparator(final String sortKeys) {
+        Reject.ifNull(sortKeys);
+
+        final List<Comparator<Entry>> comparators = new LinkedList<>();
+        final StringTokenizer tokenizer = new StringTokenizer(sortKeys, ",");
+        while (tokenizer.hasMoreTokens()) {
+            final String token = tokenizer.nextToken().trim();
+            comparators.add(valueOf(token).comparator());
+        }
+        if (comparators.isEmpty()) {
+            final LocalizableMessage message = ERR_SORT_KEY_NO_SORT_KEYS.get(sortKeys);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+        return new CompositeEntryComparator(comparators);
+    }
+
+    /**
+     * Parses the provided string representation of a sort key as a
+     * {@code SortKey}. The string representation has the following ABNF (see
+     * RFC 4512 for definitions of the elements below):
+     *
+     * <pre>
+     *   SortKey = [ PLUS / HYPHEN ]    ; order specifier
+     *             attributedescription ; attribute description
+     *             [ COLON oid ]        ; ordering matching rule OID
+     * </pre>
+     *
+     * Examples:
+     *
+     * <pre>
+     *   cn                           ; case ignore ascending sort on "cn"
+     *   -cn                          ; case ignore descending sort on "cn"
+     *   +cn;lang-fr                  ; case ignore ascending sort on "cn;lang-fr"
+     *   -cn;lang-fr:caseExactMatch   ; case exact ascending sort on "cn;lang-fr"
+     * </pre>
+     *
+     * @param sortKey
+     *            The string representation of a sort key.
+     * @return The parsed {@code SortKey}.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code sortKey} is not a valid string representation of a
+     *             sort key.
+     * @throws NullPointerException
+     *             If {@code sortKey} was {@code null}.
+     */
+    public static SortKey valueOf(String sortKey) {
+        Reject.ifNull(sortKey);
+
+        boolean reverseOrder = false;
+        if (sortKey.startsWith("-")) {
+            reverseOrder = true;
+            sortKey = sortKey.substring(1);
+        } else if (sortKey.startsWith("+")) {
+            sortKey = sortKey.substring(1);
+        }
+
+        final int colonPos = sortKey.indexOf(':');
+        if (colonPos < 0) {
+            if (sortKey.length() == 0) {
+                final LocalizableMessage message = ERR_SORT_KEY_NO_ATTR_NAME.get(sortKey);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            return new SortKey(sortKey, reverseOrder, null);
+        } else if (colonPos == 0) {
+            final LocalizableMessage message = ERR_SORT_KEY_NO_ATTR_NAME.get(sortKey);
+            throw new LocalizedIllegalArgumentException(message);
+        } else if (colonPos == (sortKey.length() - 1)) {
+            final LocalizableMessage message = ERR_SORT_KEY_NO_MATCHING_RULE.get(sortKey);
+            throw new LocalizedIllegalArgumentException(message);
+        } else {
+            final String attrName = sortKey.substring(0, colonPos);
+            final String ruleID = sortKey.substring(colonPos + 1);
+
+            return new SortKey(attrName, reverseOrder, ruleID);
+        }
+    }
+
+    private final String attributeDescription;
+
+    private final String orderingMatchingRule;
+
+    private final boolean isReverseOrder;
+
+    /**
+     * Creates a new sort key using the provided attribute description. The
+     * returned sort key will compare attributes in the order specified using
+     * the named ordering matching rule.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be sorted using this sort key.
+     * @param isReverseOrder
+     *            {@code true} if this sort key should be evaluated in reverse
+     *            (descending) order.
+     * @param orderingMatchingRule
+     *            The name or OID of the ordering matching rule, which should be
+     *            used when comparing attributes using this sort key, or
+     *            {@code null} if the default ordering matching rule associated
+     *            with the attribute should be used.
+     * @throws NullPointerException
+     *             If {@code AttributeDescription} was {@code null}.
+     */
+    public SortKey(final AttributeDescription attributeDescription, final boolean isReverseOrder,
+            final MatchingRule orderingMatchingRule) {
+        Reject.ifNull(attributeDescription);
+        this.attributeDescription = attributeDescription.toString();
+        this.orderingMatchingRule =
+                orderingMatchingRule != null ? orderingMatchingRule.getNameOrOID() : null;
+        this.isReverseOrder = isReverseOrder;
+    }
+
+    /**
+     * Creates a new sort key using the provided attribute description. The
+     * returned sort key will compare attributes in ascending order using the
+     * default ordering matching rule associated with the attribute.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be sorted using this sort key.
+     * @throws NullPointerException
+     *             If {@code AttributeDescription} was {@code null}.
+     */
+    public SortKey(final String attributeDescription) {
+        this(attributeDescription, false, null);
+    }
+
+    /**
+     * Creates a new sort key using the provided attribute description. The
+     * returned sort key will compare attributes in the order specified using
+     * the default ordering matching rule associated with the attribute.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be sorted using this sort key.
+     * @param isReverseOrder
+     *            {@code true} if this sort key should be evaluated in reverse
+     *            (descending) order.
+     * @throws NullPointerException
+     *             If {@code AttributeDescription} was {@code null}.
+     */
+    public SortKey(final String attributeDescription, final boolean isReverseOrder) {
+        this(attributeDescription, isReverseOrder, null);
+    }
+
+    /**
+     * Creates a new sort key using the provided attribute description. The
+     * returned sort key will compare attributes in the order specified using
+     * the named ordering matching rule.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be sorted using this sort key.
+     * @param isReverseOrder
+     *            {@code true} if this sort key should be evaluated in reverse
+     *            (descending) order.
+     * @param orderingMatchingRule
+     *            The name or OID of the ordering matching rule, which should be
+     *            used when comparing attributes using this sort key, or
+     *            {@code null} if the default ordering matching rule associated
+     *            with the attribute should be used.
+     * @throws NullPointerException
+     *             If {@code AttributeDescription} was {@code null}.
+     */
+    public SortKey(final String attributeDescription, final boolean isReverseOrder,
+            final String orderingMatchingRule) {
+        Reject.ifNull(attributeDescription);
+        this.attributeDescription = attributeDescription;
+        this.orderingMatchingRule = orderingMatchingRule;
+        this.isReverseOrder = isReverseOrder;
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * this sort key. The attribute description and matching rule, if present,
+     * will be decoded using the default schema.
+     *
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If attributeDescription is not a valid LDAP string
+     *             representation of an attribute description, or if no ordering
+     *             matching rule was found.
+     */
+    public Comparator<Entry> comparator() {
+        return comparator(Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns a {@code Comparator} which can be used to compare entries using
+     * this sort key. The attribute description and matching rule, if present,
+     * will be decoded using the provided schema.
+     *
+     * @param schema
+     *            The schema which should be used for decoding the attribute
+     *            description and matching rule.
+     * @return The {@code Comparator}.
+     * @throws LocalizedIllegalArgumentException
+     *             If attributeDescription is not a valid LDAP string
+     *             representation of an attribute description, or if no ordering
+     *             matching rule was found.
+     * @throws NullPointerException
+     *             If {@code schema} was {@code null}.
+     */
+    public Comparator<Entry> comparator(final Schema schema) {
+        Reject.ifNull(schema);
+
+        final AttributeDescription ad = AttributeDescription.valueOf(attributeDescription, schema);
+
+        MatchingRule mrule;
+        if (orderingMatchingRule != null) {
+            // FIXME: need to check that the matching rule is a matching rule
+            // and can
+            // be used with the attribute.
+            mrule = schema.getMatchingRule(orderingMatchingRule);
+
+            if (mrule == null) {
+                // Specified ordering matching rule not found.
+                final LocalizableMessage message =
+                        ERR_SORT_KEY_MRULE_NOT_FOUND.get(toString(), orderingMatchingRule);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        } else {
+            mrule = ad.getAttributeType().getOrderingMatchingRule();
+
+            if (mrule == null) {
+                // No default ordering matching rule found.
+                final LocalizableMessage message =
+                        ERR_SORT_KEY_DEFAULT_MRULE_NOT_FOUND.get(toString(), attributeDescription);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        return new EntryComparator(ad, mrule, isReverseOrder);
+    }
+
+    /**
+     * Returns the name of the attribute to be sorted using this sort key.
+     *
+     * @return The name of the attribute to be sorted using this sort key.
+     */
+    public String getAttributeDescription() {
+        return attributeDescription;
+    }
+
+    /**
+     * Returns the name or OID of the ordering matching rule, if specified,
+     * which should be used when comparing attributes using this sort key.
+     *
+     * @return The name or OID of the ordering matching rule, if specified,
+     *         which should be used when comparing attributes using this sort
+     *         key, or {@code null} if the default ordering matching rule
+     *         associated with the attribute should be used.
+     */
+    public String getOrderingMatchingRule() {
+        return orderingMatchingRule;
+    }
+
+    /**
+     * Returns {@code true} if this sort key should be evaluated in reverse
+     * (descending) order. More specifically, comparisons performed using the
+     * ordering matching rule associated with this sort key will have their
+     * results inverted.
+     *
+     * @return {@code true} if this sort key should be evaluated in reverse
+     *         (descending) order.
+     */
+    public boolean isReverseOrder() {
+        return isReverseOrder;
+    }
+
+    /**
+     * Returns a string representation of this sort key using the format defined
+     * in {@link #valueOf(String)}.
+     *
+     * @return A string representation of this sort key.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        if (isReverseOrder) {
+            builder.append('-');
+        }
+        builder.append(attributeDescription);
+        if (orderingMatchingRule != null) {
+            builder.append(':');
+            builder.append(orderingMatchingRule);
+        }
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutChecker.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutChecker.java
new file mode 100644
index 0000000..5f84918
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutChecker.java
@@ -0,0 +1,170 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.util.Collections.newSetFromMap;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * Checks {@code TimeoutEventListener listeners} for events that have timed out.
+ * <p>
+ * All listeners registered with the {@code #addListener()} method are called
+ * back with {@code TimeoutEventListener#handleTimeout()} to be able to handle
+ * the timeout.
+ */
+public final class TimeoutChecker {
+    /**
+     * Global reference on the timeout checker.
+     */
+    public static final ReferenceCountedObject<TimeoutChecker> TIMEOUT_CHECKER =
+            new ReferenceCountedObject<TimeoutChecker>() {
+                @Override
+                protected void destroyInstance(final TimeoutChecker instance) {
+                    instance.shutdown();
+                }
+
+                @Override
+                protected TimeoutChecker newInstance() {
+                    return new TimeoutChecker();
+                }
+            };
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * Condition variable used for coordinating the timeout thread.
+     */
+    private final Object stateLock = new Object();
+
+    /**
+     * The listener set must be safe from CMEs. For example, if the listener is
+     * a connection, expiring requests can cause the connection to be closed.
+     */
+    private final Set<TimeoutEventListener> listeners =
+            newSetFromMap(new ConcurrentHashMap<TimeoutEventListener, Boolean>());
+
+    /**
+     * Used to signal thread shutdown.
+     */
+    private volatile boolean shutdownRequested;
+
+    /**
+     * Contains the minimum delay for listeners which were added while the
+     * timeout check was not sleeping (i.e. while it was processing listeners).
+     */
+    private volatile long pendingListenerMinDelay = Long.MAX_VALUE;
+
+    private TimeoutChecker() {
+        final Thread checkerThread = new Thread("OpenDJ LDAP SDK Timeout Checker") {
+            @Override
+            public void run() {
+                logger.debug(LocalizableMessage.raw("Timeout Checker Starting"));
+                while (!shutdownRequested) {
+                    /*
+                     * New listeners may be added during iteration and may not
+                     * be included in the computation of the new delay. This
+                     * could potentially result in the timeout checker waiting
+                     * longer than it should, or even forever (e.g. if the new
+                     * listener is the first).
+                     */
+                    final long currentTime = System.currentTimeMillis();
+                    long delay = Long.MAX_VALUE;
+                    pendingListenerMinDelay = Long.MAX_VALUE;
+                    for (final TimeoutEventListener listener : listeners) {
+                        logger.trace(LocalizableMessage.raw("Checking connection %s delay = %d", listener, delay));
+
+                        // May update the connections set.
+                        final long newDelay = listener.handleTimeout(currentTime);
+                        if (newDelay > 0) {
+                            delay = Math.min(newDelay, delay);
+                        }
+                    }
+
+                    try {
+                        synchronized (stateLock) {
+                            // Include any pending listener delays.
+                            delay = Math.min(pendingListenerMinDelay, delay);
+                            if (shutdownRequested) {
+                                // Stop immediately.
+                                break;
+                            } else if (delay <= 0) {
+                                /*
+                                 * If there is at least one connection then the
+                                 * delay should be > 0.
+                                 */
+                                stateLock.wait();
+                            } else {
+                                stateLock.wait(delay);
+                            }
+                        }
+                    } catch (final InterruptedException e) {
+                        shutdownRequested = true;
+                    }
+                }
+            }
+        };
+
+        checkerThread.setDaemon(true);
+        checkerThread.start();
+    }
+
+    /**
+     * Registers a timeout event listener for timeout notification.
+     *
+     * @param listener
+     *            The timeout event listener.
+     */
+    public void addListener(final TimeoutEventListener listener) {
+        /*
+         * Only add the listener if it has a non-zero timeout. This assumes that
+         * the timeout is fixed.
+         */
+        final long timeout = listener.getTimeout();
+        if (timeout > 0) {
+            listeners.add(listener);
+            synchronized (stateLock) {
+                pendingListenerMinDelay = Math.min(pendingListenerMinDelay, timeout);
+                stateLock.notifyAll();
+            }
+        }
+    }
+
+    /**
+     * Deregisters a timeout event listener for timeout notification.
+     *
+     * @param listener
+     *            The timeout event listener.
+     */
+    public void removeListener(final TimeoutEventListener listener) {
+        listeners.remove(listener);
+        // No need to signal.
+    }
+
+    private void shutdown() {
+        synchronized (stateLock) {
+            shutdownRequested = true;
+            stateLock.notifyAll();
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutEventListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutEventListener.java
new file mode 100644
index 0000000..5832dcf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutEventListener.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+/**
+ * Listener on timeout events.
+ * <p>
+ * The listener must register itself with a {@code TimeoutChecker} using
+ * {@code TimeoutChecker#addListener()} method to be called back with
+ * {@code #handleTimeout} method.
+ * <p>
+ * The listener must deregister itself using
+ * {@code TimeoutChecker#removeListener()} to stop being called back.
+ */
+public interface TimeoutEventListener {
+
+    /**
+     * Handle a timeout event.
+     *
+     * @param currentTime
+     *            Time to use as current time for any check.
+     * @return The delay to wait before next timeout callback in milliseconds,
+     *         or zero if this listener should no longer be notified.
+     */
+    long handleTimeout(long currentTime);
+
+    /**
+     * Returns the timeout for this listener.
+     *
+     * @return The timeout in milliseconds.
+     */
+    long getTimeout();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutResultException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutResultException.java
new file mode 100644
index 0000000..9163da7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TimeoutResultException.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.responses.Result;
+
+/**
+ * Thrown when the result code returned in a Result indicates that the Request
+ * was aborted because it did not complete in the required time out period.
+ */
+@SuppressWarnings("serial")
+public class TimeoutResultException extends LdapException {
+    TimeoutResultException(final Result result) {
+        super(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TreeMapEntry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TreeMapEntry.java
new file mode 100644
index 0000000..658eecd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TreeMapEntry.java
@@ -0,0 +1,166 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.TreeMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.requests.Requests;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An implementation of the {@code Entry} interface which uses a {@code TreeMap}
+ * for storing attributes. Attributes are returned in ascending order of
+ * attribute description, with {@code objectClass} first, then all user
+ * attributes, and finally any operational attributes. All operations are
+ * supported by this implementation. For example, you can build an entry like
+ * this:
+ *
+ * <pre>
+ * Entry entry = new TreeMapEntry("cn=Bob,ou=People,dc=example,dc=com")
+ *    .addAttribute("cn", "Bob")
+ *    .addAttribute("objectclass", "top")
+ *    .addAttribute("objectclass", "person")
+ *    .addAttribute("objectclass", "organizationalPerson")
+ *    .addAttribute("objectclass", "inetOrgPerson")
+ *    .addAttribute("mail", "subgenius@example.com")
+ *    .addAttribute("sn", "Dobbs");
+ * </pre>
+ *
+ * <p>
+ * A {@code TreeMapEntry} stores references to attributes which have been added
+ * using the {@link #addAttribute} methods. Attributes sharing the same
+ * attribute description are merged by adding the values of the new attribute to
+ * the existing attribute. More specifically, the existing attribute must be
+ * modifiable for the merge to succeed. Similarly, the {@link #removeAttribute}
+ * methods remove the specified values from the existing attribute. The
+ * {@link #replaceAttribute} methods remove the existing attribute (if present)
+ * and store a reference to the new attribute - neither the new or existing
+ * attribute need to be modifiable in this case.
+ */
+public final class TreeMapEntry extends AbstractMapEntry {
+    /**
+     * An entry factory which can be used to create new tree map entries.
+     */
+    public static final EntryFactory FACTORY = new EntryFactory() {
+        @Override
+        public Entry newEntry(final DN name) {
+            return new TreeMapEntry(name);
+        }
+    };
+
+    /**
+     * Creates an entry having the same distinguished name, attributes, and
+     * object classes of the provided entry. This constructor performs a deep
+     * copy of {@code entry} and will copy each attribute as a
+     * {@link LinkedAttribute}.
+     * <p>
+     * A shallow copy constructor is provided by {@link #TreeMapEntry(Entry)}.
+     *
+     * @param entry
+     *            The entry to be copied.
+     * @return A deep copy of {@code entry}.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     * @see #TreeMapEntry(Entry)
+     */
+    public static TreeMapEntry deepCopyOfEntry(final Entry entry) {
+        TreeMapEntry copy = new TreeMapEntry(entry.getName());
+        for (final Attribute attribute : entry.getAllAttributes()) {
+            copy.addAttribute(new LinkedAttribute(attribute));
+        }
+        return copy;
+    }
+
+    /**
+     * Creates an entry with an empty (root) distinguished name and no
+     * attributes.
+     */
+    public TreeMapEntry() {
+        this(DN.rootDN());
+    }
+
+    /**
+     * Creates an empty entry using the provided distinguished name and no
+     * attributes.
+     *
+     * @param name
+     *            The distinguished name of this entry.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public TreeMapEntry(final DN name) {
+        super(Reject.checkNotNull(name), new TreeMap<AttributeDescription, Attribute>());
+    }
+
+    /**
+     * Creates an entry having the same distinguished name, attributes, and
+     * object classes of the provided entry. This constructor performs a shallow
+     * copy of {@code entry} and will not copy the attributes contained in
+     * {@code entry}.
+     * <p>
+     * A deep copy constructor is provided by {@link #deepCopyOfEntry(Entry)}
+     *
+     * @param entry
+     *            The entry to be copied.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     * @see #deepCopyOfEntry(Entry)
+     */
+    public TreeMapEntry(final Entry entry) {
+        this(entry.getName());
+        for (final Attribute attribute : entry.getAllAttributes()) {
+            addAttribute(attribute);
+        }
+    }
+
+    /**
+     * Creates an empty entry using the provided distinguished name decoded
+     * using the default schema.
+     *
+     * @param name
+     *            The distinguished name of this entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public TreeMapEntry(final String name) {
+        this(DN.valueOf(name));
+    }
+
+    /**
+     * Creates a new entry using the provided lines of LDIF decoded using the
+     * default schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing the an LDIF add change record or an
+     *            LDIF entry record.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public TreeMapEntry(final String... ldifLines) {
+        this(Requests.newAddRequest(ldifLines));
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TrustManagers.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TrustManagers.java
new file mode 100644
index 0000000..91614ea
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/TrustManagers.java
@@ -0,0 +1,373 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/** This class contains methods for creating common types of trust manager. */
+public final class TrustManagers {
+
+    /**
+     * An X509TrustManager which rejects certificate chains whose subject DN
+     * does not match a specified host name.
+     */
+    private static final class CheckHostName implements X509TrustManager {
+
+        private final X509TrustManager trustManager;
+
+        private final String hostNamePattern;
+
+        private CheckHostName(final X509TrustManager trustManager, final String hostNamePattern) {
+            this.trustManager = trustManager;
+            this.hostNamePattern = hostNamePattern;
+        }
+
+        @Override
+        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            verifyHostName(chain);
+            trustManager.checkClientTrusted(chain, authType);
+        }
+
+        @Override
+        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            verifyHostName(chain);
+            trustManager.checkServerTrusted(chain, authType);
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return trustManager.getAcceptedIssuers();
+        }
+
+        /**
+         * Checks whether a host name matches the provided pattern. It accepts
+         * the use of wildcards in the pattern, e.g. {@code *.example.com}.
+         *
+         * @param hostName
+         *            The host name.
+         * @param pattern
+         *            The host name pattern, which may contain wild cards.
+         * @return {@code true} if the host name matched the pattern, otherwise
+         *         {@code false}.
+         */
+        private boolean hostNameMatchesPattern(final String hostName, final String pattern) {
+            final String[] nameElements = hostName.split("\\.");
+            final String[] patternElements = pattern.split("\\.");
+
+            boolean hostMatch = nameElements.length == patternElements.length;
+            for (int i = 0; i < nameElements.length && hostMatch; i++) {
+                final String ne = nameElements[i];
+                final String pe = patternElements[i];
+                if (!pe.equals("*")) {
+                    hostMatch = ne.equalsIgnoreCase(pe);
+                }
+            }
+            return hostMatch;
+        }
+
+        private void verifyHostName(final X509Certificate[] chain) {
+            try {
+                // TODO: NPE if root DN.
+                final DN dn =
+                        DN.valueOf(chain[0].getSubjectX500Principal().getName(), Schema
+                                .getCoreSchema());
+                final String value =
+                        dn.iterator().next().iterator().next().getAttributeValue().toString();
+                if (!hostNameMatchesPattern(value, hostNamePattern)) {
+                    throw new CertificateException(
+                            "The host name contained in the certificate chain subject DN \'"
+                                    + chain[0].getSubjectX500Principal()
+                                    + "' does not match the host name \'" + hostNamePattern + "'");
+                }
+            } catch (final Throwable t) {
+                LOG.log(Level.WARNING, "Error parsing subject dn: "
+                        + chain[0].getSubjectX500Principal(), t);
+            }
+        }
+    }
+
+    /** An X509TrustManager which rejects certificates which have expired or are not yet valid. */
+    private static final class CheckValidityDates implements X509TrustManager {
+
+        private final X509TrustManager trustManager;
+
+        private CheckValidityDates(final X509TrustManager trustManager) {
+            this.trustManager = trustManager;
+        }
+
+        @Override
+        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            verifyExpiration(chain);
+            trustManager.checkClientTrusted(chain, authType);
+        }
+
+        @Override
+        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            verifyExpiration(chain);
+            trustManager.checkServerTrusted(chain, authType);
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return trustManager.getAcceptedIssuers();
+        }
+
+        private void verifyExpiration(final X509Certificate[] chain) throws CertificateException {
+            final Date currentDate = new Date();
+            for (final X509Certificate c : chain) {
+                try {
+                    c.checkValidity(currentDate);
+                } catch (final CertificateExpiredException e) {
+                    LOG.log(Level.WARNING, "Refusing to trust security" + " certificate \""
+                            + c.getSubjectDN().getName() + "\" because it" + " expired on "
+                            + String.valueOf(c.getNotAfter()));
+
+                    throw e;
+                } catch (final CertificateNotYetValidException e) {
+                    LOG.log(Level.WARNING, "Refusing to trust security" + " certificate \""
+                            + c.getSubjectDN().getName() + "\" because it" + " is not valid until "
+                            + String.valueOf(c.getNotBefore()));
+
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /** An X509TrustManager which does not trust any certificates. */
+    private static final class DistrustAll implements X509TrustManager {
+        /** Single instance. */
+        private static final DistrustAll INSTANCE = new DistrustAll();
+
+        /** Prevent instantiation. */
+        private DistrustAll() {
+            // Nothing to do.
+        }
+
+        @Override
+        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            throw new CertificateException();
+        }
+
+        @Override
+        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+            throw new CertificateException();
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+
+    /** An X509TrustManager which trusts all certificates. */
+    private static final class TrustAll implements X509TrustManager {
+
+        /** Single instance. */
+        private static final TrustAll INSTANCE = new TrustAll();
+
+        /** Prevent instantiation. */
+        private TrustAll() {
+            // Nothing to do.
+        }
+
+        @Override
+        public void checkClientTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+        }
+
+        @Override
+        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
+                throws CertificateException {
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+
+    private static final Logger LOG = Logger.getLogger(TrustManagers.class.getName());
+
+    /**
+     * Wraps the provided {@code X509TrustManager} by adding additional
+     * validation which rejects certificate chains whose subject DN does not
+     * match the specified host name pattern. The pattern may contain
+     * wild-cards, for example {@code *.example.com}.
+     *
+     * @param hostNamePattern
+     *            A host name pattern which the RDN value contained in
+     *            certificate subject DNs must match.
+     * @param trustManager
+     *            The trust manager to be wrapped.
+     * @return The wrapped trust manager.
+     * @throws NullPointerException
+     *             If {@code trustManager} or {@code hostNamePattern} was
+     *             {@code null}.
+     */
+    public static X509TrustManager checkHostName(final String hostNamePattern,
+            final X509TrustManager trustManager) {
+        Reject.ifNull(trustManager, hostNamePattern);
+        return new CheckHostName(trustManager, hostNamePattern);
+    }
+
+    /**
+     * Creates a new {@code X509TrustManager} which will use the named trust
+     * store file to determine whether to trust a certificate. It will use the
+     * default trust store format for the JVM (e.g. {@code JKS}) and will not
+     * use a password to open the trust store.
+     *
+     * @param file
+     *            The trust store file name.
+     * @return A new {@code X509TrustManager} which will use the named trust
+     *         store file to determine whether to trust a certificate.
+     * @throws GeneralSecurityException
+     *             If the trust store could not be loaded, perhaps due to
+     *             incorrect format, or missing algorithms.
+     * @throws IOException
+     *             If the trust store file could not be found or could not be
+     *             read.
+     * @throws NullPointerException
+     *             If {@code file} was {@code null}.
+     */
+    public static X509TrustManager checkUsingTrustStore(final String file)
+            throws GeneralSecurityException, IOException {
+        return checkUsingTrustStore(file, null, null);
+    }
+
+    /**
+     * Creates a new {@code X509TrustManager} which will use the named trust
+     * store file to determine whether to trust a certificate. It will use the
+     * provided trust store format and password.
+     *
+     * @param file
+     *            The trust store file name.
+     * @param password
+     *            The trust store password, which may be {@code null}.
+     * @param format
+     *            The trust store format, which may be {@code null} to indicate
+     *            that the default trust store format for the JVM (e.g.
+     *            {@code JKS}) should be used.
+     * @return A new {@code X509TrustManager} which will use the named trust
+     *         store file to determine whether to trust a certificate.
+     * @throws GeneralSecurityException
+     *             If the trust store could not be loaded, perhaps due to
+     *             incorrect format, or missing algorithms.
+     * @throws IOException
+     *             If the trust store file could not be found or could not be
+     *             read.
+     * @throws NullPointerException
+     *             If {@code file} was {@code null}.
+     */
+    public static X509TrustManager checkUsingTrustStore(final String file, final char[] password,
+            final String format) throws GeneralSecurityException, IOException {
+        Reject.ifNull(file);
+
+        final File trustStoreFile = new File(file);
+        final String trustStoreFormat = format != null ? format : KeyStore.getDefaultType();
+
+        final KeyStore keyStore = KeyStore.getInstance(trustStoreFormat);
+        try (FileInputStream fos = new FileInputStream(trustStoreFile)) {
+            keyStore.load(fos, password);
+        }
+
+        final TrustManagerFactory tmf =
+                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        tmf.init(keyStore);
+
+        X509TrustManager x509tm = null;
+        for (final TrustManager tm : tmf.getTrustManagers()) {
+            if (tm instanceof X509TrustManager) {
+                x509tm = (X509TrustManager) tm;
+                break;
+            }
+        }
+
+        if (x509tm == null) {
+            throw new NoSuchAlgorithmException();
+        }
+
+        return x509tm;
+    }
+
+    /**
+     * Wraps the provided {@code X509TrustManager} by adding additional
+     * validation which rejects certificate chains containing certificates which
+     * have expired or are not yet valid.
+     *
+     * @param trustManager
+     *            The trust manager to be wrapped.
+     * @return The wrapped trust manager.
+     * @throws NullPointerException
+     *             If {@code trustManager} was {@code null}.
+     */
+    public static X509TrustManager checkValidityDates(final X509TrustManager trustManager) {
+        Reject.ifNull(trustManager);
+        return new CheckValidityDates(trustManager);
+    }
+
+    /**
+     * Returns an {@code X509TrustManager} which does not trust any
+     * certificates.
+     *
+     * @return An {@code X509TrustManager} which does not trust any
+     *         certificates.
+     */
+    public static X509TrustManager distrustAll() {
+        return DistrustAll.INSTANCE;
+    }
+
+    /**
+     * Returns an {@code X509TrustManager} which trusts all certificates.
+     *
+     * @return An {@code X509TrustManager} which trusts all certificates.
+     */
+    public static X509TrustManager trustAll() {
+        return TrustAll.INSTANCE;
+    }
+
+    /** Prevent insantiation. */
+    private TrustManagers() {
+        // Nothing to do.
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java
new file mode 100644
index 0000000..c2c6ea3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java
@@ -0,0 +1,158 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * The persistent search request control for Active Directory as defined by
+ * Microsoft. This control allows a client to receive notification of changes
+ * that occur in an Active Directory server.
+ * <br/>
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request =
+ *         Requests.newSearchRequest("dc=example,dc=com",
+ *                 SearchScope.WHOLE_SUBTREE, "(objectclass=*)", "cn",
+ *                 "isDeleted", "whenChanged", "whenCreated").addControl(
+ *                 ADNotificationRequestControl.newControl(true));
+ *
+ * ConnectionEntryReader reader = connection.search(request);
+ *
+ * while (reader.hasNext()) {
+ *     if (!reader.isReference()) {
+ *         SearchResultEntry entry = reader.readEntry(); // Entry that changed
+ *
+ *         Boolean isDeleted = entry.parseAttribute("isDeleted").asBoolean();
+ *         if (isDeleted != null && isDeleted) {
+ *             // Handle entry deletion
+ *         }
+ *         String whenCreated = entry.parseAttribute("whenCreated").asString();
+ *         String whenChanged = entry.parseAttribute("whenChanged").asString();
+ *         if (whenCreated != null && whenChanged != null) {
+ *             if (whenCreated.equals(whenChanged)) {
+ *                 //Handle entry addition
+ *             } else {
+ *                 //Handle entry modification
+ *             }
+ *         }
+ *     } else {
+ *         reader.readReference(); //read and ignore reference
+ *     }
+ * }
+ *
+ * </pre>
+ *
+ * @see <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa772153(v=vs.85).aspx">
+ *      Change Notifications in Active Directory Domain Services</a>
+ */
+public final class ADNotificationRequestControl implements Control {
+
+    /**
+     * The OID for the Microsoft Active Directory persistent search request
+     * control. The control itself is empty and the changes are returned as
+     * attributes, such as "isDeleted", "whenChanged", "whenCreated".
+     */
+    public static final String OID = "1.2.840.113556.1.4.528";
+
+    /**
+     * The name of the isDeleted attribute as defined in the Active Directory
+     * schema. If the value of the attribute is <code>TRUE</code>, the object
+     * has been marked for deletion.
+     */
+    public static final String IS_DELETED_ATTR = "isDeleted";
+
+    /**
+     * The name of the whenCreated attribute as defined in the Active Directory
+     * schema. Holds the date of the creation of the object in GeneralizedTime
+     * format.
+     */
+    public static final String WHEN_CREATED_ATTR = "whenCreated";
+
+    /**
+     * The name of the whenChanged attribute as defined in the Active Directory
+     * schema. Holds the date of the last modification of the object in
+     * GeneralizedTime format.
+     */
+    public static final String WHEN_CHANGED_ATTR = "whenChanged";
+
+    /**
+     * The name of the objectGUID attribute as defined in the Active Directory
+     * schema. This is the unique identifier of an object stored in binary
+     * format.
+     */
+    public static final String OBJECT_GUID_ATTR = "objectGUID";
+
+    /**
+     * The name of the uSNChanged attribute as defined in the Active Directory
+     * schema. This attribute can be used to determine whether the current
+     * state of the object on the server reflects the latest changes that the
+     * client has received.
+     */
+    public static final String USN_CHANGED_ATTR = "uSNChanged";
+
+    private final boolean isCritical;
+
+    private ADNotificationRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    /**
+     * Creates a new Active Directory change notification request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @return The new control.
+     */
+    public static ADNotificationRequestControl newControl(final boolean isCritical) {
+        return new ADNotificationRequestControl(isCritical);
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ADNotificationRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AssertionRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AssertionRequestControl.java
new file mode 100644
index 0000000..18ccff8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AssertionRequestControl.java
@@ -0,0 +1,193 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDAPASSERT_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDAPASSERT_INVALID_CONTROL_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDAPASSERT_NO_CONTROL_VALUE;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Filter;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The assertion request control as defined in RFC 4528. The Assertion control
+ * allows a client to specify that a directory operation should only be
+ * processed if an assertion applied to the target entry of the operation is
+ * true. It can be used to construct "test and set", "test and clear", and other
+ * conditional operations.
+ * <p>
+ * The following excerpt shows how to check that no description exists on an
+ * entry before adding a description.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * connection.bind(...);
+ *
+ * String entryDN = ...;
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(entryDN)
+ *             .addControl(AssertionRequestControl.newControl(
+ *                     true, Filter.valueOf("!(description=*)")))
+ *             .addModification(ModificationType.ADD, "description",
+ *                     "Created using LDAP assertion control");
+ *
+ * connection.modify(request);
+ * ...
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4528">RFC 4528 - Lightweight
+ *      Directory Access Protocol (LDAP) Assertion Control </a>
+ */
+public final class AssertionRequestControl implements Control {
+    /** The IANA-assigned OID for the LDAP assertion request control. */
+    public static final String OID = "1.3.6.1.1.12";
+
+    /** A decoder which can be used for decoding the LDAP assertion request control. */
+    public static final ControlDecoder<AssertionRequestControl> DECODER =
+            new ControlDecoder<AssertionRequestControl>() {
+
+                @Override
+                public AssertionRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof AssertionRequestControl) {
+                        return (AssertionRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_LDAPASSERT_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_LDAPASSERT_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    try {
+                        final ASN1Reader reader = ASN1.getReader(control.getValue());
+                        final Filter filter = LDAP.readFilter(reader);
+                        return new AssertionRequestControl(control.isCritical(), filter);
+                    } catch (final IOException e) {
+                        throw DecodeException.error(ERR_LDAPASSERT_INVALID_CONTROL_VALUE
+                                .get(getExceptionMessage(e)), e);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new assertion using the provided criticality and assertion
+     * filter.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param filter
+     *            The assertion filter.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code filter} was {@code null}.
+     */
+    public static AssertionRequestControl newControl(final boolean isCritical, final Filter filter) {
+        return new AssertionRequestControl(isCritical, filter);
+    }
+
+    /** The assertion filter. */
+    private final Filter filter;
+
+    private final boolean isCritical;
+
+    /** Prevent direct instantiation. */
+    private AssertionRequestControl(final boolean isCritical, final Filter filter) {
+        Reject.ifNull(filter);
+        this.isCritical = isCritical;
+        this.filter = filter;
+    }
+
+    /**
+     * Returns the assertion filter.
+     *
+     * @return The assertion filter.
+     */
+    public Filter getFilter() {
+        return filter;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            LDAP.writeFilter(writer, filter);
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AssertionRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", filter=\"");
+        builder.append(filter);
+        builder.append("\")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityRequestControl.java
new file mode 100644
index 0000000..8dea2a9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityRequestControl.java
@@ -0,0 +1,158 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_AUTHZIDREQ_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_AUTHZIDREQ_CONTROL_HAS_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The authorization request control as defined in RFC 3829. The authorization
+ * identity control extends the Lightweight Directory Access Protocol (LDAP)
+ * bind operation with a mechanism for requesting and returning the
+ * authorization identity it establishes.
+ * <p>
+ * The following excerpt shows how to get the authorization identity established
+ * when binding to the directory server.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String bindDN = ...;
+ * String bindPassword = ...;
+ *
+ * BindRequest request =
+ *         Requests.newSimpleBindRequest(bindDN, bindPassword.toCharArray())
+ *             .addControl(AuthorizationIdentityRequestControl
+ *                     .newControl(true));
+ *
+ * BindResult result = connection.bind(request);
+ * AuthorizationIdentityResponseControl control =
+ *         result.getControl(AuthorizationIdentityResponseControl.DECODER,
+ *                 new DecodeOptions());
+ * // Authorization ID returned: control.getAuthorizationID()
+ * </pre>
+ *
+ * @see AuthorizationIdentityResponseControl
+ * @see org.forgerock.opendj.ldap.requests.WhoAmIExtendedRequest
+ * @see <a href="http://tools.ietf.org/html/rfc3829">RFC 3829 - Lightweight
+ *      Directory Access Protocol (LDAP) Authorization Identity Request and
+ *      Response Controls </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4532">RFC 4532 - Lightweight
+ *      Directory Access Protocol (LDAP) "Who am I?" Operation </a>
+ */
+public final class AuthorizationIdentityRequestControl implements Control {
+    /** The OID for the authorization identity request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.16";
+
+    private final boolean isCritical;
+
+    private static final AuthorizationIdentityRequestControl CRITICAL_INSTANCE =
+            new AuthorizationIdentityRequestControl(true);
+
+    private static final AuthorizationIdentityRequestControl NONCRITICAL_INSTANCE =
+            new AuthorizationIdentityRequestControl(false);
+
+    /** A decoder which can be used for decoding the authorization identity request control. */
+    public static final ControlDecoder<AuthorizationIdentityRequestControl> DECODER =
+            new ControlDecoder<AuthorizationIdentityRequestControl>() {
+
+                @Override
+                public AuthorizationIdentityRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof AuthorizationIdentityRequestControl) {
+                        return (AuthorizationIdentityRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_AUTHZIDREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message = ERR_AUTHZIDREQ_CONTROL_HAS_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new authorization identity request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static AuthorizationIdentityRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    /** Prevent direct instantiation. */
+    private AuthorizationIdentityRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AuthorizationIdentityRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityResponseControl.java
new file mode 100644
index 0000000..5518a3f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/AuthorizationIdentityResponseControl.java
@@ -0,0 +1,180 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_AUTHZIDRESP_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_AUTHZIDRESP_NO_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The authorization response control as defined in RFC 3829. The authorization
+ * identity control extends the Lightweight Directory Access Protocol (LDAP)
+ * bind operation with a mechanism for requesting and returning the
+ * authorization identity it establishes.
+ * <p>
+ * The authorization identity is specified using an authorization ID, or
+ * {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ * <p>
+ * The following excerpt shows how to get the authorization identity established
+ * when binding to the directory server.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String bindDN = ...;
+ * String bindPassword = ...;
+ *
+ * BindRequest request =
+ *         Requests.newSimpleBindRequest(bindDN, bindPassword.toCharArray())
+ *             .addControl(AuthorizationIdentityRequestControl
+ *                     .newControl(true));
+ *
+ * BindResult result = connection.bind(request);
+ * AuthorizationIdentityResponseControl control =
+ *         result.getControl(AuthorizationIdentityResponseControl.DECODER,
+ *                 new DecodeOptions());
+ * // Authorization ID returned: control.getAuthorizationID()
+ * </pre>
+ *
+ * @see AuthorizationIdentityRequestControl
+ * @see org.forgerock.opendj.ldap.requests.WhoAmIExtendedRequest
+ * @see <a href="http://tools.ietf.org/html/rfc3829">RFC 3829 - Lightweight
+ *      Directory Access Protocol (LDAP) Authorization Identity Request and
+ *      Response Controls </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4532">RFC 4532 - Lightweight
+ *      Directory Access Protocol (LDAP) "Who am I?" Operation </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public final class AuthorizationIdentityResponseControl implements Control {
+
+    /** The OID for the authorization identity response control. */
+    public static final String OID = "2.16.840.1.113730.3.4.15";
+
+    /**
+     * Creates a new authorization identity response control using the provided
+     * authorization ID.
+     *
+     * @param authorizationID
+     *            The authorization ID for this control.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code authorizationID} was {@code null}.
+     */
+    public static AuthorizationIdentityResponseControl newControl(final String authorizationID) {
+        return new AuthorizationIdentityResponseControl(false, authorizationID);
+    }
+
+    /** The authorization ID for this control. */
+    private final String authorizationID;
+
+    private final boolean isCritical;
+
+    /** A decoder which can be used for decoding the authorization identity response control. */
+    public static final ControlDecoder<AuthorizationIdentityResponseControl> DECODER =
+            new ControlDecoder<AuthorizationIdentityResponseControl>() {
+
+                @Override
+                public AuthorizationIdentityResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof AuthorizationIdentityResponseControl) {
+                        return (AuthorizationIdentityResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_AUTHZIDRESP_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_AUTHZIDRESP_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final String authID = control.getValue().toString();
+                    return new AuthorizationIdentityResponseControl(control.isCritical(), authID);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /** Prevent direct instantiation. */
+    private AuthorizationIdentityResponseControl(final boolean isCritical,
+            final String authorizationID) {
+        Reject.ifNull(authorizationID);
+        this.isCritical = isCritical;
+        this.authorizationID = authorizationID;
+    }
+
+    /**
+     * Returns the authorization ID of the user. The authorization ID usually
+     * has the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authorization ID of the user.
+     */
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return ByteString.valueOfUtf8(authorizationID);
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AuthorizationIdentityResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", authzID=\"");
+        builder.append(authorizationID);
+        builder.append("\")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/Control.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/Control.java
new file mode 100644
index 0000000..ae1c613
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/Control.java
@@ -0,0 +1,90 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * Controls provide a mechanism whereby the semantics and arguments of existing
+ * LDAP operations may be extended. One or more controls may be attached to a
+ * single LDAP message. A control only affects the semantics of the message it
+ * is attached to. Controls sent by clients are termed 'request controls', and
+ * those sent by servers are termed 'response controls'.
+ * <p>
+ * To determine whether a directory server supports a given control, read the
+ * list of supported controls from the root DSE to get a collection of control
+ * OIDs, and then check for a match:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * Collection&lt;String&gt; supported =
+ *     RootDSE.readRootDSE(connection).getSupportedControls();
+ *
+ * Control control = ...;
+ * String OID = control.getOID();
+ * if (supported != null && !supported.isEmpty() && supported.contains(OID)) {
+ *     // The control is supported. Use it here...
+ * }
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
+ *      Directory Access Protocol (LDAP): The Protocol </a>
+ */
+public interface Control {
+
+    /**
+     * Returns the numeric OID associated with this control.
+     *
+     * @return The numeric OID associated with this control.
+     */
+    String getOID();
+
+    /**
+     * Returns the value, if any, associated with this control. Its format is
+     * defined by the specification of this control.
+     *
+     * @return The value associated with this control, or {@code null} if there
+     *         is no value.
+     */
+    ByteString getValue();
+
+    /**
+     * Returns {@code true} if this control has a value. In some circumstances
+     * it may be useful to determine if a control has a value, without actually
+     * calculating the value and incurring any performance costs.
+     *
+     * @return {@code true} if this control has a value, or {@code false} if
+     *         there is no value.
+     */
+    boolean hasValue();
+
+    /**
+     * Returns {@code true} if it is unacceptable to perform the operation
+     * without applying the semantics of this control.
+     * <p>
+     * The criticality field only has meaning in controls attached to request
+     * messages (except UnbindRequest). For controls attached to response
+     * messages and the UnbindRequest, the criticality field SHOULD be
+     * {@code false}, and MUST be ignored by the receiving protocol peer. A
+     * value of {@code true} indicates that it is unacceptable to perform the
+     * operation without applying the semantics of the control.
+     *
+     * @return {@code true} if this control must be processed by the Directory
+     *         Server, or {@code false} if it can be ignored.
+     */
+    boolean isCritical();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ControlDecoder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ControlDecoder.java
new file mode 100644
index 0000000..692330a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ControlDecoder.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.controls;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+/**
+ * A factory interface for decoding a control as a control of specific type.
+ *
+ * @param <C>
+ *            The type of control decoded by this control decoder.
+ */
+public interface ControlDecoder<C extends Control> {
+    /**
+     * Decodes the provided control as a {@code Control} of type {@code C}.
+     *
+     * @param control
+     *            The control to be decoded.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the control.
+     * @return The decoded control.
+     * @throws DecodeException
+     *             If the control contained the wrong OID, it did not have a
+     *             value, or if its value could not be decoded.
+     */
+    C decodeControl(Control control, DecodeOptions options) throws DecodeException;
+
+    /**
+     * Returns the numeric OID associated with this control decoder.
+     *
+     * @return The numeric OID associated with this control decoder.
+     */
+    String getOID();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/EntryChangeNotificationResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/EntryChangeNotificationResponseControl.java
new file mode 100644
index 0000000..f9d45e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/EntryChangeNotificationResponseControl.java
@@ -0,0 +1,344 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/**
+ * The entry change notification response control as defined in
+ * draft-ietf-ldapext-psearch. This control provides additional information
+ * about the change that caused a particular entry to be returned as the result
+ * of a persistent search.
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request =
+ *         Requests.newSearchRequest(
+ *                 "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+ *                 "(objectclass=inetOrgPerson)", "cn")
+ *                 .addControl(PersistentSearchRequestControl.newControl(
+ *                             true, true, true, // critical,changesOnly,returnECs
+ *                             PersistentSearchChangeType.ADD,
+ *                             PersistentSearchChangeType.DELETE,
+ *                             PersistentSearchChangeType.MODIFY,
+ *                             PersistentSearchChangeType.MODIFY_DN));
+ *
+ * ConnectionEntryReader reader = connection.search(request);
+ *
+ * while (reader.hasNext()) {
+ *     if (!reader.isReference()) {
+ *         SearchResultEntry entry = reader.readEntry(); // Entry that changed
+ *
+ *         EntryChangeNotificationResponseControl control = entry.getControl(
+ *                 EntryChangeNotificationResponseControl.DECODER,
+ *                 new DecodeOptions());
+ *
+ *         PersistentSearchChangeType type = control.getChangeType();
+ *         if (type.equals(PersistentSearchChangeType.MODIFY_DN)) {
+ *             // Previous DN: control.getPreviousName()
+ *         }
+ *         // Change number: control.getChangeNumber());
+ *     }
+ * }
+ *
+ * </pre>
+ *
+ * @see PersistentSearchRequestControl
+ * @see PersistentSearchChangeType
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch
+ *      - Persistent Search: A Simple LDAP Change Notification Mechanism </a>
+ */
+public final class EntryChangeNotificationResponseControl implements Control {
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the entry change notification response control. */
+    public static final String OID = "2.16.840.1.113730.3.4.7";
+
+    /** A decoder which can be used for decoding the entry change notification response control. */
+    public static final ControlDecoder<EntryChangeNotificationResponseControl> DECODER =
+            new ControlDecoder<EntryChangeNotificationResponseControl>() {
+
+                @Override
+                public EntryChangeNotificationResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control, options);
+
+                    if (control instanceof EntryChangeNotificationResponseControl) {
+                        return (EntryChangeNotificationResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_ECN_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_ECN_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    String previousDNString = null;
+                    long changeNumber = -1;
+                    PersistentSearchChangeType changeType;
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+
+                        final int changeTypeInt = reader.readEnumerated();
+                        switch (changeTypeInt) {
+                        case 1:
+                            changeType = PersistentSearchChangeType.ADD;
+                            break;
+                        case 2:
+                            changeType = PersistentSearchChangeType.DELETE;
+                            break;
+                        case 4:
+                            changeType = PersistentSearchChangeType.MODIFY;
+                            break;
+                        case 8:
+                            changeType = PersistentSearchChangeType.MODIFY_DN;
+                            break;
+                        default:
+                            final LocalizableMessage message =
+                                    ERR_ECN_BAD_CHANGE_TYPE.get(changeTypeInt);
+                            throw DecodeException.error(message);
+                        }
+
+                        if (reader.hasNextElement()
+                                && (reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE)) {
+                            if (changeType != PersistentSearchChangeType.MODIFY_DN) {
+                                final LocalizableMessage message =
+                                        ERR_ECN_ILLEGAL_PREVIOUS_DN.get(String.valueOf(changeType));
+                                throw DecodeException.error(message);
+                            }
+
+                            previousDNString = reader.readOctetStringAsString();
+                        }
+                        if (reader.hasNextElement()
+                                && (reader.peekType() == ASN1.UNIVERSAL_INTEGER_TYPE)) {
+                            changeNumber = reader.readInteger();
+                        }
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("%s", e));
+
+                        final LocalizableMessage message =
+                                ERR_ECN_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+
+                    final Schema schema =
+                            options.getSchemaResolver().resolveSchema(previousDNString);
+                    DN previousDN = null;
+                    if (previousDNString != null) {
+                        try {
+                            previousDN = DN.valueOf(previousDNString, schema);
+                        } catch (final LocalizedIllegalArgumentException e) {
+                            final LocalizableMessage message =
+                                    ERR_ECN_INVALID_PREVIOUS_DN.get(getExceptionMessage(e));
+                            throw DecodeException.error(message, e);
+                        }
+                    }
+
+                    return new EntryChangeNotificationResponseControl(control.isCritical(),
+                            changeType, previousDN, changeNumber);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new entry change notification response control with the
+     * provided change type and optional previous distinguished name and change
+     * number.
+     *
+     * @param type
+     *            The change type for this change notification control.
+     * @param previousName
+     *            The distinguished name that the entry had prior to a modify DN
+     *            operation, or <CODE>null</CODE> if the operation was not a
+     *            modify DN.
+     * @param changeNumber
+     *            The change number for the associated change, or a negative
+     *            value if no change number is available.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code type} was {@code null}.
+     */
+    public static EntryChangeNotificationResponseControl newControl(
+            final PersistentSearchChangeType type, final DN previousName, final long changeNumber) {
+        return new EntryChangeNotificationResponseControl(false, type, previousName, changeNumber);
+    }
+
+    /**
+     * Creates a new entry change notification response control with the
+     * provided change type and optional previous distinguished name and change
+     * number. The previous distinguished name, if provided, will be decoded
+     * using the default schema.
+     *
+     * @param type
+     *            The change type for this change notification control.
+     * @param previousName
+     *            The distinguished name that the entry had prior to a modify DN
+     *            operation, or <CODE>null</CODE> if the operation was not a
+     *            modify DN.
+     * @param changeNumber
+     *            The change number for the associated change, or a negative
+     *            value if no change number is available.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code previousName} is not a valid LDAP string
+     *             representation of a DN.
+     * @throws NullPointerException
+     *             If {@code type} was {@code null}.
+     */
+    public static EntryChangeNotificationResponseControl newControl(
+            final PersistentSearchChangeType type, final String previousName,
+            final long changeNumber) {
+        return new EntryChangeNotificationResponseControl(false, type, DN.valueOf(previousName),
+                changeNumber);
+    }
+
+    /** The previous DN for this change notification control. */
+    private final DN previousName;
+
+    /** The change number for this change notification control. */
+    private final long changeNumber;
+
+    /** The change type for this change notification control. */
+    private final PersistentSearchChangeType changeType;
+
+    private final boolean isCritical;
+
+    private EntryChangeNotificationResponseControl(final boolean isCritical,
+            final PersistentSearchChangeType changeType, final DN previousName,
+            final long changeNumber) {
+        Reject.ifNull(changeType);
+        this.isCritical = isCritical;
+        this.changeType = changeType;
+        this.previousName = previousName;
+        this.changeNumber = changeNumber;
+    }
+
+    /**
+     * Returns the change number for this entry change notification control.
+     *
+     * @return The change number for this entry change notification control, or
+     *         a negative value if no change number is available.
+     */
+    public long getChangeNumber() {
+        return changeNumber;
+    }
+
+    /**
+     * Returns the change type for this entry change notification control.
+     *
+     * @return The change type for this entry change notification control.
+     */
+    public PersistentSearchChangeType getChangeType() {
+        return changeType;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns the distinguished name that the entry had prior to a modify DN
+     * operation, or <CODE>null</CODE> if the operation was not a modify DN.
+     *
+     * @return The distinguished name that the entry had prior to a modify DN
+     *         operation.
+     */
+    public DN getPreviousName() {
+        return previousName;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeInteger(changeType.intValue());
+
+            if (previousName != null) {
+                writer.writeOctetString(previousName.toString());
+            }
+
+            if (changeNumber > 0) {
+                writer.writeInteger(changeNumber);
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("EntryChangeNotificationResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", changeType=");
+        builder.append(changeType);
+        builder.append(", previousDN=\"");
+        builder.append(previousName);
+        builder.append("\"");
+        builder.append(", changeNumber=");
+        builder.append(changeNumber);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GenericControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GenericControl.java
new file mode 100644
index 0000000..066e155
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GenericControl.java
@@ -0,0 +1,155 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A generic control which can be used to represent arbitrary raw request and
+ * response controls.
+ */
+public final class GenericControl implements Control {
+
+    /**
+     * Creates a new control having the same OID, criticality, and value as the
+     * provided control.
+     *
+     * @param control
+     *            The control to be copied.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code control} was {@code null}.
+     */
+    public static GenericControl newControl(final Control control) {
+        Reject.ifNull(control);
+
+        if (control instanceof GenericControl) {
+            return (GenericControl) control;
+        }
+
+        return new GenericControl(control.getOID(), control.isCritical(), control.getValue());
+    }
+
+    /**
+     * Creates a new non-critical control having the provided OID and no value.
+     *
+     * @param oid
+     *            The numeric OID associated with this control.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code oid} was {@code null}.
+     */
+    public static GenericControl newControl(final String oid) {
+        return new GenericControl(oid, false, null);
+    }
+
+    /**
+     * Creates a new control having the provided OID and criticality, but no
+     * value.
+     *
+     * @param oid
+     *            The numeric OID associated with this control.
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code oid} was {@code null}.
+     */
+    public static GenericControl newControl(final String oid, final boolean isCritical) {
+        return new GenericControl(oid, isCritical, null);
+    }
+
+    /**
+     * Creates a new control having the provided OID, criticality, and value.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param oid
+     *            The numeric OID associated with this control.
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param value
+     *            The value associated with this control, or {@code null} if
+     *            there is no value. Its format is defined by the specification
+     *            of this control.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code oid} was {@code null}.
+     */
+    public static GenericControl newControl(final String oid, final boolean isCritical,
+            final Object value) {
+        return new GenericControl(oid, isCritical, (value == null) ? null : ByteString
+                .valueOfObject(value));
+    }
+
+    private final String oid;
+
+    private final boolean isCritical;
+
+    private final ByteString value;
+
+    /** Prevent direct instantiation. */
+    private GenericControl(final String oid, final boolean isCritical, final ByteString value) {
+        Reject.ifNull(oid);
+        this.oid = oid;
+        this.isCritical = isCritical;
+        this.value = value;
+    }
+
+    @Override
+    public String getOID() {
+        return oid;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return value != null;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Control(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        if (value != null) {
+            builder.append(", value=");
+            builder.append(value.toHexPlusAsciiString(4));
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GetEffectiveRightsRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GetEffectiveRightsRequestControl.java
new file mode 100644
index 0000000..43200e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/GetEffectiveRightsRequestControl.java
@@ -0,0 +1,367 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A partial implementation of the get effective rights request control as
+ * defined in draft-ietf-ldapext-acl-model. The main differences are:
+ * <ul>
+ * <li>The response control is not supported. Instead the OpenDJ implementation
+ * creates attributes containing effective rights information with the entry
+ * being returned.
+ * <li>The attribute type names are dynamically created.
+ * <li>The set of attributes for which effective rights information is to be
+ * requested can be included in the control.
+ * </ul>
+ * The get effective rights request control value has the following BER
+ * encoding:
+ *
+ * <pre>
+ *  GetRightsControl ::= SEQUENCE {
+ *    authzId    authzId  -- Only the "dn:DN" form is supported.
+ *    attributes  SEQUENCE OF AttributeType
+ *  }
+ * </pre>
+ *
+ * You can use the control to retrieve effective rights during a search:
+ *
+ * <pre>
+ * String authDN = ...;
+ *
+ * SearchRequest request =
+ *         Requests.newSearchRequest(
+ *                     "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+ *                     "(uid=bjensen)", "cn", "aclRights", "aclRightsInfo")
+ *                     .addControl(GetEffectiveRightsRequestControl.newControl(
+ *                             true, authDN, "cn"));
+ *
+ * ConnectionEntryReader reader = connection.search(request);
+ * while (reader.hasNext()) {
+ *      if (!reader.isReference()) {
+ *          SearchResultEntry entry = reader.readEntry();
+ *          // Interpret aclRights and aclRightsInfo
+ *      }
+ * }
+ * </pre>
+ *
+ * The entries returned by the search hold the {@code aclRights} and
+ * {@code aclRightsInfo} attributes with the effective rights information. You
+ * must parse the attribute options and values to interpret the information.
+ *
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-ietf-ldapext-acl-model">draft-ietf-ldapext-acl-model
+ *      - Access Control Model for LDAPv3 </a>
+ */
+public final class GetEffectiveRightsRequestControl implements Control {
+    /** The OID for the get effective rights request control. */
+    public static final String OID = "1.3.6.1.4.1.42.2.27.9.5.2";
+
+    /** A decoder which can be used for decoding the get effective rights request control. */
+    public static final ControlDecoder<GetEffectiveRightsRequestControl> DECODER =
+            new ControlDecoder<GetEffectiveRightsRequestControl>() {
+
+                @Override
+                public GetEffectiveRightsRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof GetEffectiveRightsRequestControl) {
+                        return (GetEffectiveRightsRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_GETEFFECTIVERIGHTS_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    DN authorizationDN = null;
+                    List<AttributeType> attributes = Collections.emptyList();
+
+                    if (control.hasValue()) {
+                        final ASN1Reader reader = ASN1.getReader(control.getValue());
+                        try {
+                            reader.readStartSequence();
+                            final String authzIDString = reader.readOctetStringAsString();
+                            final String lowerAuthzIDString = authzIDString.toLowerCase();
+                            Schema schema;
+
+                            // Make sure authzId starts with "dn:" and is a
+                            // valid DN.
+                            if (lowerAuthzIDString.startsWith("dn:")) {
+                                final String authorizationDNString = authzIDString.substring(3);
+                                schema =
+                                        options.getSchemaResolver().resolveSchema(
+                                                authorizationDNString);
+                                try {
+                                    authorizationDN = DN.valueOf(authorizationDNString, schema);
+                                } catch (final LocalizedIllegalArgumentException e) {
+                                    final LocalizableMessage message =
+                                            ERR_GETEFFECTIVERIGHTS_INVALID_AUTHZIDDN
+                                                    .get(getExceptionMessage(e));
+                                    throw DecodeException.error(message, e);
+                                }
+                            } else {
+                                final LocalizableMessage message =
+                                        INFO_GETEFFECTIVERIGHTS_INVALID_AUTHZID
+                                                .get(lowerAuthzIDString);
+                                throw DecodeException.error(message);
+                            }
+
+                            // There is an sequence containing an attribute list, try to decode it.
+                            if (reader.hasNextElement()) {
+                                attributes = new LinkedList<>();
+                                reader.readStartSequence();
+                                while (reader.hasNextElement()) {
+                                    // Decode as an attribute type.
+                                    final String attributeName = reader.readOctetStringAsString();
+                                    AttributeType attributeType;
+                                    try {
+                                        // FIXME: we're using the schema
+                                        // associated with the authzid
+                                        // which is not really correct. We
+                                        // should really use the schema
+                                        // associated with the entry.
+                                        attributeType = schema.getAttributeType(attributeName);
+                                    } catch (final UnknownSchemaElementException e) {
+                                        final LocalizableMessage message =
+                                                ERR_GETEFFECTIVERIGHTS_UNKNOWN_ATTRIBUTE
+                                                        .get(attributeName);
+                                        throw DecodeException.error(message, e);
+                                    }
+                                    attributes.add(attributeType);
+                                }
+                                reader.readEndSequence();
+                                attributes = Collections.unmodifiableList(attributes);
+                            }
+                            reader.readEndSequence();
+                        } catch (final IOException e) {
+                            final LocalizableMessage message =
+                                    INFO_GETEFFECTIVERIGHTS_DECODE_ERROR.get(e.getMessage());
+                            throw DecodeException.error(message);
+                        }
+                    }
+
+                    return new GetEffectiveRightsRequestControl(control.isCritical(),
+                            authorizationDN, attributes);
+
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new get effective rights request control with the provided
+     * criticality, optional authorization name and attribute list.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param authorizationName
+     *            The distinguished name of the user for which effective rights
+     *            are to be returned, or {@code null} if the client's
+     *            authentication ID is to be used.
+     * @param attributes
+     *            The list of attributes for which effective rights are to be
+     *            returned, which may be empty indicating that no attribute
+     *            rights are to be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static GetEffectiveRightsRequestControl newControl(final boolean isCritical,
+            final DN authorizationName, final Collection<AttributeType> attributes) {
+        Reject.ifNull(attributes);
+
+        final Collection<AttributeType> copyOfAttributes =
+                Collections.unmodifiableList(new ArrayList<AttributeType>(attributes));
+        return new GetEffectiveRightsRequestControl(isCritical, authorizationName, copyOfAttributes);
+    }
+
+    /**
+     * Creates a new get effective rights request control with the provided
+     * criticality, optional authorization name and attribute list. The
+     * authorization name and attributes, if provided, will be decoded using the
+     * default schema.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param authorizationName
+     *            The distinguished name of the user for which effective rights
+     *            are to be returned, or {@code null} if the client's
+     *            authentication ID is to be used.
+     * @param attributes
+     *            The list of attributes for which effective rights are to be
+     *            returned, which may be empty indicating that no attribute
+     *            rights are to be returned.
+     * @return The new control.
+     * @throws UnknownSchemaElementException
+     *             If the default schema is a strict schema and one or more of
+     *             the requested attribute types were not recognized.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationName} is not a valid LDAP string
+     *             representation of a DN.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static GetEffectiveRightsRequestControl newControl(final boolean isCritical,
+            final String authorizationName, final String... attributes) {
+        Reject.ifNull((Object) attributes);
+
+        final DN dn = authorizationName == null ? null : DN.valueOf(authorizationName);
+
+        List<AttributeType> copyOfAttributes;
+        if (attributes != null && attributes.length > 0) {
+            copyOfAttributes = new ArrayList<>(attributes.length);
+            for (final String attribute : attributes) {
+                copyOfAttributes.add(Schema.getDefaultSchema().getAttributeType(attribute));
+            }
+            copyOfAttributes = Collections.unmodifiableList(copyOfAttributes);
+        } else {
+            copyOfAttributes = Collections.emptyList();
+        }
+
+        return new GetEffectiveRightsRequestControl(isCritical, dn, copyOfAttributes);
+    }
+
+    /** The DN representing the authzId (may be null meaning use the client's DN). */
+    private final DN authorizationName;
+
+    /** The unmodifiable list of attributes to be queried (may be empty). */
+    private final Collection<AttributeType> attributes;
+
+    private final boolean isCritical;
+
+    private GetEffectiveRightsRequestControl(final boolean isCritical, final DN authorizationName,
+            final Collection<AttributeType> attributes) {
+        this.isCritical = isCritical;
+        this.authorizationName = authorizationName;
+        this.attributes = attributes;
+    }
+
+    /**
+     * Returns an unmodifiable list of attributes for which effective rights are
+     * to be returned, which may be empty indicating that no attribute rights
+     * are to be returned.
+     *
+     * @return The unmodifiable list of attributes for which effective rights
+     *         are to be returned.
+     */
+    public Collection<AttributeType> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Returns the distinguished name of the user for which effective rights are
+     * to be returned, or {@code null} if the client's authentication ID is to
+     * be used.
+     *
+     * @return The distinguished name of the user for which effective rights are
+     *         to be returned.
+     */
+    public DN getAuthorizationName() {
+        return authorizationName;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            if (authorizationName != null) {
+                writer.writeOctetString("dn:" + authorizationName);
+            }
+
+            if (!attributes.isEmpty()) {
+                writer.writeStartSequence();
+                for (final AttributeType attribute : attributes) {
+                    writer.writeOctetString(attribute.getNameOrOID());
+                }
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return authorizationName != null || !attributes.isEmpty();
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GetEffectiveRightsRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", authorizationDN=\"");
+        builder.append(authorizationName);
+        builder.append("\"");
+        builder.append(", attributes=(");
+        builder.append(attributes);
+        builder.append("))");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ManageDsaITRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ManageDsaITRequestControl.java
new file mode 100644
index 0000000..d9966da
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ManageDsaITRequestControl.java
@@ -0,0 +1,160 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_MANAGEDSAIT_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_MANAGEDSAIT_INVALID_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The ManageDsaIT request control as defined in RFC 3296. This control allows
+ * manipulation of referral and other special objects as normal objects.
+ * <p>
+ * When this control is present in the request, the server will not generate a
+ * referral or continuation reference based upon information held in referral
+ * objects and instead will treat the referral object as a normal entry. The
+ * server, however, is still free to return referrals for other reasons.
+ *
+ * <pre>
+ * // &quot;dc=ref,dc=com&quot; holds a referral to something else.
+ *
+ * // Referral without the ManageDsaIT control:
+ * SearchRequest request = Requests.newSearchRequest(
+ *          &quot;dc=ref,dc=com&quot;,
+ *          SearchScope.SUBORDINATES,
+ *          &quot;(objectclass=*)&quot;,
+ *          &quot;&quot;);
+ *
+ * ConnectionEntryReader reader = connection.search(request);
+ * while (reader.hasNext()) {
+ *     if (reader.isReference()) {
+ *         SearchResultReference ref = reader.readReference();
+ *         // References: ref.getURIs()
+ *     }
+ * }
+ *
+ * // Referral with the ManageDsaIT control:
+ * request.addControl(ManageDsaITRequestControl.newControl(true));
+ * SearchResultEntry entry = connection.searchSingleEntry(request);
+ * // ...
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3296">RFC 3296 - Named
+ *      Subordinate References in Lightweight Directory Access Protocol (LDAP)
+ *      Directories </a>
+ */
+public final class ManageDsaITRequestControl implements Control {
+    /** The OID for the ManageDsaIT request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.2";
+
+    private static final ManageDsaITRequestControl CRITICAL_INSTANCE =
+            new ManageDsaITRequestControl(true);
+    private static final ManageDsaITRequestControl NONCRITICAL_INSTANCE =
+            new ManageDsaITRequestControl(false);
+
+    /** A decoder which can be used for decoding the Manage DsaIT request control. */
+    public static final ControlDecoder<ManageDsaITRequestControl> DECODER =
+            new ControlDecoder<ManageDsaITRequestControl>() {
+
+                @Override
+                public ManageDsaITRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof ManageDsaITRequestControl) {
+                        return (ManageDsaITRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_MANAGEDSAIT_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_MANAGEDSAIT_INVALID_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new ManageDsaIT request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static ManageDsaITRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private final boolean isCritical;
+
+    private ManageDsaITRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ManageDsaITRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/MatchedValuesRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/MatchedValuesRequestControl.java
new file mode 100644
index 0000000..2e4556b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/MatchedValuesRequestControl.java
@@ -0,0 +1,353 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.AbstractFilterVisitor;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.util.Reject;
+
+/**
+ * The matched values request control as defined in RFC 3876. The matched values
+ * control may be included in a search request to indicate that only attribute
+ * values matching one or more filters contained in the matched values control
+ * should be returned to the client.
+ * <p>
+ * The matched values request control supports a subset of the LDAP filter type
+ * defined in RFC 4511, and is defined as follows:
+ *
+ * <pre>
+ * ValuesReturnFilter ::= SEQUENCE OF SimpleFilterItem
+ *
+ * SimpleFilterItem ::= CHOICE {
+ *        equalityMatch   [3] AttributeValueAssertion,
+ *        substrings      [4] SubstringFilter,
+ *        greaterOrEqual  [5] AttributeValueAssertion,
+ *        lessOrEqual     [6] AttributeValueAssertion,
+ *        present         [7] AttributeDescription,
+ *        approxMatch     [8] AttributeValueAssertion,
+ *        extensibleMatch [9] SimpleMatchingAssertion }
+ *
+ * SimpleMatchingAssertion ::= SEQUENCE {
+ *        matchingRule    [1] MatchingRuleId OPTIONAL,
+ *        type            [2] AttributeDescription OPTIONAL,
+ * --- at least one of the above must be present
+ *        matchValue      [3] AssertionValue}
+ * </pre>
+ *
+ * For example Barbara Jensen's entry contains two common name values, Barbara
+ * Jensen and Babs Jensen. The following code retrieves only the latter.
+ *
+ * <pre>
+ * String DN = &quot;uid=bjensen,ou=People,dc=example,dc=com&quot;;
+ * SearchRequest request = Requests.newSearchRequest(DN,
+ *          SearchScope.BASE_OBJECT, &quot;(objectclass=*)&quot;, &quot;cn&quot;)
+ *          .addControl(MatchedValuesRequestControl
+ *                  .newControl(true, &quot;(cn=Babs Jensen)&quot;));
+ *
+ * // Get the entry, retrieving cn: Babs Jensen, not cn: Barbara Jensen
+ * SearchResultEntry entry = connection.searchSingleEntry(request);
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3876">RFC 3876 - Returning
+ *      Matched Values with the Lightweight Directory Access Protocol version 3
+ *      (LDAPv3) </a>
+ */
+public final class MatchedValuesRequestControl implements Control {
+    /** Visitor for validating matched values filters. */
+    private static final class FilterValidator extends
+            AbstractFilterVisitor<LocalizedIllegalArgumentException, Filter> {
+
+        @Override
+        public LocalizedIllegalArgumentException visitAndFilter(final Filter p,
+                final List<Filter> subFilters) {
+            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_AND.get(p.toString());
+            return new LocalizedIllegalArgumentException(message);
+        }
+
+        @Override
+        public LocalizedIllegalArgumentException visitExtensibleMatchFilter(final Filter p,
+                final String matchingRule, final String attributeDescription,
+                final ByteString assertionValue, final boolean dnAttributes) {
+            if (dnAttributes) {
+                final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_EXT.get(p.toString());
+                return new LocalizedIllegalArgumentException(message);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public LocalizedIllegalArgumentException visitNotFilter(final Filter p,
+                final Filter subFilter) {
+            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_NOT.get(p.toString());
+            return new LocalizedIllegalArgumentException(message);
+        }
+
+        @Override
+        public LocalizedIllegalArgumentException visitOrFilter(final Filter p,
+                final List<Filter> subFilters) {
+            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_OR.get(p.toString());
+            return new LocalizedIllegalArgumentException(message);
+        }
+
+        @Override
+        public LocalizedIllegalArgumentException visitUnrecognizedFilter(final Filter p,
+                final byte filterTag, final ByteString filterBytes) {
+            final LocalizableMessage message =
+                    ERR_MVFILTER_BAD_FILTER_UNRECOGNIZED.get(p.toString(), filterTag);
+            return new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * The OID for the matched values request control used to specify which
+     * particular attribute values should be returned in a search result entry.
+     */
+    public static final String OID = "1.2.826.0.1.3344810.2.3";
+
+    /** A decoder which can be used for decoding the matched values request control. */
+    public static final ControlDecoder<MatchedValuesRequestControl> DECODER =
+            new ControlDecoder<MatchedValuesRequestControl>() {
+
+                @Override
+                public MatchedValuesRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof MatchedValuesRequestControl) {
+                        return (MatchedValuesRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_MATCHEDVALUES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_MATCHEDVALUES_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+                        if (!reader.hasNextElement()) {
+                            final LocalizableMessage message = ERR_MATCHEDVALUES_NO_FILTERS.get();
+                            throw DecodeException.error(message);
+                        }
+
+                        final LinkedList<Filter> filters = new LinkedList<>();
+                        do {
+                            final Filter filter = LDAP.readFilter(reader);
+                            try {
+                                validateFilter(filter);
+                            } catch (final LocalizedIllegalArgumentException e) {
+                                throw DecodeException.error(e.getMessageObject());
+                            }
+                            filters.add(filter);
+                        } while (reader.hasNextElement());
+
+                        reader.readEndSequence();
+
+                        return new MatchedValuesRequestControl(control.isCritical(), Collections
+                                .unmodifiableList(filters));
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("%s", e));
+
+                        final LocalizableMessage message =
+                                ERR_MATCHEDVALUES_CANNOT_DECODE_VALUE_AS_SEQUENCE
+                                        .get(getExceptionMessage(e));
+                        throw DecodeException.error(message);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    private static final FilterValidator FILTER_VALIDATOR = new FilterValidator();
+
+    /**
+     * Creates a new matched values request control with the provided
+     * criticality and list of filters.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param filters
+     *            The list of filters of which at least one must match an
+     *            attribute value in order for the attribute value to be
+     *            returned to the client. The list must not be empty.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If one or more filters failed to conform to the filter
+     *             constraints defined in RFC 3876.
+     * @throws IllegalArgumentException
+     *             If {@code filters} was empty.
+     * @throws NullPointerException
+     *             If {@code filters} was {@code null}.
+     */
+    public static MatchedValuesRequestControl newControl(final boolean isCritical,
+            final Collection<Filter> filters) {
+        Reject.ifNull(filters);
+        Reject.ifFalse(filters.size() > 0, "filters is empty");
+
+        List<Filter> copyOfFilters;
+        if (filters.size() == 1) {
+            copyOfFilters = Collections.singletonList(validateFilter(filters.iterator().next()));
+        } else {
+            copyOfFilters = new ArrayList<>(filters.size());
+            for (final Filter filter : filters) {
+                copyOfFilters.add(validateFilter(filter));
+            }
+            copyOfFilters = Collections.unmodifiableList(copyOfFilters);
+        }
+
+        return new MatchedValuesRequestControl(isCritical, copyOfFilters);
+    }
+
+    /**
+     * Creates a new matched values request control with the provided
+     * criticality and list of filters.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param filters
+     *            The list of filters of which at least one must match an
+     *            attribute value in order for the attribute value to be
+     *            returned to the client. The list must not be empty.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If one or more filters could not be parsed, or if one or more
+     *             filters failed to conform to the filter constraints defined
+     *             in RFC 3876.
+     * @throws NullPointerException
+     *             If {@code filters} was {@code null}.
+     */
+    public static MatchedValuesRequestControl newControl(final boolean isCritical,
+            final String... filters) {
+        Reject.ifFalse(filters.length > 0, "filters is empty");
+
+        final List<Filter> parsedFilters = new ArrayList<>(filters.length);
+        for (final String filter : filters) {
+            parsedFilters.add(validateFilter(Filter.valueOf(filter)));
+        }
+        return new MatchedValuesRequestControl(isCritical, Collections
+                .unmodifiableList(parsedFilters));
+    }
+
+    private static Filter validateFilter(final Filter filter) {
+        final LocalizedIllegalArgumentException e = filter.accept(FILTER_VALIDATOR, filter);
+        if (e != null) {
+            throw e;
+        }
+        return filter;
+    }
+
+    private final Collection<Filter> filters;
+
+    private final boolean isCritical;
+
+    private MatchedValuesRequestControl(final boolean isCritical, final Collection<Filter> filters) {
+        this.isCritical = isCritical;
+        this.filters = filters;
+    }
+
+    /**
+     * Returns an unmodifiable collection containing the list of filters
+     * associated with this matched values control.
+     *
+     * @return An unmodifiable collection containing the list of filters
+     *         associated with this matched values control.
+     */
+    public Collection<Filter> getFilters() {
+        return filters;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            for (final Filter f : filters) {
+                LDAP.writeFilter(writer, f);
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("MatchedValuesRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiredResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiredResponseControl.java
new file mode 100644
index 0000000..a19e0b7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiredResponseControl.java
@@ -0,0 +1,155 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PWEXPIRED_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PWEXPIRED_CONTROL_INVALID_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Netscape password expired response control as defined in
+ * draft-vchu-ldap-pwd-policy. This control indicates to a client that their
+ * password has expired and must be changed. This control always has a value
+ * which is the string {@code "0"}.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ * char[] password = ...;
+ *
+ * try {
+ *     connection.bind(DN, password);
+ * } catch (LdapException e) {
+ *     Result result = e.getResult();
+ *     try {
+ *         PasswordExpiredResponseControl control =
+ *                 result.getControl(PasswordExpiredResponseControl.DECODER,
+ *                         new DecodeOptions());
+ *         if (!(control == null) && control.hasValue()) {
+ *             // Password expired
+ *         }
+ *     } catch (DecodeException de) {
+ *         // Failed to decode the response control.
+ *     }
+ * }
+ * </pre>
+ *
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy">draft-vchu-ldap-pwd-policy
+ *      - Password Policy for LDAP Directories </a>
+ */
+public final class PasswordExpiredResponseControl implements Control {
+    /** The OID for the Netscape password expired response control. */
+    public static final String OID = "2.16.840.1.113730.3.4.4";
+
+    private final boolean isCritical;
+
+    private static final PasswordExpiredResponseControl CRITICAL_INSTANCE =
+            new PasswordExpiredResponseControl(true);
+    private static final PasswordExpiredResponseControl NONCRITICAL_INSTANCE =
+            new PasswordExpiredResponseControl(false);
+
+    /** A decoder which can be used for decoding the password expired response control. */
+    public static final ControlDecoder<PasswordExpiredResponseControl> DECODER =
+            new ControlDecoder<PasswordExpiredResponseControl>() {
+
+                @Override
+                public PasswordExpiredResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PasswordExpiredResponseControl) {
+                        return (PasswordExpiredResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PWEXPIRED_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        try {
+                            Integer.parseInt(control.getValue().toString());
+                        } catch (final Exception e) {
+                            final LocalizableMessage message =
+                                    ERR_PWEXPIRED_CONTROL_INVALID_VALUE.get();
+                            throw DecodeException.error(message);
+                        }
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    private static final ByteString CONTROL_VALUE = ByteString.valueOfUtf8("0");
+
+    /**
+     * Creates a new Netscape password expired response control.
+     *
+     * @return The new control.
+     */
+    public static PasswordExpiredResponseControl newControl() {
+        return NONCRITICAL_INSTANCE;
+    }
+
+    private PasswordExpiredResponseControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return CONTROL_VALUE;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordExpiredResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiringResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiringResponseControl.java
new file mode 100644
index 0000000..d46cace
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordExpiringResponseControl.java
@@ -0,0 +1,177 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The Netscape password expiring response control as defined in
+ * draft-vchu-ldap-pwd-policy. This control serves as a warning to clients that
+ * the user's password is about to expire. The only element contained in the
+ * control value is a string representation of the number of seconds until
+ * expiration.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ * char[] password = ...;
+ *
+ * BindResult result = connection.bind(DN, password);
+ * try {
+ *     PasswordExpiringResponseControl control =
+ *             result.getControl(PasswordExpiringResponseControl.DECODER,
+ *                     new DecodeOptions());
+ *     if (!(control == null) && control.hasValue()) {
+ *         // Password expires in control.getSecondsUntilExpiration() seconds
+ *     }
+ * } catch (DecodeException de) {
+ *     // Failed to decode the response control.
+ * }
+ * </pre>
+ *
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-vchu-ldap-pwd-policy">draft-vchu-ldap-pwd-policy
+ *      - Password Policy for LDAP Directories </a>
+ */
+public final class PasswordExpiringResponseControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the Netscape password expiring response control. */
+    public static final String OID = "2.16.840.1.113730.3.4.5";
+
+    /** A decoder which can be used for decoding the password expiring response control. */
+    public static final ControlDecoder<PasswordExpiringResponseControl> DECODER =
+            new ControlDecoder<PasswordExpiringResponseControl>() {
+
+                @Override
+                public PasswordExpiringResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PasswordExpiringResponseControl) {
+                        return (PasswordExpiringResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PWEXPIRING_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        final LocalizableMessage message = ERR_PWEXPIRING_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    int secondsUntilExpiration;
+                    try {
+                        secondsUntilExpiration = Integer.parseInt(control.getValue().toString());
+                    } catch (final Exception e) {
+                        logger.debug(LocalizableMessage.raw("%s", e));
+
+                        final LocalizableMessage message =
+                                ERR_PWEXPIRING_CANNOT_DECODE_SECONDS_UNTIL_EXPIRATION
+                                        .get(getExceptionMessage(e));
+                        throw DecodeException.error(message);
+                    }
+
+                    return new PasswordExpiringResponseControl(control.isCritical(),
+                            secondsUntilExpiration);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new Netscape password expiring response control with the
+     * provided amount of time until expiration.
+     *
+     * @param secondsUntilExpiration
+     *            The length of time in seconds until the password actually
+     *            expires.
+     * @return The new control.
+     */
+    public static PasswordExpiringResponseControl newControl(final int secondsUntilExpiration) {
+        return new PasswordExpiringResponseControl(false, secondsUntilExpiration);
+    }
+
+    /** The length of time in seconds until the password actually expires. */
+    private final int secondsUntilExpiration;
+
+    private final boolean isCritical;
+
+    private PasswordExpiringResponseControl(final boolean isCritical,
+            final int secondsUntilExpiration) {
+        this.isCritical = isCritical;
+        this.secondsUntilExpiration = secondsUntilExpiration;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns the length of time in seconds until the password actually
+     * expires.
+     *
+     * @return The length of time in seconds until the password actually
+     *         expires.
+     */
+    public int getSecondsUntilExpiration() {
+        return secondsUntilExpiration;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return ByteString.valueOfUtf8(String.valueOf(secondsUntilExpiration));
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordExpiringResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", secondsUntilExpiration=");
+        builder.append(secondsUntilExpiration);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyErrorType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyErrorType.java
new file mode 100644
index 0000000..3a8b48b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyErrorType.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+/**
+ * A password policy error type as defined in draft-behera-ldap-password-policy
+ * is used to indicate problems concerning a user's account or password.
+ *
+ * @see PasswordPolicyRequestControl
+ * @see PasswordPolicyResponseControl
+ * @see PasswordPolicyWarningType
+ * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy">
+ *      draft-behera-ldap-password-policy - Password Policy for LDAP Directories
+ *      </a>
+ */
+public enum PasswordPolicyErrorType {
+    /** Indicates that the password has expired and must be reset. */
+    PASSWORD_EXPIRED(0, "passwordExpired"),
+
+    /** Indicates that the user's account has been locked. */
+    ACCOUNT_LOCKED(1, "accountLocked"),
+
+    /**
+     * Indicates that the password must be changed before the user will be
+     * allowed to perform any operation other than bind and modify.
+     */
+    CHANGE_AFTER_RESET(2, "changeAfterReset"),
+
+    /** Indicates that a user is restricted from changing her password. */
+    PASSWORD_MOD_NOT_ALLOWED(3, "passwordModNotAllowed"),
+
+    /** Indicates that the old password must be supplied in order to modify the password. */
+    MUST_SUPPLY_OLD_PASSWORD(4, "mustSupplyOldPassword"),
+
+    /** Indicates that a password doesn't pass quality checking. */
+    INSUFFICIENT_PASSWORD_QUALITY(5, "insufficientPasswordQuality"),
+
+    /** Indicates that a password is not long enough. */
+    PASSWORD_TOO_SHORT(6, "passwordTooShort"),
+
+    /** Indicates that the age of the password to be modified is not yet old enough. */
+    PASSWORD_TOO_YOUNG(7, "passwordTooYoung"),
+
+    /** Indicates that a password has already been used and the user must choose a different one. */
+    PASSWORD_IN_HISTORY(8, "passwordInHistory");
+
+    private final int intValue;
+
+    private final String name;
+
+    private PasswordPolicyErrorType(final int intValue, final String name) {
+        this.intValue = intValue;
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    /**
+     * Returns the integer value for this password policy error type.
+     *
+     * @return The integer value for this password policy error type.
+     */
+    int intValue() {
+        return intValue;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyRequestControl.java
new file mode 100644
index 0000000..fa6c3dd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyRequestControl.java
@@ -0,0 +1,171 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PWPOLICYREQ_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PWPOLICYREQ_CONTROL_HAS_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The password policy request control as defined in
+ * draft-behera-ldap-password-policy.
+ * <p>
+ * This control may be sent with any request in order to convey to the server
+ * that this client is aware of, and can process the password policy response
+ * control. When a server receives this control, it will return the password
+ * policy response control when appropriate and with the proper data.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ * char[] password = ...;
+ *
+ * try {
+ *     BindRequest request = Requests.newSimpleBindRequest(DN, password)
+ *             .addControl(PasswordPolicyRequestControl.newControl(true));
+ *
+ *     BindResult result = connection.bind(request);
+ *
+ *     PasswordPolicyResponseControl control =
+ *             result.getControl(PasswordPolicyResponseControl.DECODER,
+ *                     new DecodeOptions());
+ *     if (!(control == null) && !(control.getWarningType() == null)) {
+ *         // Password policy warning, use control.getWarningType(),
+ *         // and control.getWarningValue().
+ *     }
+ * } catch (LdapException e) {
+ *     Result result = e.getResult();
+ *     try {
+ *         PasswordPolicyResponseControl control =
+ *                 result.getControl(PasswordPolicyResponseControl.DECODER,
+ *                         new DecodeOptions());
+ *         if (!(control == null)) {
+ *             // Password policy error, use control.getErrorType().
+ *         }
+ *     } catch (DecodeException de) {
+ *         // Failed to decode the response control.
+ *     }
+ * } catch (DecodeException e) {
+ *     // Failed to decode the response control.
+ * }
+ * </pre>
+ *
+ * @see PasswordPolicyResponseControl
+ * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy">
+ *      draft-behera-ldap-password-policy - Password Policy for LDAP Directories
+ *      </a>
+ */
+public final class PasswordPolicyRequestControl implements Control {
+    /** The OID for the password policy control from draft-behera-ldap-password-policy. */
+    public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1";
+
+    private final boolean isCritical;
+
+    private static final PasswordPolicyRequestControl CRITICAL_INSTANCE =
+            new PasswordPolicyRequestControl(true);
+    private static final PasswordPolicyRequestControl NONCRITICAL_INSTANCE =
+            new PasswordPolicyRequestControl(false);
+
+    /** A decoder which can be used for decoding the password policy request control. */
+    public static final ControlDecoder<PasswordPolicyRequestControl> DECODER =
+            new ControlDecoder<PasswordPolicyRequestControl>() {
+
+                @Override
+                public PasswordPolicyRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PasswordPolicyRequestControl) {
+                        return (PasswordPolicyRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PWPOLICYREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message = ERR_PWPOLICYREQ_CONTROL_HAS_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new password policy request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static PasswordPolicyRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private PasswordPolicyRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordPolicyRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyResponseControl.java
new file mode 100644
index 0000000..9351647
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyResponseControl.java
@@ -0,0 +1,354 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.byteToHex;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The password policy response control as defined in
+ * draft-behera-ldap-password-policy.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ * char[] password = ...;
+ *
+ * try {
+ *     BindRequest request = Requests.newSimpleBindRequest(DN, password)
+ *             .addControl(PasswordPolicyRequestControl.newControl(true));
+ *
+ *     BindResult result = connection.bind(request);
+ *
+ *     PasswordPolicyResponseControl control =
+ *             result.getControl(PasswordPolicyResponseControl.DECODER,
+ *                     new DecodeOptions());
+ *     if (!(control == null) && !(control.getWarningType() == null)) {
+ *         // Password policy warning, use control.getWarningType(),
+ *         // and control.getWarningValue().
+ *     }
+ * } catch (LdapException e) {
+ *     Result result = e.getResult();
+ *     try {
+ *         PasswordPolicyResponseControl control =
+ *                 result.getControl(PasswordPolicyResponseControl.DECODER,
+ *                         new DecodeOptions());
+ *         if (!(control == null)) {
+ *             // Password policy error, use control.getErrorType().
+ *         }
+ *     } catch (DecodeException de) {
+ *         // Failed to decode the response control.
+ *     }
+ * } catch (DecodeException e) {
+ *     // Failed to decode the response control.
+ * }
+ * </pre>
+ *
+ * If the client has sent a passwordPolicyRequest control, the server (when
+ * solicited by the inclusion of the request control) sends this control with
+ * the following operation responses: bindResponse, modifyResponse, addResponse,
+ * compareResponse and possibly extendedResponse, to inform of various
+ * conditions, and MAY be sent with other operations (in the case of the
+ * changeAfterReset error).
+ *
+ * @see PasswordPolicyRequestControl
+ * @see PasswordPolicyWarningType
+ * @see PasswordPolicyErrorType
+ * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy">
+ *      draft-behera-ldap-password-policy - Password Policy for LDAP Directories
+ *      </a>
+ */
+public final class PasswordPolicyResponseControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the password policy control from draft-behera-ldap-password-policy. */
+    public static final String OID = PasswordPolicyRequestControl.OID;
+
+    private final int warningValue;
+
+    private final PasswordPolicyErrorType errorType;
+
+    private final PasswordPolicyWarningType warningType;
+
+    /** A decoder which can be used for decoding the password policy response control. */
+    public static final ControlDecoder<PasswordPolicyResponseControl> DECODER =
+            new ControlDecoder<PasswordPolicyResponseControl>() {
+
+                @Override
+                public PasswordPolicyResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PasswordPolicyResponseControl) {
+                        return (PasswordPolicyResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PWPOLICYRES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_PWPOLICYRES_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        PasswordPolicyWarningType warningType = null;
+                        PasswordPolicyErrorType errorType = null;
+                        int warningValue = -1;
+
+                        reader.readStartSequence();
+
+                        if (reader.hasNextElement() && (reader.peekType() == TYPE_WARNING_ELEMENT)) {
+                            // Its a CHOICE element. Read as sequence to
+                            // retrieve
+                            // nested element.
+                            reader.readStartSequence();
+                            final int warningChoiceValue = (0x7F & reader.peekType());
+                            if (warningChoiceValue < 0
+                                    || warningChoiceValue >= PasswordPolicyWarningType.values().length) {
+                                final LocalizableMessage message =
+                                        ERR_PWPOLICYRES_INVALID_WARNING_TYPE.get(byteToHex(reader
+                                                .peekType()));
+                                throw DecodeException.error(message);
+                            } else {
+                                warningType =
+                                        PasswordPolicyWarningType.values()[warningChoiceValue];
+                            }
+                            warningValue = (int) reader.readInteger();
+                            reader.readEndSequence();
+                        }
+
+                        if (reader.hasNextElement() && (reader.peekType() == TYPE_ERROR_ELEMENT)) {
+                            final int errorValue = reader.readEnumerated();
+                            if (errorValue < 0
+                                    || errorValue >= PasswordPolicyErrorType.values().length) {
+                                final LocalizableMessage message =
+                                        ERR_PWPOLICYRES_INVALID_ERROR_TYPE.get(errorValue);
+                                throw DecodeException.error(message);
+                            } else {
+                                errorType = PasswordPolicyErrorType.values()[errorValue];
+                            }
+                        }
+
+                        reader.readEndSequence();
+
+                        return new PasswordPolicyResponseControl(control.isCritical(), warningType,
+                                warningValue, errorType);
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("%s", e));
+
+                        final LocalizableMessage message =
+                                ERR_PWPOLICYRES_DECODE_ERROR.get(getExceptionMessage(e));
+                        throw DecodeException.error(message);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new password policy response control with the provided error.
+     *
+     * @param errorType
+     *            The password policy error type.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code errorType} was {@code null}.
+     */
+    public static PasswordPolicyResponseControl newControl(final PasswordPolicyErrorType errorType) {
+        Reject.ifNull(errorType);
+
+        return new PasswordPolicyResponseControl(false, null, -1, errorType);
+    }
+
+    /**
+     * Creates a new password policy response control with the provided warning.
+     *
+     * @param warningType
+     *            The password policy warning type.
+     * @param warningValue
+     *            The password policy warning value.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code warningValue} was negative.
+     * @throws NullPointerException
+     *             If {@code warningType} was {@code null}.
+     */
+    public static PasswordPolicyResponseControl newControl(
+            final PasswordPolicyWarningType warningType, final int warningValue) {
+        Reject.ifNull(warningType);
+        Reject.ifFalse(warningValue >= 0, "warningValue is negative");
+
+        return new PasswordPolicyResponseControl(false, warningType, warningValue, null);
+    }
+
+    /**
+     * Creates a new password policy response control with the provided warning
+     * and error.
+     *
+     * @param warningType
+     *            The password policy warning type.
+     * @param warningValue
+     *            The password policy warning value.
+     * @param errorType
+     *            The password policy error type.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code warningValue} was negative.
+     * @throws NullPointerException
+     *             If {@code warningType} or {@code errorType} was {@code null}.
+     */
+    public static PasswordPolicyResponseControl newControl(
+            final PasswordPolicyWarningType warningType, final int warningValue,
+            final PasswordPolicyErrorType errorType) {
+        Reject.ifNull(warningType);
+        Reject.ifNull(errorType);
+        Reject.ifFalse(warningValue >= 0, "warningValue is negative");
+
+        return new PasswordPolicyResponseControl(false, warningType, warningValue, errorType);
+    }
+
+    private final boolean isCritical;
+
+    /** The BER type value for the warning element of the control value. */
+    private static final byte TYPE_WARNING_ELEMENT = (byte) 0xA0;
+
+    /** The BER type value for the error element of the control value. */
+    private static final byte TYPE_ERROR_ELEMENT = (byte) 0x81;
+
+    private PasswordPolicyResponseControl(final boolean isCritical,
+            final PasswordPolicyWarningType warningType, final int warningValue,
+            final PasswordPolicyErrorType errorType) {
+        this.isCritical = isCritical;
+        this.warningType = warningType;
+        this.warningValue = warningValue;
+        this.errorType = errorType;
+    }
+
+    /**
+     * Returns the password policy error type, if available.
+     *
+     * @return The password policy error type, or {@code null} if this control
+     *         does not contain a error.
+     */
+    public PasswordPolicyErrorType getErrorType() {
+        return errorType;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            if (warningType != null) {
+                // Just write the CHOICE element as a single element SEQUENCE.
+                writer.writeStartSequence(TYPE_WARNING_ELEMENT);
+                writer.writeInteger((byte) (0x80 | warningType.intValue()), warningValue);
+                writer.writeEndSequence();
+            }
+
+            if (errorType != null) {
+                writer.writeInteger(TYPE_ERROR_ELEMENT, errorType.intValue());
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Returns the password policy warning type, if available.
+     *
+     * @return The password policy warning type, or {@code null} if this control
+     *         does not contain a warning.
+     */
+    public PasswordPolicyWarningType getWarningType() {
+        return warningType;
+    }
+
+    /**
+     * Returns the password policy warning value, if available. The value is
+     * undefined if this control does not contain a warning.
+     *
+     * @return The password policy warning value, or {@code -1} if this control
+     *         does not contain a warning.
+     */
+    public int getWarningValue() {
+        return warningValue;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordPolicyResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        if (warningType != null) {
+            builder.append(", warningType=");
+            builder.append(warningType);
+            builder.append(", warningValue=");
+            builder.append(warningValue);
+        }
+        if (errorType != null) {
+            builder.append(", errorType=");
+            builder.append(errorType);
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyWarningType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyWarningType.java
new file mode 100644
index 0000000..4f9d030
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PasswordPolicyWarningType.java
@@ -0,0 +1,65 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+/**
+ * A password policy warning type as defined in
+ * draft-behera-ldap-password-policy is used to indicate the current state of a
+ * user's password. More specifically, the number of seconds before a password
+ * will expire, or the remaining number of times a user will be allowed to
+ * authenticate with an expired password.
+ *
+ * @see PasswordPolicyRequestControl
+ * @see PasswordPolicyResponseControl
+ * @see PasswordPolicyErrorType
+ * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy">
+ *      draft-behera-ldap-password-policy - Password Policy for LDAP Directories
+ *      </a>
+ */
+public enum PasswordPolicyWarningType {
+    /** Indicates the number of seconds before a password will expire. */
+    TIME_BEFORE_EXPIRATION(0, "timeBeforeExpiration"),
+
+    /**
+     * Indicates the remaining number of times a user will be allowed to
+     * authenticate with an expired password.
+     */
+    GRACE_LOGINS_REMAINING(1, "graceAuthNsRemaining");
+
+    private final int intValue;
+
+    private final String name;
+
+    private PasswordPolicyWarningType(final int intValue, final String name) {
+        this.intValue = intValue;
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    /**
+     * Returns the integer value for this password policy warning type.
+     *
+     * @return The integer value for this password policy warning type.
+     */
+    int intValue() {
+        return intValue;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PermissiveModifyRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PermissiveModifyRequestControl.java
new file mode 100644
index 0000000..a8fdc91
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PermissiveModifyRequestControl.java
@@ -0,0 +1,158 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PERMISSIVE_MODIFY_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_PERMISSIVE_MODIFY_INVALID_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The Microsoft defined permissive modify request control. The OID for this
+ * control is 1.2.840.113556.1.4.1413, and it does not have a value.
+ * <p>
+ * This control can only be used with LDAP modify requests. It changes the
+ * behavior of the modify operation as follows:
+ * <ul>
+ * <li>Attempts to add an attribute value which already exists will be ignored
+ * and will not cause an
+ * {@link org.forgerock.opendj.ldap.ResultCode#ATTRIBUTE_OR_VALUE_EXISTS
+ * AttributeValueExists} error result to be returned.
+ * <li>Attempts to delete an attribute value which does not exist will be
+ * ignored and will not cause an
+ * {@link org.forgerock.opendj.ldap.ResultCode#NO_SUCH_ATTRIBUTE
+ * NoSuchAttribute} error result to be returned.
+ * </ul>
+ * In other words, a modify request {@code add} modification <i>ensures</i> that
+ * the attribute contains the specified attribute value, and a {@code delete}
+ * modification <i>ensures</i> that the attribute does not contain the specified
+ * attribute value.
+ *
+ * <pre>
+ * String groupDN = ...;
+ * String memberDN = ...;
+ * Connection connection = ...;
+ *
+ * // Add a member to a static group, telling the directory server not to
+ * // complain if the member already belongs to the group.
+ * ModifyRequest request = Requests.newModifyRequest(groupDN)
+ *          .addControl(PermissiveModifyRequestControl.newControl(true))
+ *          .addModification(ModificationType.ADD, "member", memberDN);
+ * connection.modify(request);
+ * </pre>
+ */
+public final class PermissiveModifyRequestControl implements Control {
+    /** The OID for the permissive modify request control. */
+    public static final String OID = "1.2.840.113556.1.4.1413";
+
+    private static final PermissiveModifyRequestControl CRITICAL_INSTANCE =
+            new PermissiveModifyRequestControl(true);
+
+    private static final PermissiveModifyRequestControl NONCRITICAL_INSTANCE =
+            new PermissiveModifyRequestControl(false);
+
+    /** A decoder which can be used for decoding the permissive modify request control. */
+    public static final ControlDecoder<PermissiveModifyRequestControl> DECODER =
+            new ControlDecoder<PermissiveModifyRequestControl>() {
+
+                @Override
+                public PermissiveModifyRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PermissiveModifyRequestControl) {
+                        return (PermissiveModifyRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PERMISSIVE_MODIFY_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_PERMISSIVE_MODIFY_INVALID_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new permissive modify request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static PermissiveModifyRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private final boolean isCritical;
+
+    private PermissiveModifyRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PermissiveModifyRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchChangeType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchChangeType.java
new file mode 100644
index 0000000..8e75f7c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchChangeType.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+/**
+ * A persistent search change type as defined in draft-ietf-ldapext-psearch is
+ * used to indicate the type of update operation that caused an entry change
+ * notification to occur.
+ *
+ * @see PersistentSearchRequestControl
+ * @see EntryChangeNotificationResponseControl
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch
+ *      - Persistent Search: A Simple LDAP Change Notification Mechanism </a>
+ */
+public enum PersistentSearchChangeType {
+    /** Indicates that an Add operation triggered the entry change notification. */
+    ADD(1, "add"),
+    /** Indicates that an Delete operation triggered the entry change notification. */
+    DELETE(2, "delete"),
+    /** Indicates that an Modify operation triggered the entry change notification. */
+    MODIFY(4, "modify"),
+    /** Indicates that an Modify DN operation triggered the entry change notification. */
+    MODIFY_DN(8, "modifyDN");
+
+    private final String name;
+    private final int intValue;
+
+    private PersistentSearchChangeType(final int intValue, final String name) {
+        this.name = name;
+        this.intValue = intValue;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    /**
+     * Returns the integer value for this change type as defined in the internet
+     * draft.
+     *
+     * @return The integer value for this change type.
+     */
+    public int intValue() {
+        return intValue;
+    }
+
+    /**
+     * Returns the enum value that would return the provided argument value from its {@link #intValue} method.
+     *
+     * @param value The value to match.
+     * @return The appropriate enum value.
+     */
+    public static PersistentSearchChangeType valueOf(int value) {
+        switch (value) {
+        case 1:
+            return ADD;
+        case 2:
+            return DELETE;
+        case 4:
+            return MODIFY;
+        case 8:
+            return MODIFY_DN;
+        default:
+            throw new IllegalArgumentException("Unknown int value: " + value);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchRequestControl.java
new file mode 100644
index 0000000..e0fe8eb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PersistentSearchRequestControl.java
@@ -0,0 +1,371 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The persistent search request control as defined in
+ * draft-ietf-ldapext-psearch. This control allows a client to receive
+ * notification of changes that occur in an LDAP server.
+ * <p>
+ * You can examine the entry change notification response control to get more
+ * information about a change returned by the persistent search.
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request =
+ *         Requests.newSearchRequest(
+ *                 "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+ *                 "(objectclass=inetOrgPerson)", "cn")
+ *                 .addControl(PersistentSearchRequestControl.newControl(
+ *                             true, true, true, // critical,changesOnly,returnECs
+ *                             PersistentSearchChangeType.ADD,
+ *                             PersistentSearchChangeType.DELETE,
+ *                             PersistentSearchChangeType.MODIFY,
+ *                             PersistentSearchChangeType.MODIFY_DN));
+ *
+ * ConnectionEntryReader reader = connection.search(request);
+ *
+ * while (reader.hasNext()) {
+ *     if (!reader.isReference()) {
+ *         SearchResultEntry entry = reader.readEntry(); // Entry that changed
+ *
+ *         EntryChangeNotificationResponseControl control = entry.getControl(
+ *                 EntryChangeNotificationResponseControl.DECODER,
+ *                 new DecodeOptions());
+ *
+ *         PersistentSearchChangeType type = control.getChangeType();
+ *         if (type.equals(PersistentSearchChangeType.MODIFY_DN)) {
+ *             // Previous DN: control.getPreviousName()
+ *         }
+ *         // Change number: control.getChangeNumber());
+ *     }
+ * }
+ *
+ * </pre>
+ *
+ * @see EntryChangeNotificationResponseControl
+ * @see PersistentSearchChangeType
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch
+ *      - Persistent Search: A Simple LDAP Change Notification Mechanism </a>
+ */
+public final class PersistentSearchRequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the persistent search request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.3";
+
+    /** A decoder which can be used for decoding the persistent search request control. */
+    public static final ControlDecoder<PersistentSearchRequestControl> DECODER =
+            new ControlDecoder<PersistentSearchRequestControl>() {
+
+                @Override
+                public PersistentSearchRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PersistentSearchRequestControl) {
+                        return (PersistentSearchRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PSEARCH_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        final LocalizableMessage message = ERR_PSEARCH_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    boolean changesOnly;
+                    boolean returnECs;
+                    int changeTypes;
+
+                    try {
+                        reader.readStartSequence();
+
+                        changeTypes = (int) reader.readInteger();
+                        changesOnly = reader.readBoolean();
+                        returnECs = reader.readBoolean();
+
+                        reader.readEndSequence();
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read sequence", e));
+
+                        final LocalizableMessage message =
+                                ERR_PSEARCH_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+
+                    final Set<PersistentSearchChangeType> changeTypeSet =
+                            EnumSet.noneOf(PersistentSearchChangeType.class);
+
+                    if ((changeTypes & 15) != 0) {
+                        final LocalizableMessage message =
+                                ERR_PSEARCH_BAD_CHANGE_TYPES.get(changeTypes);
+                        throw DecodeException.error(message);
+                    }
+
+                    if ((changeTypes & 1) != 0) {
+                        changeTypeSet.add(PersistentSearchChangeType.ADD);
+                    }
+
+                    if ((changeTypes & 2) != 0) {
+                        changeTypeSet.add(PersistentSearchChangeType.DELETE);
+                    }
+
+                    if ((changeTypes & 4) != 0) {
+                        changeTypeSet.add(PersistentSearchChangeType.MODIFY);
+                    }
+
+                    if ((changeTypes & 8) != 0) {
+                        changeTypeSet.add(PersistentSearchChangeType.MODIFY_DN);
+                    }
+
+                    return new PersistentSearchRequestControl(control.isCritical(), changesOnly,
+                            returnECs, Collections.unmodifiableSet(changeTypeSet));
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new persistent search request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param changesOnly
+     *            Indicates whether or not only updated entries should be
+     *            returned (added, modified, deleted, or subject to a modifyDN
+     *            operation). If this parameter is {@code false} then the search
+     *            will initially return all the existing entries which match the
+     *            filter.
+     * @param returnECs
+     *            Indicates whether or not the entry change notification control
+     *            should be included in updated entries that match the
+     *            associated search criteria.
+     * @param changeTypes
+     *            The types of update operation for which change notifications
+     *            should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code changeTypes} was {@code null}.
+     */
+    public static PersistentSearchRequestControl newControl(final boolean isCritical,
+            final boolean changesOnly, final boolean returnECs,
+            final Collection<PersistentSearchChangeType> changeTypes) {
+        Reject.ifNull(changeTypes);
+
+        final Set<PersistentSearchChangeType> copyOfChangeTypes =
+                EnumSet.noneOf(PersistentSearchChangeType.class);
+        copyOfChangeTypes.addAll(changeTypes);
+        return new PersistentSearchRequestControl(isCritical, changesOnly, returnECs, Collections
+                .unmodifiableSet(copyOfChangeTypes));
+    }
+
+    /**
+     * Creates a new persistent search request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param changesOnly
+     *            Indicates whether or not only updated entries should be
+     *            returned (added, modified, deleted, or subject to a modifyDN
+     *            operation). If this parameter is {@code false} then the search
+     *            will initially return all the existing entries which match the
+     *            filter.
+     * @param returnECs
+     *            Indicates whether or not the entry change notification control
+     *            should be included in updated entries that match the
+     *            associated search criteria.
+     * @param changeTypes
+     *            The types of update operation for which change notifications
+     *            should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code changeTypes} was {@code null}.
+     */
+    public static PersistentSearchRequestControl newControl(final boolean isCritical,
+            final boolean changesOnly, final boolean returnECs,
+            final PersistentSearchChangeType... changeTypes) {
+        Reject.ifNull((Object) changeTypes);
+
+        return newControl(isCritical, changesOnly, returnECs, Arrays.asList(changeTypes));
+    }
+
+    /**
+     * Indicates whether to only return entries that have been updated
+     * since the beginning of the search.
+     */
+    private final boolean changesOnly;
+
+    /**
+     * Indicates whether entries returned as a result of changes to
+     * directory data should include the entry change notification control.
+     */
+    private final boolean returnECs;
+
+    /** The logical OR of change types associated with this control. */
+    private final Set<PersistentSearchChangeType> changeTypes;
+
+    private final boolean isCritical;
+
+    private PersistentSearchRequestControl(final boolean isCritical, final boolean changesOnly,
+            final boolean returnECs, final Set<PersistentSearchChangeType> changeTypes) {
+        this.isCritical = isCritical;
+        this.changesOnly = changesOnly;
+        this.returnECs = returnECs;
+        this.changeTypes = changeTypes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the types of update operation for
+     * which change notifications should be returned.
+     *
+     * @return An unmodifiable set containing the types of update operation for
+     *         which change notifications should be returned.
+     */
+    public Set<PersistentSearchChangeType> getChangeTypes() {
+        return changeTypes;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+
+            int changeTypesInt = 0;
+            for (final PersistentSearchChangeType changeType : changeTypes) {
+                changeTypesInt |= changeType.intValue();
+            }
+            writer.writeInteger(changeTypesInt);
+
+            writer.writeBoolean(changesOnly);
+            writer.writeBoolean(returnECs);
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    /**
+     * Returns {@code true} if only updated entries should be returned (added,
+     * modified, deleted, or subject to a modifyDN operation), otherwise
+     * {@code false} if the search will initially return all the existing
+     * entries which match the filter.
+     *
+     * @return {@code true} if only updated entries should be returned (added,
+     *         modified, deleted, or subject to a modifyDN operation), otherwise
+     *         {@code false} if the search will initially return all the
+     *         existing entries which match the filter.
+     */
+    public boolean isChangesOnly() {
+        return changesOnly;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    /**
+     * Returns {@code true} if the entry change notification control should be
+     * included in updated entries that match the associated search criteria.
+     *
+     * @return {@code true} if the entry change notification control should be
+     *         included in updated entries that match the associated search
+     *         criteria.
+     */
+    public boolean isReturnECs() {
+        return returnECs;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PersistentSearchRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", changeTypes=[");
+
+        boolean comma = false;
+        for (final PersistentSearchChangeType type : changeTypes) {
+            if (comma) {
+                builder.append(", ");
+            }
+            builder.append(type);
+            comma = true;
+        }
+
+        builder.append("](");
+        builder.append(changeTypes);
+        builder.append("), changesOnly=");
+        builder.append(changesOnly);
+        builder.append(", returnECs=");
+        builder.append(returnECs);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
new file mode 100644
index 0000000..6bb6a7a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
@@ -0,0 +1,284 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.*;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The post-read request control as defined in RFC 4527. This control allows the
+ * client to read the target entry of an update operation immediately after the
+ * modifications are applied. These reads are done as an atomic part of the
+ * update operation.
+ * <p>
+ * The following example gets a modified entry from the result of a modify
+ * operation.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ *
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(DN)
+ *         .addControl(PostReadRequestControl.newControl(true, "description"))
+ *         .addModification(ModificationType.REPLACE,
+ *                 "description", "Using the PostReadRequestControl");
+ *
+ * Result result = connection.modify(request);
+ * PostReadResponseControl control =
+ *         result.getControl(PostReadResponseControl.DECODER,
+ *                 new DecodeOptions());
+ * Entry modifiedEntry = control.getEntry();
+ * </pre>
+ *
+ * @see PostReadResponseControl
+ * @see <a href="http://tools.ietf.org/html/rfc4527">RFC 4527 - Lightweight
+ *      Directory Access Protocol (LDAP) Read Entry Controls </a>
+ */
+public final class PostReadRequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /**
+     * The IANA-assigned OID for the LDAP post-read request control used for
+     * retrieving an entry in the state it had immediately after an update was
+     * applied.
+     */
+    public static final String OID = "1.3.6.1.1.13.2";
+
+    /** The list of raw attributes to return in the entry. */
+    private final List<String> attributes;
+
+    private final boolean isCritical;
+
+    private static final PostReadRequestControl CRITICAL_EMPTY_INSTANCE =
+            new PostReadRequestControl(true, Collections.<String> emptyList());
+
+    private static final PostReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
+            new PostReadRequestControl(false, Collections.<String> emptyList());
+
+    /** A decoder which can be used for decoding the post-read request control. */
+    public static final ControlDecoder<PostReadRequestControl> DECODER =
+            new ControlDecoder<PostReadRequestControl>() {
+
+                @Override
+                public PostReadRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PostReadRequestControl) {
+                        return (PostReadRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_POSTREAD_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        final LocalizableMessage message = ERR_POSTREADREQ_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    List<String> attributes;
+                    try {
+                        reader.readStartSequence();
+                        if (reader.hasNextElement()) {
+                            final String firstAttribute = reader.readOctetStringAsString();
+                            if (reader.hasNextElement()) {
+                                attributes = new ArrayList<>();
+                                attributes.add(firstAttribute);
+                                do {
+                                    attributes.add(reader.readOctetStringAsString());
+                                } while (reader.hasNextElement());
+                                attributes = unmodifiableList(attributes);
+                            } else {
+                                attributes = singletonList(firstAttribute);
+                            }
+                        } else {
+                            attributes = emptyList();
+                        }
+                        reader.readEndSequence();
+                    } catch (final Exception ae) {
+                        logger.debug(LocalizableMessage.raw("Unable to read sequence", ae));
+
+                        final LocalizableMessage message =
+                                ERR_POSTREADREQ_CANNOT_DECODE_VALUE.get(ae.getMessage());
+                        throw DecodeException.error(message, ae);
+                    }
+
+                    if (attributes.isEmpty()) {
+                        return control.isCritical() ? CRITICAL_EMPTY_INSTANCE
+                                : NONCRITICAL_EMPTY_INSTANCE;
+                    } else {
+                        return new PostReadRequestControl(control.isCritical(), attributes);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new post-read request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param attributes
+     *            The list of attributes to be included with the response
+     *            control. Attributes that are sub-types of listed attributes
+     *            are implicitly included. The list may be empty, indicating
+     *            that all user attributes should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static PostReadRequestControl newControl(final boolean isCritical,
+            final Collection<String> attributes) {
+        Reject.ifNull(attributes);
+
+        if (attributes.isEmpty()) {
+            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
+        } else if (attributes.size() == 1) {
+            return new PostReadRequestControl(isCritical, singletonList(attributes.iterator()
+                    .next()));
+        } else {
+            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    attributes)));
+        }
+    }
+
+    /**
+     * Creates a new post-read request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param attributes
+     *            The list of attributes to be included with the response
+     *            control. Attributes that are sub-types of listed attributes
+     *            are implicitly included. The list may be empty, indicating
+     *            that all user attributes should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static PostReadRequestControl newControl(final boolean isCritical,
+            final String... attributes) {
+        Reject.ifNull((Object) attributes);
+
+        if (attributes.length == 0) {
+            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
+        } else if (attributes.length == 1) {
+            return new PostReadRequestControl(isCritical, singletonList(attributes[0]));
+        } else {
+            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    asList(attributes))));
+        }
+    }
+
+    private PostReadRequestControl(final boolean isCritical, final List<String> attributes) {
+        this.isCritical = isCritical;
+        this.attributes = attributes;
+    }
+
+    /**
+     * Returns an unmodifiable list containing the names of attributes to be
+     * included with the response control. Attributes that are sub-types of
+     * listed attributes are implicitly included. The returned list may be
+     * empty, indicating that all user attributes should be returned.
+     *
+     * @return An unmodifiable list containing the names of attributes to be
+     *         included with the response control.
+     */
+    public List<String> getAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            if (attributes != null) {
+                for (final String attr : attributes) {
+                    writer.writeOctetString(attr);
+                }
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PostReadRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", attributes=");
+        builder.append(attributes);
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadResponseControl.java
new file mode 100644
index 0000000..92e6cad
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PostReadResponseControl.java
@@ -0,0 +1,210 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.util.Reject;
+
+/**
+ * The post-read response control as defined in RFC 4527. This control is
+ * returned by the server in response to a successful update operation which
+ * included a post-read request control. The control contains a Search Result
+ * Entry containing, subject to access controls and other constraints, values of
+ * the requested attributes.
+ * <p>
+ * The following example gets a modified entry from the result of a modify
+ * operation.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ *
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(DN)
+ *         .addControl(PostReadRequestControl.newControl(true, "description"))
+ *         .addModification(ModificationType.REPLACE,
+ *                 "description", "Using the PostReadRequestControl");
+ *
+ * Result result = connection.modify(request);
+ * PostReadResponseControl control =
+ *         result.getControl(PostReadResponseControl.DECODER,
+ *                 new DecodeOptions());
+ * Entry modifiedEntry = control.getEntry();
+ * </pre>
+ *
+ * @see PostReadRequestControl
+ * @see <a href="http://tools.ietf.org/html/rfc4527">RFC 4527 - Lightweight
+ *      Directory Access Protocol (LDAP) Read Entry Controls </a>
+ */
+public final class PostReadResponseControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /**
+     * The IANA-assigned OID for the LDAP post-read response control used for
+     * retrieving an entry in the state it had immediately after an update was
+     * applied.
+     */
+    public static final String OID = PostReadRequestControl.OID;
+
+    /** A decoder which can be used for decoding the post-read response control. */
+    public static final ControlDecoder<PostReadResponseControl> DECODER =
+            new ControlDecoder<PostReadResponseControl>() {
+
+                @Override
+                public PostReadResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PostReadResponseControl) {
+                        return (PostReadResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_POSTREAD_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        final LocalizableMessage message = ERR_POSTREADRESP_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    final Entry entry;
+                    try {
+                        entry = LDAP.readEntry(reader, options);
+                    } catch (final IOException le) {
+                        logger.debug(LocalizableMessage.raw("Unable to read result entry ", le));
+                        final LocalizableMessage message =
+                                ERR_POSTREADRESP_CANNOT_DECODE_VALUE.get(le.getMessage());
+                        throw DecodeException.error(message, le);
+                    }
+
+                    /*
+                     * FIXME: the RFC states that the control contains a
+                     * SearchResultEntry rather than an Entry. Can we assume
+                     * that the response will not contain a nested set of
+                     * controls?
+                     */
+                    return new PostReadResponseControl(control.isCritical(), Entries
+                            .unmodifiableEntry(entry));
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new post-read response control.
+     *
+     * @param entry
+     *            The entry whose contents reflect the state of the updated
+     *            entry immediately after the update operation was performed.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public static PostReadResponseControl newControl(final Entry entry) {
+        /*
+         * FIXME: all other control implementations are fully immutable. We
+         * should really do a defensive copy here in order to be consistent,
+         * rather than just wrap it. Also, the RFC states that the control
+         * contains a SearchResultEntry rather than an Entry. Can we assume that
+         * the response will not contain a nested set of controls?
+         */
+        return new PostReadResponseControl(false, Entries.unmodifiableEntry(entry));
+    }
+
+    private final Entry entry;
+
+    private final boolean isCritical;
+
+    private PostReadResponseControl(final boolean isCritical, final Entry entry) {
+        this.isCritical = isCritical;
+        this.entry = entry;
+    }
+
+    /**
+     * Returns an unmodifiable entry whose contents reflect the state of the
+     * updated entry immediately after the update operation was performed.
+     *
+     * @return The unmodifiable entry whose contents reflect the state of the
+     *         updated entry immediately after the update operation was
+     *         performed.
+     */
+    public Entry getEntry() {
+        return entry;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        try {
+            final ByteStringBuilder buffer = new ByteStringBuilder();
+            LDAP.writeEntry(ASN1.getWriter(buffer), entry);
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PostReadResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", entry=");
+        builder.append(entry);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
new file mode 100644
index 0000000..01ebac8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
@@ -0,0 +1,285 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.unmodifiableList;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The pre-read request control as defined in RFC 4527. This control allows the
+ * client to read the target entry of an update operation immediately before the
+ * modifications are applied. These reads are done as an atomic part of the
+ * update operation.
+ * <p>
+ * The following example gets the entry as it was before the modify operation.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ *
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(DN)
+ *         .addControl(PreReadRequestControl.newControl(true, "mail"))
+ *         .addModification(ModificationType.REPLACE,
+ *                 "mail", "modified@example.com");
+ *
+ * Result result = connection.modify(request);
+ * PreReadResponseControl control =
+ *             result.getControl(PreReadResponseControl.DECODER,
+ *                     new DecodeOptions());
+ * Entry unmodifiedEntry = control.getEntry();
+ * </pre>
+ *
+ * @see PreReadResponseControl
+ * @see <a href="http://tools.ietf.org/html/rfc4527">RFC 4527 - Lightweight
+ *      Directory Access Protocol (LDAP) Read Entry Controls </a>
+ */
+public final class PreReadRequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /**
+     * The IANA-assigned OID for the LDAP pre-read request control used for
+     * retrieving an entry in the state it had immediately before an update was
+     * applied.
+     */
+    public static final String OID = "1.3.6.1.1.13.1";
+
+    /** The list of raw attributes to return in the entry. */
+    private final List<String> attributes;
+
+    private final boolean isCritical;
+
+    private static final PreReadRequestControl CRITICAL_EMPTY_INSTANCE = new PreReadRequestControl(
+            true, Collections.<String> emptyList());
+
+    private static final PreReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
+            new PreReadRequestControl(false, Collections.<String> emptyList());
+
+    /** A decoder which can be used for decoding the pre-read request control. */
+    public static final ControlDecoder<PreReadRequestControl> DECODER =
+            new ControlDecoder<PreReadRequestControl>() {
+
+                @Override
+                public PreReadRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PreReadRequestControl) {
+                        return (PreReadRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PREREAD_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        final LocalizableMessage message = ERR_PREREADREQ_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    List<String> attributes;
+                    try {
+                        reader.readStartSequence();
+                        if (reader.hasNextElement()) {
+                            final String firstAttribute = reader.readOctetStringAsString();
+                            if (reader.hasNextElement()) {
+                                attributes = new ArrayList<>();
+                                attributes.add(firstAttribute);
+                                do {
+                                    attributes.add(reader.readOctetStringAsString());
+                                } while (reader.hasNextElement());
+                                attributes = unmodifiableList(attributes);
+                            } else {
+                                attributes = singletonList(firstAttribute);
+                            }
+                        } else {
+                            attributes = emptyList();
+                        }
+                        reader.readEndSequence();
+                    } catch (final Exception ex) {
+                        logger.debug(LocalizableMessage.raw("Unable to read sequence", ex));
+
+                        final LocalizableMessage message =
+                                ERR_PREREADREQ_CANNOT_DECODE_VALUE.get(ex.getMessage());
+                        throw DecodeException.error(message, ex);
+                    }
+
+                    if (attributes.isEmpty()) {
+                        return control.isCritical() ? CRITICAL_EMPTY_INSTANCE
+                                : NONCRITICAL_EMPTY_INSTANCE;
+                    } else {
+                        return new PreReadRequestControl(control.isCritical(), attributes);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new pre-read request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param attributes
+     *            The list of attributes to be included with the response
+     *            control. Attributes that are sub-types of listed attributes
+     *            are implicitly included. The list may be empty, indicating
+     *            that all user attributes should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static PreReadRequestControl newControl(final boolean isCritical,
+            final Collection<String> attributes) {
+        Reject.ifNull(attributes);
+
+        if (attributes.isEmpty()) {
+            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
+        } else if (attributes.size() == 1) {
+            return new PreReadRequestControl(isCritical,
+                    singletonList(attributes.iterator().next()));
+        } else {
+            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    attributes)));
+        }
+    }
+
+    /**
+     * Creates a new pre-read request control.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored
+     * @param attributes
+     *            The list of attributes to be included with the response
+     *            control. Attributes that are sub-types of listed attributes
+     *            are implicitly included. The list may be empty, indicating
+     *            that all user attributes should be returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code attributes} was {@code null}.
+     */
+    public static PreReadRequestControl newControl(final boolean isCritical,
+            final String... attributes) {
+        Reject.ifNull((Object) attributes);
+
+        if (attributes.length == 0) {
+            return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
+        } else if (attributes.length == 1) {
+            return new PreReadRequestControl(isCritical, singletonList(attributes[0]));
+        } else {
+            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    asList(attributes))));
+        }
+    }
+
+    private PreReadRequestControl(final boolean isCritical, final List<String> attributes) {
+        this.isCritical = isCritical;
+        this.attributes = attributes;
+    }
+
+    /**
+     * Returns an unmodifiable list containing the names of attributes to be
+     * included with the response control. Attributes that are sub-types of
+     * listed attributes are implicitly included. The returned list may be
+     * empty, indicating that all user attributes should be returned.
+     *
+     * @return An unmodifiable list containing the names of attributes to be
+     *         included with the response control.
+     */
+    public List<String> getAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            if (attributes != null) {
+                for (final String attr : attributes) {
+                    writer.writeOctetString(attr);
+                }
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PreReadRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", attributes=");
+        builder.append(attributes);
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadResponseControl.java
new file mode 100644
index 0000000..1b4b77f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/PreReadResponseControl.java
@@ -0,0 +1,209 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.util.Reject;
+
+/**
+ * The pre-read response control as defined in RFC 4527. This control is
+ * returned by the server in response to a successful update operation which
+ * included a pre-read request control. The control contains a Search Result
+ * Entry containing, subject to access controls and other constraints, values of
+ * the requested attributes.
+ * <p>
+ * The following example gets the entry as it was before the modify operation.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String DN = ...;
+ *
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(DN)
+ *         .addControl(PreReadRequestControl.newControl(true, "mail"))
+ *         .addModification(ModificationType.REPLACE,
+ *                 "mail", "modified@example.com");
+ *
+ * Result result = connection.modify(request);
+ * PreReadResponseControl control =
+ *             result.getControl(PreReadResponseControl.DECODER,
+ *                     new DecodeOptions());
+ * Entry unmodifiedEntry = control.getEntry();
+ * </pre>
+ *
+ * @see PreReadRequestControl
+ * @see <a href="http://tools.ietf.org/html/rfc4527">RFC 4527 - Lightweight
+ *      Directory Access Protocol (LDAP) Read Entry Controls </a>
+ */
+public final class PreReadResponseControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /**
+     * The IANA-assigned OID for the LDAP pre-read response control used for
+     * retrieving an entry in the state it had immediately before an update was
+     * applied.
+     */
+    public static final String OID = PreReadRequestControl.OID;
+
+    /** A decoder which can be used for decoding the pre-read response control. */
+    public static final ControlDecoder<PreReadResponseControl> DECODER =
+            new ControlDecoder<PreReadResponseControl>() {
+
+                @Override
+                public PreReadResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof PreReadResponseControl) {
+                        return (PreReadResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PREREAD_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        final LocalizableMessage message = ERR_PREREADRESP_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    final Entry entry;
+                    try {
+                        entry = LDAP.readEntry(reader, options);
+                    } catch (final IOException le) {
+                        logger.debug(LocalizableMessage.raw("Unable to read result entry", le));
+                        final LocalizableMessage message =
+                                ERR_PREREADRESP_CANNOT_DECODE_VALUE.get(le.getMessage());
+                        throw DecodeException.error(message, le);
+                    }
+
+                    /*
+                     * FIXME: the RFC states that the control contains a
+                     * SearchResultEntry rather than an Entry. Can we assume
+                     * that the response will not contain a nested set of
+                     * controls?
+                     */
+                    return new PreReadResponseControl(control.isCritical(), Entries
+                            .unmodifiableEntry(entry));
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new pre-read response control.
+     *
+     * @param entry
+     *            The entry whose contents reflect the state of the updated
+     *            entry immediately before the update operation was performed.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public static PreReadResponseControl newControl(final Entry entry) {
+        /*
+         * FIXME: all other control implementations are fully immutable. We
+         * should really do a defensive copy here in order to be consistent,
+         * rather than just wrap it. Also, the RFC states that the control
+         * contains a SearchResultEntry rather than an Entry. Can we assume that
+         * the response will not contain a nested set of controls?
+         */
+        return new PreReadResponseControl(false, Entries.unmodifiableEntry(entry));
+    }
+
+    private final Entry entry;
+
+    private final boolean isCritical;
+
+    private PreReadResponseControl(final boolean isCritical, final Entry entry) {
+        this.isCritical = isCritical;
+        this.entry = entry;
+    }
+
+    /**
+     * Returns an unmodifiable entry whose contents reflect the state of the
+     * updated entry immediately before the update operation was performed.
+     *
+     * @return The unmodifiable entry whose contents reflect the state of the
+     *         updated entry immediately before the update operation was
+     *         performed.
+     */
+    public Entry getEntry() {
+        return entry;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        try {
+            final ByteStringBuilder buffer = new ByteStringBuilder();
+            LDAP.writeEntry(ASN1.getWriter(buffer), entry);
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PreReadResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", entry=");
+        builder.append(entry);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV1RequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV1RequestControl.java
new file mode 100644
index 0000000..3395a6c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV1RequestControl.java
@@ -0,0 +1,220 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/**
+ * The proxy authorization v1 request control as defined in
+ * draft-weltman-ldapv3-proxy-04. This control allows a user to request that an
+ * operation be performed using the authorization of another user. The target
+ * user is specified as a DN in the control value, which distinguishes it from
+ * later versions of the control (which used a different OID) in which the
+ * target user was specified using an authorization ID.
+ * <p>
+ * This control implementation is based on version 1 of the proxied
+ * authorization control as defined in early versions of
+ * draft-weltman-ldapv3-proxy (this implementation is based on the "-04"
+ * revision) and is intended for use in legacy applications. New applications
+ * should use the v2 version of this control in preference.
+ *
+ * @see <a href="http://tools.ietf.org/html/draft-weltman-ldapv3-proxy-04">
+ *      draft-weltman-ldapv3-proxy-04 - LDAP Proxied Authorization Control </a>
+ */
+public final class ProxiedAuthV1RequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the proxied authorization v1 control. */
+    public static final String OID = "2.16.840.1.113730.3.4.12";
+
+    /** A decoder which can be used for decoding the proxied authorization v1 request control. */
+    public static final ControlDecoder<ProxiedAuthV1RequestControl> DECODER =
+            new ControlDecoder<ProxiedAuthV1RequestControl>() {
+
+                @Override
+                public ProxiedAuthV1RequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof ProxiedAuthV1RequestControl) {
+                        return (ProxiedAuthV1RequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH1_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.isCritical()) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH1_CONTROL_NOT_CRITICAL.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_PROXYAUTH1_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    String authorizationDNString;
+                    try {
+                        reader.readStartSequence();
+                        authorizationDNString = reader.readOctetStringAsString();
+                        reader.readEndSequence();
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read sequence", e));
+
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH1_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+
+                    final Schema schema =
+                            options.getSchemaResolver().resolveSchema(authorizationDNString);
+                    DN authorizationDN;
+                    try {
+                        authorizationDN = DN.valueOf(authorizationDNString, schema);
+                    } catch (final LocalizedIllegalArgumentException e) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH1_INVALID_AUTHZIDDN.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+
+                    return new ProxiedAuthV1RequestControl(authorizationDN);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new proxy authorization v1 request control with the provided
+     * authorization name.
+     *
+     * @param authorizationName
+     *            The distinguished name of the user whose authorization is to
+     *            be used when performing the operation.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code authorizationName} was {@code null}.
+     */
+    public static ProxiedAuthV1RequestControl newControl(final DN authorizationName) {
+        Reject.ifNull(authorizationName);
+        return new ProxiedAuthV1RequestControl(authorizationName);
+    }
+
+    /**
+     * Creates a new proxy authorization v1 request control with the provided
+     * authorization name decoded using the default schema.
+     *
+     * @param authorizationName
+     *            The distinguished name of the user whose authorization is to
+     *            be used when performing the operation.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationName} is not a valid LDAP string
+     *             representation of a DN.
+     * @throws NullPointerException
+     *             If {@code authorizationName} was {@code null}.
+     */
+    public static ProxiedAuthV1RequestControl newControl(final String authorizationName) {
+        Reject.ifNull(authorizationName);
+        return new ProxiedAuthV1RequestControl(DN.valueOf(authorizationName));
+    }
+
+    private final DN authorizationName;
+
+    private ProxiedAuthV1RequestControl(final DN authorizationName) {
+        this.authorizationName = authorizationName;
+    }
+
+    /**
+     * Returns the distinguished name of the user whose authorization is to be
+     * used when performing the operation.
+     *
+     * @return The distinguished name of the user whose authorization is to be
+     *         used when performing the operation.
+     */
+    public DN getAuthorizationDNName() {
+        return authorizationName;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeOctetString(authorizationName.toString());
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder();
+        buffer.append("ProxiedAuthorizationV1Control(oid=");
+        buffer.append(getOID());
+        buffer.append(", criticality=");
+        buffer.append(isCritical());
+        buffer.append(", proxyDN=\"");
+        buffer.append(authorizationName);
+        buffer.append("\")");
+        return buffer.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV2RequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV2RequestControl.java
new file mode 100644
index 0000000..8ac94a3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ProxiedAuthV2RequestControl.java
@@ -0,0 +1,231 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The proxy authorization v2 request control as defined in RFC 4370. This
+ * control allows a user to request that an operation be performed using the
+ * authorization of another user.
+ * <p>
+ * The target user is specified using an authorization ID, or {@code authzId},
+ * as defined in RFC 4513 section 5.2.1.8.
+ * <p>
+ * This example shows an application replacing a description on a user entry on
+ * behalf of a directory administrator.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String bindDN = "cn=My App,ou=Apps,dc=example,dc=com";          // Client app
+ * char[] password = ...;
+ * String targetDn = "uid=bjensen,ou=People,dc=example,dc=com";    // Regular user
+ * String authzId = "dn:uid=kvaughan,ou=People,dc=example,dc=com"; // Admin user
+ *
+ * ModifyRequest request =
+ *         Requests.newModifyRequest(targetDn)
+ *         .addControl(ProxiedAuthV2RequestControl.newControl(authzId))
+ *         .addModification(ModificationType.REPLACE, "description",
+ *                 "Done with proxied authz");
+ *
+ * connection.bind(bindDN, password);
+ * connection.modify(request);
+ * Entry entry = connection.readEntry(targetDn, "description");
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4370">RFC 4370 - Lightweight
+ *      Directory Access Protocol (LDAP) Proxied Authorization Control </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public final class ProxiedAuthV2RequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the proxied authorization v2 control. */
+    public static final String OID = "2.16.840.1.113730.3.4.18";
+
+    private static final ProxiedAuthV2RequestControl ANONYMOUS =
+            new ProxiedAuthV2RequestControl("");
+
+    /** A decoder which can be used for decoding the proxied authorization v2 request control. */
+    public static final ControlDecoder<ProxiedAuthV2RequestControl> DECODER =
+            new ControlDecoder<ProxiedAuthV2RequestControl>() {
+
+                @Override
+                public ProxiedAuthV2RequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof ProxiedAuthV2RequestControl) {
+                        return (ProxiedAuthV2RequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH2_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.isCritical()) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH2_CONTROL_NOT_CRITICAL.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_PROXYAUTH2_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    String authorizationID;
+                    try {
+                        if (reader.elementAvailable()) {
+                            // Try the legacy encoding where the value is
+                            // wrapped by an extra octet string
+                            authorizationID = reader.readOctetStringAsString();
+                        } else {
+                            authorizationID = control.getValue().toString();
+                        }
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read exceptionID", e));
+
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH2_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+
+                    if (authorizationID.length() == 0) {
+                        // Anonymous.
+                        return ANONYMOUS;
+                    }
+
+                    final int colonIndex = authorizationID.indexOf(':');
+                    if (colonIndex < 0) {
+                        final LocalizableMessage message =
+                                ERR_PROXYAUTH2_INVALID_AUTHZID_TYPE.get(authorizationID);
+                        throw DecodeException.error(message);
+                    }
+
+                    return new ProxiedAuthV2RequestControl(authorizationID);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new proxy authorization v2 request control with the provided
+     * authorization ID. The authorization ID usually has the form "dn:"
+     * immediately followed by the distinguished name of the user, or "u:"
+     * followed by a user ID string, but other forms are permitted.
+     *
+     * @param authorizationID
+     *            The authorization ID of the user whose authorization is to be
+     *            used when performing the operation.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws NullPointerException
+     *             If {@code authorizationName} was {@code null}.
+     */
+    public static ProxiedAuthV2RequestControl newControl(final String authorizationID) {
+        if (authorizationID.length() == 0) {
+            // Anonymous.
+            return ANONYMOUS;
+        }
+
+        final int colonIndex = authorizationID.indexOf(':');
+        if (colonIndex < 0) {
+            final LocalizableMessage message =
+                    ERR_PROXYAUTH2_INVALID_AUTHZID_TYPE.get(authorizationID);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+
+        return new ProxiedAuthV2RequestControl(authorizationID);
+    }
+
+    /** The authorization ID from the control value. */
+    private final String authorizationID;
+
+    private ProxiedAuthV2RequestControl(final String authorizationID) {
+        this.authorizationID = authorizationID;
+    }
+
+    /**
+     * Returns the authorization ID of the user whose authorization is to be
+     * used when performing the operation. The authorization ID usually has the
+     * form "dn:" immediately followed by the distinguished name of the user, or
+     * "u:" followed by a user ID string, but other forms are permitted.
+     *
+     * @return The authorization ID of the user whose authorization is to be
+     *         used when performing the operation.
+     */
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return ByteString.valueOfUtf8(authorizationID);
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ProxiedAuthorizationV2Control(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", authorizationID=\"");
+        builder.append(authorizationID);
+        builder.append("\")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortRequestControl.java
new file mode 100644
index 0000000..134bc63
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortRequestControl.java
@@ -0,0 +1,323 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.SortKey;
+import org.forgerock.util.Reject;
+
+/**
+ * The server-side sort request control as defined in RFC 2891. This control may
+ * be included in a search request to indicate that search result entries should
+ * be sorted by the server before being returned. The sort order is specified
+ * using one or more sort keys, the first being the primary key, and so on.
+ * <p>
+ * This controls may be useful when the client has limited functionality or for
+ * some other reason cannot sort the results but still needs them sorted. In
+ * cases where the client can sort the results client-side sorting is
+ * recommended in order to reduce load on the server. See {@link SortKey} for an
+ * example of client-side sorting.
+ * <p>
+ * The following example demonstrates how to work with a server-side sort.
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request = Requests.newSearchRequest(
+ *         "ou=People,dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
+ *         .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("cn")));
+ *
+ * SearchResultHandler resultHandler = new MySearchResultHandler();
+ * Result result = connection.search(request, resultHandler);
+ *
+ * ServerSideSortResponseControl control = result.getControl(
+ *         ServerSideSortResponseControl.DECODER, new DecodeOptions());
+ * if (control != null && control.getResult() == ResultCode.SUCCESS) {
+ *     // Entries are sorted.
+ * } else {
+ *     // Entries not sorted.
+ * }
+ * </pre>
+ *
+ * @see ServerSideSortResponseControl
+ * @see SortKey
+ * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control
+ *      Extension for Server Side Sorting of Search Results </a>
+ */
+public final class ServerSideSortRequestControl implements Control {
+    /** The OID for the server-side sort request control. */
+    public static final String OID = "1.2.840.113556.1.4.473";
+
+    /** The BER type to use when encoding the orderingRule element. */
+    private static final byte TYPE_ORDERING_RULE_ID = (byte) 0x80;
+
+    /** The BER type to use when encoding the reverseOrder element. */
+    private static final byte TYPE_REVERSE_ORDER = (byte) 0x81;
+
+    /** A decoder which can be used for decoding the server side sort request control. */
+    public static final ControlDecoder<ServerSideSortRequestControl> DECODER =
+            new ControlDecoder<ServerSideSortRequestControl>() {
+
+                @Override
+                public ServerSideSortRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof ServerSideSortRequestControl) {
+                        return (ServerSideSortRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_SORTREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The request control must always have a value.
+                        final LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+                        if (!reader.hasNextElement()) {
+                            final LocalizableMessage message =
+                                    INFO_SORTREQ_CONTROL_NO_SORT_KEYS.get();
+                            throw DecodeException.error(message);
+                        }
+
+                        final List<SortKey> keys = new LinkedList<>();
+                        while (reader.hasNextElement()) {
+                            reader.readStartSequence();
+                            final String attrName = reader.readOctetStringAsString();
+
+                            String orderingRule = null;
+                            boolean reverseOrder = false;
+                            if (reader.hasNextElement()
+                                    && (reader.peekType() == TYPE_ORDERING_RULE_ID)) {
+                                orderingRule = reader.readOctetStringAsString();
+                            }
+                            if (reader.hasNextElement()
+                                    && (reader.peekType() == TYPE_REVERSE_ORDER)) {
+                                reverseOrder = reader.readBoolean();
+                            }
+                            reader.readEndSequence();
+
+                            keys.add(new SortKey(attrName, reverseOrder, orderingRule));
+                        }
+                        reader.readEndSequence();
+
+                        return new ServerSideSortRequestControl(control.isCritical(), Collections
+                                .unmodifiableList(keys));
+                    } catch (final IOException e) {
+                        final LocalizableMessage message =
+                                INFO_SORTREQ_CONTROL_CANNOT_DECODE_VALUE
+                                        .get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new server side sort request control with the provided
+     * criticality and list of sort keys.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param keys
+     *            The list of sort keys.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code keys} was {@code null}.
+     */
+    public static ServerSideSortRequestControl newControl(final boolean isCritical,
+            final Collection<SortKey> keys) {
+        Reject.ifNull(keys);
+        Reject.ifFalse(!keys.isEmpty(), "keys must not be empty");
+
+        return new ServerSideSortRequestControl(isCritical, Collections
+                .unmodifiableList(new ArrayList<SortKey>(keys)));
+    }
+
+    /**
+     * Creates a new server side sort request control with the provided
+     * criticality and list of sort keys.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param keys
+     *            The list of sort keys.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code keys} was empty.
+     * @throws NullPointerException
+     *             If {@code keys} was {@code null}.
+     */
+    public static ServerSideSortRequestControl newControl(final boolean isCritical,
+            final SortKey... keys) {
+        return newControl(isCritical, Arrays.asList(keys));
+    }
+
+    /**
+     * Creates a new server side sort request control with the provided
+     * criticality and string representation of a list of sort keys. The string
+     * representation is comprised of a comma separate list of sort keys as
+     * defined in {@link SortKey#valueOf(String)}. There must be at least one
+     * sort key present in the string representation.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param sortKeys
+     *            The list of sort keys.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code sortKeys} is not a valid string representation of a
+     *             list of sort keys.
+     * @throws NullPointerException
+     *             If {@code sortKeys} was {@code null}.
+     */
+    public static ServerSideSortRequestControl newControl(final boolean isCritical,
+            final String sortKeys) {
+        Reject.ifNull(sortKeys);
+
+        final List<SortKey> keys = new LinkedList<>();
+        final StringTokenizer tokenizer = new StringTokenizer(sortKeys, ",");
+        while (tokenizer.hasMoreTokens()) {
+            final String token = tokenizer.nextToken().trim();
+            keys.add(SortKey.valueOf(token));
+        }
+        if (keys.isEmpty()) {
+            final LocalizableMessage message = ERR_SORT_KEY_NO_SORT_KEYS.get(sortKeys);
+            throw new LocalizedIllegalArgumentException(message);
+        }
+        return new ServerSideSortRequestControl(isCritical, Collections.unmodifiableList(keys));
+    }
+
+    private final List<SortKey> sortKeys;
+
+    private final boolean isCritical;
+
+    private ServerSideSortRequestControl(final boolean isCritical, final List<SortKey> keys) {
+        this.isCritical = isCritical;
+        this.sortKeys = keys;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns an unmodifiable list containing the sort keys associated with
+     * this server side sort request control. The list will contain at least one
+     * sort key.
+     *
+     * @return An unmodifiable list containing the sort keys associated with
+     *         this server side sort request control.
+     */
+    public List<SortKey> getSortKeys() {
+        return sortKeys;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            for (final SortKey sortKey : sortKeys) {
+                writer.writeStartSequence();
+                writer.writeOctetString(sortKey.getAttributeDescription());
+
+                if (sortKey.getOrderingMatchingRule() != null) {
+                    writer.writeOctetString(TYPE_ORDERING_RULE_ID, sortKey
+                            .getOrderingMatchingRule());
+                }
+
+                if (sortKey.isReverseOrder()) {
+                    writer.writeBoolean(TYPE_REVERSE_ORDER, true);
+                }
+
+                writer.writeEndSequence();
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder();
+        buffer.append("ServerSideSortRequestControl(oid=");
+        buffer.append(getOID());
+        buffer.append(", criticality=");
+        buffer.append(isCritical());
+        buffer.append(", sortKeys=");
+        buffer.append(sortKeys);
+        buffer.append(")");
+        return buffer.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortResponseControl.java
new file mode 100644
index 0000000..2b7f412
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ServerSideSortResponseControl.java
@@ -0,0 +1,310 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SORTRES_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_SORTRES_CONTROL_CANNOT_DECODE_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_SORTRES_CONTROL_NO_VALUE;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Reject;
+
+/**
+ * The server-side sort response control as defined in RFC 2891. This control is
+ * included with a search result in response to a server-side sort request
+ * included with a search request. The client application is assured that the
+ * search results are sorted in the specified key order if and only if the
+ * result code in this control is success. If the server omits this control from
+ * the search result, the client SHOULD assume that the sort control was ignored
+ * by the server.
+ * <p>
+ * The following example demonstrates how to work with a server-side sort.
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request = Requests.newSearchRequest(
+ *         "ou=People,dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
+ *         .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("cn")));
+ *
+ * SearchResultHandler resultHandler = new MySearchResultHandler();
+ * Result result = connection.search(request, resultHandler);
+ *
+ * ServerSideSortResponseControl control = result.getControl(
+ *         ServerSideSortResponseControl.DECODER, new DecodeOptions());
+ * if (control != null && control.getResult() == ResultCode.SUCCESS) {
+ *     // Entries are sorted.
+ * } else {
+ *     // Entries not sorted.
+ * }
+ * </pre>
+ *
+ * @see ServerSideSortRequestControl
+ * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control
+ *      Extension for Server Side Sorting of Search Results </a>
+ */
+public final class ServerSideSortResponseControl implements Control {
+    /** The OID for the server-side sort response control. */
+    public static final String OID = "1.2.840.113556.1.4.474";
+
+    /** A decoder which can be used for decoding the server side sort response control. */
+    public static final ControlDecoder<ServerSideSortResponseControl> DECODER =
+            new ControlDecoder<ServerSideSortResponseControl>() {
+
+                @Override
+                public ServerSideSortResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control, options);
+
+                    if (control instanceof ServerSideSortResponseControl) {
+                        return (ServerSideSortResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_SORTRES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The request control must always have a value.
+                        final LocalizableMessage message = INFO_SORTRES_CONTROL_NO_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+
+                        // FIXME: should really check that result code is one of
+                        // the expected
+                        // values listed in the RFC.
+                        final ResultCode result = ResultCode.valueOf(reader.readEnumerated());
+
+                        AttributeDescription attributeDescription = null;
+                        if (reader.hasNextElement()) {
+                            // FIXME: which schema should we use?
+                            final Schema schema = options.getSchemaResolver().resolveSchema("");
+                            final String ads = reader.readOctetStringAsString();
+                            attributeDescription = AttributeDescription.valueOf(ads, schema);
+                        }
+
+                        return new ServerSideSortResponseControl(control.isCritical(), result,
+                                attributeDescription);
+                    } catch (final IOException | LocalizedIllegalArgumentException e) {
+                        final LocalizableMessage message =
+                                INFO_SORTRES_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /** The BER type to use when encoding the attribute type element. */
+    private static final byte TYPE_ATTRIBUTE_TYPE = (byte) 0x80;
+
+    /**
+     * Creates a new server-side response control with the provided sort result
+     * and no attribute description.
+     *
+     * @param result
+     *            The result code indicating the outcome of the server-side sort
+     *            request. {@link ResultCode#SUCCESS} if the search results were
+     *            sorted in accordance with the keys specified in the
+     *            server-side sort request control, or an error code indicating
+     *            why the results could not be sorted (such as
+     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
+     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static ServerSideSortResponseControl newControl(final ResultCode result) {
+        Reject.ifNull(result);
+
+        return new ServerSideSortResponseControl(false, result, null);
+    }
+
+    /**
+     * Creates a new server-side response control with the provided sort result
+     * and attribute description.
+     *
+     * @param result
+     *            The result code indicating the outcome of the server-side sort
+     *            request. {@link ResultCode#SUCCESS} if the search results were
+     *            sorted in accordance with the keys specified in the
+     *            server-side sort request control, or an error code indicating
+     *            why the results could not be sorted (such as
+     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
+     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
+     * @param attributeDescription
+     *            The first attribute description specified in the list of sort
+     *            keys that was in error, may be {@code null}.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static ServerSideSortResponseControl newControl(final ResultCode result,
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(result);
+
+        return new ServerSideSortResponseControl(false, result, attributeDescription);
+    }
+
+    /**
+     * Creates a new server-side response control with the provided sort result
+     * and attribute description. The attribute description will be decoded
+     * using the default schema.
+     *
+     * @param result
+     *            The result code indicating the outcome of the server-side sort
+     *            request. {@link ResultCode#SUCCESS} if the search results were
+     *            sorted in accordance with the keys specified in the
+     *            server-side sort request control, or an error code indicating
+     *            why the results could not be sorted (such as
+     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
+     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
+     * @param attributeDescription
+     *            The first attribute description specified in the list of sort
+     *            keys that was in error, may be {@code null}.
+     * @return The new control.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be parsed using the
+     *             default schema.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static ServerSideSortResponseControl newControl(final ResultCode result,
+            final String attributeDescription) {
+        Reject.ifNull(result);
+
+        if (attributeDescription != null) {
+            return new ServerSideSortResponseControl(false, result, AttributeDescription
+                    .valueOf(attributeDescription));
+        } else {
+            return new ServerSideSortResponseControl(false, result, null);
+        }
+    }
+
+    private final ResultCode result;
+
+    private final AttributeDescription attributeDescription;
+
+    private final boolean isCritical;
+
+    private ServerSideSortResponseControl(final boolean isCritical, final ResultCode result,
+            final AttributeDescription attributeDescription) {
+        this.isCritical = isCritical;
+        this.result = result;
+        this.attributeDescription = attributeDescription;
+    }
+
+    /**
+     * Returns the first attribute description specified in the list of sort
+     * keys that was in error, or {@code null} if the attribute description was
+     * not included with this control.
+     *
+     * @return The first attribute description specified in the list of sort
+     *         keys that was in error.
+     */
+    public AttributeDescription getAttributeDescription() {
+        return attributeDescription;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns a result code indicating the outcome of the server-side sort
+     * request. This will be {@link ResultCode#SUCCESS} if the search results
+     * were sorted in accordance with the keys specified in the server-side sort
+     * request control, or an error code indicating why the results could not be
+     * sorted (such as {@link ResultCode#NO_SUCH_ATTRIBUTE} or
+     * {@link ResultCode#INAPPROPRIATE_MATCHING}).
+     *
+     * @return The result code indicating the outcome of the server-side sort
+     *         request.
+     */
+    public ResultCode getResult() {
+        return result;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeEnumerated(result.intValue());
+            if (attributeDescription != null) {
+                writer.writeOctetString(TYPE_ATTRIBUTE_TYPE, attributeDescription.toString());
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ServerSideSortResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", result=");
+        builder.append(result);
+        if (attributeDescription != null) {
+            builder.append(", attributeDescription=");
+            builder.append(attributeDescription);
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SimplePagedResultsControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SimplePagedResultsControl.java
new file mode 100644
index 0000000..e79b58a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SimplePagedResultsControl.java
@@ -0,0 +1,315 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The simple paged results request and response control as defined in RFC 2696.
+ * This control allows a client to control the rate at which an LDAP server
+ * returns the results of an LDAP search operation. This control may be useful
+ * when the LDAP client has limited resources and may not be able to process the
+ * entire result set from a given LDAP query, or when the LDAP client is
+ * connected over a low-bandwidth connection.
+ * <p>
+ * This control is included in the searchRequest and searchResultDone messages
+ * and has the following structure:
+ *
+ * <pre>
+ * realSearchControlValue ::= SEQUENCE {
+ *         size            INTEGER (0..maxInt),
+ *                                 -- requested page size from client
+ *                                 -- result set size estimate from server
+ *         cookie          OCTET STRING
+ * }
+ * </pre>
+ * <p>
+ * The following example demonstrates use of simple paged results to handle
+ * three entries at a time.
+ *
+ * <pre>
+ * ByteString cookie = ByteString.empty();
+ * SearchRequest request;
+ * SearchResultHandler resultHandler = new MySearchResultHandler();
+ * Result result;
+ *
+ * int page = 1;
+ * do {
+        System.out.println("# Simple paged results: Page " + page);
+ *
+ *      request = Requests.newSearchRequest(
+                "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
+                .addControl(SimplePagedResultsControl.newControl(true, 3, cookie));
+ *
+ *      result = connection.search(request, resultHandler);
+ *      try {
+ *      SimplePagedResultsControl control = result.getControl(
+                SimplePagedResultsControl.DECODER, new DecodeOptions());
+        cookie = control.getCookie();
+        } catch (final DecodeException e) {
+            // Failed to decode the response control.
+        }
+ *
+ *      ++page;
+ * } while (cookie.length() != 0);
+ * </pre>
+ *
+ * The search result handler in this case displays pages of results as LDIF on
+ * standard out.
+ *
+ * <pre>
+ * private static class MySearchResultHandler implements SearchResultHandler {
+ *
+ *     {@literal @}Override
+ *     public void handleExceptionResult(LdapException error) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void handleResult(Result result) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleEntry(SearchResultEntry entry) {
+ *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+ *         try {
+ *             writer.writeEntry(entry);
+ *             writer.flush();
+ *         } catch (final IOException e) {
+ *             // The writer could not write to System.out.
+ *         }
+ *         return true;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleReference(SearchResultReference reference) {
+ *         System.out.println("Got a reference: " + reference.toString());
+ *         return false;
+ *     }
+ * }
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2696">RFC 2696 - LDAP Control
+ *      Extension for Simple Paged Results Manipulation </a>
+ */
+public final class SimplePagedResultsControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the paged results request/response control defined in RFC 2696. */
+    public static final String OID = "1.2.840.113556.1.4.319";
+
+    /** A decoder which can be used for decoding the simple paged results control. */
+    public static final ControlDecoder<SimplePagedResultsControl> DECODER =
+            new ControlDecoder<SimplePagedResultsControl>() {
+
+                @Override
+                public SimplePagedResultsControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof SimplePagedResultsControl) {
+                        return (SimplePagedResultsControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_CONTROL_BAD_OID.get(control.getOID(), OID));
+                    }
+
+                    if (!control.hasValue()) {
+                        // The control must always have a value.
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_NULL.get());
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+                    } catch (final Exception e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read start sequence", e));
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SEQUENCE.get(e), e);
+                    }
+
+                    int size;
+                    try {
+                        size = (int) reader.readInteger();
+                    } catch (final Exception e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read size", e));
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SIZE.get(e), e);
+                    }
+
+                    ByteString cookie;
+                    try {
+                        cookie = reader.readOctetString();
+                    } catch (final Exception e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read cookie", e));
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_COOKIE.get(e), e);
+                    }
+
+                    try {
+                        reader.readEndSequence();
+                    } catch (final Exception e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read end sequence", e));
+                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SEQUENCE.get(e), e);
+                    }
+
+                    return new SimplePagedResultsControl(control.isCritical(), size, cookie);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new simple paged results control with the provided criticality,
+     * size, and cookie.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param size
+     *            The requested page size when used in a request control from
+     *            the client, or an estimate of the result set size when used in
+     *            a response control from the server (may be 0, indicating that
+     *            the server does not know).
+     * @param cookie
+     *            An opaque cookie which is used by the server to track its
+     *            position in the set of search results. The cookie must be
+     *            empty in the initial search request sent by the client. For
+     *            subsequent search requests the client must include the cookie
+     *            returned with the previous search result, until the server
+     *            returns an empty cookie indicating that the final page of
+     *            results has been returned.
+     * @return The new control.
+     * @throws NullPointerException
+     *             If {@code cookie} was {@code null}.
+     */
+    public static SimplePagedResultsControl newControl(final boolean isCritical, final int size,
+            final ByteString cookie) {
+        Reject.ifNull(cookie);
+        return new SimplePagedResultsControl(isCritical, size, cookie);
+    }
+
+    /**
+     * The control value size element, which is either the requested page size
+     * from the client, or the result set size estimate from the server.
+     */
+    private final int size;
+
+    /** The control value cookie element. */
+    private final ByteString cookie;
+
+    private final boolean isCritical;
+
+    private SimplePagedResultsControl(final boolean isCritical, final int size,
+            final ByteString cookie) {
+        this.isCritical = isCritical;
+        this.size = size;
+        this.cookie = cookie;
+    }
+
+    /**
+     * Returns the opaque cookie which is used by the server to track its
+     * position in the set of search results. The cookie must be empty in the
+     * initial search request sent by the client. For subsequent search requests
+     * the client must include the cookie returned with the previous search
+     * result, until the server returns an empty cookie indicating that the
+     * final page of results has been returned.
+     *
+     * @return The opaque cookie which is used by the server to track its
+     *         position in the set of search results.
+     */
+    public ByteString getCookie() {
+        return cookie;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns the requested page size when used in a request control from the
+     * client, or an estimate of the result set size when used in a response
+     * control from the server (may be 0, indicating that the server does not
+     * know).
+     *
+     * @return The requested page size when used in a request control from the
+     *         client, or an estimate of the result set size when used in a
+     *         response control from the server.
+     */
+    public int getSize() {
+        return size;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeInteger(size);
+            writer.writeOctetString(cookie);
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SimplePagedResultsControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", size=");
+        builder.append(size);
+        builder.append(", cookie=");
+        builder.append(cookie);
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubentriesRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubentriesRequestControl.java
new file mode 100644
index 0000000..2e75358
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubentriesRequestControl.java
@@ -0,0 +1,228 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_CANNOT_DECODE_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBENTRIES_NO_CONTROL_VALUE;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The sub-entries request control as defined in RFC 3672. This control may be
+ * included in a search request to indicate that sub-entries should be included
+ * in the search results.
+ * <p>
+ * In the absence of the sub-entries request control, sub-entries are not
+ * visible to search operations unless the target/base of the operation is a
+ * sub-entry. In the presence of the sub-entry request control, sub-entries are
+ * visible if and only if the control's value is {@code TRUE}.
+ * <p>
+ * Consider "Class of Service" sub-entries such as the following:
+ *
+ * <pre>
+ * dn: cn=Gold Class of Service,dc=example,dc=com
+ * objectClass: collectiveAttributeSubentry
+ * objectClass: extensibleObject
+ * objectClass: subentry
+ * objectClass: top
+ * cn: Gold Class of Service
+ * diskQuota;collective: 100 GB
+ * mailQuota;collective: 10 GB
+ * subtreeSpecification: { base "ou=People", specificationFilter "(classOfService=
+ *  gold)" }
+ * </pre>
+ *
+ * To access the sub-entries in your search, use the control with value
+ * {@code TRUE}.
+ *
+ * <pre>
+ * Connection connection = ...;
+ *
+ * SearchRequest request = Requests.newSearchRequest("dc=example,dc=com",
+ *         SearchScope.WHOLE_SUBTREE, "cn=*Class of Service", "cn", "subtreeSpecification")
+ *         .addControl(SubentriesRequestControl.newControl(true, true));
+ *  
+ * ConnectionEntryReader reader = connection.search(request);
+ * while (reader.hasNext()) {
+ *     if (reader.isEntry()) {
+ *         SearchResultEntry entry = reader.readEntry();
+ *         // ...
+ *     }
+ * }
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3672">RFC 3672 - Subentries in
+ *      the Lightweight Directory Access Protocol </a>
+ */
+public final class SubentriesRequestControl implements Control {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    /** The OID for the sub-entries request control. */
+    public static final String OID = "1.3.6.1.4.1.4203.1.10.1";
+
+    private static final SubentriesRequestControl CRITICAL_VISIBLE_INSTANCE =
+            new SubentriesRequestControl(true, true);
+    private static final SubentriesRequestControl NONCRITICAL_VISIBLE_INSTANCE =
+            new SubentriesRequestControl(false, true);
+    private static final SubentriesRequestControl CRITICAL_INVISIBLE_INSTANCE =
+            new SubentriesRequestControl(true, false);
+    private static final SubentriesRequestControl NONCRITICAL_INVISIBLE_INSTANCE =
+            new SubentriesRequestControl(false, false);
+
+    /** A decoder which can be used for decoding the sub-entries request control. */
+    public static final ControlDecoder<SubentriesRequestControl> DECODER =
+            new ControlDecoder<SubentriesRequestControl>() {
+
+                @Override
+                public SubentriesRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof SubentriesRequestControl) {
+                        return (SubentriesRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_SUBENTRIES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = ERR_SUBENTRIES_NO_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    final boolean visibility;
+                    try {
+                        visibility = reader.readBoolean();
+                    } catch (final IOException e) {
+                        logger.debug(LocalizableMessage.raw("Unable to read visbility", e));
+                        final LocalizableMessage message =
+                                ERR_SUBENTRIES_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message);
+                    }
+
+                    return newControl(control.isCritical(), visibility);
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new sub-entries request control having the provided criticality
+     * and sub-entry visibility.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param visibility
+     *            {@code true} if sub-entries should be included in the search
+     *            results and normal entries excluded, or {@code false} if
+     *            normal entries should be included and sub-entries excluded.
+     * @return The new control.
+     */
+    public static SubentriesRequestControl newControl(final boolean isCritical,
+            final boolean visibility) {
+        if (isCritical) {
+            return visibility ? CRITICAL_VISIBLE_INSTANCE : CRITICAL_INVISIBLE_INSTANCE;
+        } else {
+            return visibility ? NONCRITICAL_VISIBLE_INSTANCE : NONCRITICAL_INVISIBLE_INSTANCE;
+        }
+    }
+
+    private final boolean isCritical;
+    private final boolean visibility;
+
+    private SubentriesRequestControl(final boolean isCritical, final boolean visibility) {
+        this.isCritical = isCritical;
+        this.visibility = visibility;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeBoolean(visibility);
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Returns a boolean indicating whether or not sub-entries should be
+     * included in the search results.
+     *
+     * @return {@code true} if sub-entries should be included in the search
+     *         results and normal entries excluded, or {@code false} if normal
+     *         entries should be included and sub-entries excluded.
+     */
+    public boolean getVisibility() {
+        return visibility;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SubentriesRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", visibility=");
+        builder.append(getVisibility());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubtreeDeleteRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubtreeDeleteRequestControl.java
new file mode 100644
index 0000000..ccf9bc3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/SubtreeDeleteRequestControl.java
@@ -0,0 +1,143 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBTREE_DELETE_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SUBTREE_DELETE_INVALID_CONTROL_VALUE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+
+import org.forgerock.util.Reject;
+
+/**
+ * The tree delete request control as defined in draft-armijo-ldap-treedelete.
+ * This control allows a client to delete an entire subtree of a container entry
+ * in a single delete operation.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String baseDN = ...;
+ *
+ * DeleteRequest request =
+ *         Requests.newDeleteRequest(baseDN)
+ *             .addControl(SubtreeDeleteRequestControl.newControl(true));
+ * connection.delete(request);
+ * </pre>
+ *
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-armijo-ldap-treedelete">draft-armijo-ldap-treedelete
+ *      - Tree Delete Control </a>
+ */
+public final class SubtreeDeleteRequestControl implements Control {
+    /** The OID for the subtree delete request control. */
+    public static final String OID = "1.2.840.113556.1.4.805";
+
+    private static final SubtreeDeleteRequestControl CRITICAL_INSTANCE =
+            new SubtreeDeleteRequestControl(true);
+
+    private static final SubtreeDeleteRequestControl NONCRITICAL_INSTANCE =
+            new SubtreeDeleteRequestControl(false);
+
+    /** A decoder which can be used for decoding the sub-tree delete request control. */
+    public static final ControlDecoder<SubtreeDeleteRequestControl> DECODER =
+            new ControlDecoder<SubtreeDeleteRequestControl>() {
+
+                @Override
+                public SubtreeDeleteRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof SubtreeDeleteRequestControl) {
+                        return (SubtreeDeleteRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_SUBTREE_DELETE_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (control.hasValue()) {
+                        final LocalizableMessage message =
+                                ERR_SUBTREE_DELETE_INVALID_CONTROL_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    return control.isCritical() ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new tree delete request control having the provided
+     * criticality.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @return The new control.
+     */
+    public static SubtreeDeleteRequestControl newControl(final boolean isCritical) {
+        return isCritical ? CRITICAL_INSTANCE : NONCRITICAL_INSTANCE;
+    }
+
+    private final boolean isCritical;
+
+    private SubtreeDeleteRequestControl(final boolean isCritical) {
+        this.isCritical = isCritical;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SubtreeDeleteRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewRequestControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewRequestControl.java
new file mode 100644
index 0000000..334a945
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewRequestControl.java
@@ -0,0 +1,483 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.byteToHex;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_VLVREQ_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_NO_VALUE;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Reject;
+
+/**
+ * The virtual list view request control as defined in
+ * draft-ietf-ldapext-ldapv3-vlv. This control allows a client to specify that
+ * the server return, for a given search request with associated sort keys, a
+ * contiguous subset of the search result set. This subset is specified in terms
+ * of offsets into the ordered list, or in terms of a greater than or equal
+ * assertion value.
+ * <p>
+ * This control must be used in conjunction with the server-side sort request
+ * control in order to ensure that results are returned in a consistent order.
+ * <p>
+ * This control is similar to the simple paged results request control, except
+ * that it allows the client to move backwards and forwards in the result set.
+ * <p>
+ * The following example demonstrates use of the virtual list view controls.
+ *
+ * <pre>
+ * ByteString contextID = ByteString.empty();
+ *
+ * // Add a window of 2 entries on either side of the first sn=Jensen entry.
+ * SearchRequest request = Requests.newSearchRequest("ou=People,dc=example,dc=com",
+ *          SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName")
+ *          .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("sn")))
+ *          .addControl(VirtualListViewRequestControl.newAssertionControl(
+ *                  true, ByteString.valueOf("Jensen"), 2, 2, contextID));
+ *
+ * SearchResultHandler resultHandler = new MySearchResultHandler();
+ * Result result = connection.search(request, resultHandler);
+ *
+ * ServerSideSortResponseControl sssControl =
+ *         result.getControl(ServerSideSortResponseControl.DECODER, new DecodeOptions());
+ * if (sssControl != null &amp;&amp; sssControl.getResult() == ResultCode.SUCCESS) {
+ *     // Entries are sorted.
+ * } else {
+ *     // Entries not necessarily sorted
+ * }
+ *
+ * VirtualListViewResponseControl vlvControl =
+ *         result.getControl(VirtualListViewResponseControl.DECODER, new DecodeOptions());
+ * // Position in list: vlvControl.getTargetPosition()/vlvControl.getContentCount()
+ * </pre>
+ *
+ * The search result handler in this case displays pages of results as LDIF on
+ * standard out.
+ *
+ * <pre>
+ * private static class MySearchResultHandler implements SearchResultHandler {
+ *
+ *     {@literal @}Override
+ *     public void handleExceptionResult(LdapException error) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void handleResult(Result result) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleEntry(SearchResultEntry entry) {
+ *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+ *         try {
+ *             writer.writeEntry(entry);
+ *             writer.flush();
+ *         } catch (final IOException e) {
+ *             // The writer could not write to System.out.
+ *         }
+ *         return true;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleReference(SearchResultReference reference) {
+ *         System.out.println("Got a reference: " + reference.toString());
+ *         return false;
+ *     }
+ * }
+ * </pre>
+ *
+ * @see VirtualListViewResponseControl
+ * @see ServerSideSortRequestControl
+ * @see <a href="http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv">
+ *      draft-ietf-ldapext-ldapv3-vlv - LDAP Extensions for Scrolling View
+ *      Browsing of Search Results </a>
+ */
+public final class VirtualListViewRequestControl implements Control {
+    /** The OID for the virtual list view request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.9";
+
+    /** A decoder which can be used for decoding the virtual list view request control. */
+    public static final ControlDecoder<VirtualListViewRequestControl> DECODER =
+            new ControlDecoder<VirtualListViewRequestControl>() {
+
+                @Override
+                public VirtualListViewRequestControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof VirtualListViewRequestControl) {
+                        return (VirtualListViewRequestControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_VLVREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The request control must always have a value.
+                        final LocalizableMessage message = INFO_VLVREQ_CONTROL_NO_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+
+                        final int beforeCount = (int) reader.readInteger();
+                        final int afterCount = (int) reader.readInteger();
+
+                        int offset = -1;
+                        int contentCount = -1;
+                        ByteString assertionValue = null;
+                        final byte targetType = reader.peekType();
+                        switch (targetType) {
+                        case TYPE_TARGET_BYOFFSET:
+                            reader.readStartSequence();
+                            offset = (int) reader.readInteger();
+                            contentCount = (int) reader.readInteger();
+                            reader.readEndSequence();
+                            break;
+                        case TYPE_TARGET_GREATERTHANOREQUAL:
+                            assertionValue = reader.readOctetString();
+                            break;
+                        default:
+                            final LocalizableMessage message =
+                                    INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE
+                                            .get(byteToHex(targetType));
+                            throw DecodeException.error(message);
+                        }
+
+                        ByteString contextID = null;
+                        if (reader.hasNextElement()) {
+                            contextID = reader.readOctetString();
+                        }
+
+                        return new VirtualListViewRequestControl(control.isCritical(), beforeCount,
+                                afterCount, contentCount, offset, assertionValue, contextID);
+                    } catch (final IOException e) {
+                        final LocalizableMessage message =
+                                INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /** The BER type to use when encoding the byOffset target element. */
+    private static final byte TYPE_TARGET_BYOFFSET = (byte) 0xA0;
+
+    /** The BER type to use when encoding the greaterThanOrEqual target element. */
+    private static final byte TYPE_TARGET_GREATERTHANOREQUAL = (byte) 0x81;
+
+    /**
+     * Creates a new virtual list view request control that will identify the
+     * target entry by an assertion value. The assertion value is encoded
+     * according to the ORDERING matching rule for the attribute description in
+     * the sort control. The assertion value is used to determine the target
+     * entry by comparison with the values of the attribute specified as the
+     * primary sort key. The first list entry who's value is no less than (less
+     * than or equal to when the sort order is reversed) the supplied value is
+     * the target entry.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param assertionValue
+     *            The assertion value that will be used to locate the target
+     *            entry.
+     * @param beforeCount
+     *            The number of entries before the target entry to be included
+     *            in the search results.
+     * @param afterCount
+     *            The number of entries after the target entry to be included in
+     *            the search results.
+     * @param contextID
+     *            The context ID provided by the server in the last virtual list
+     *            view response for the same set of criteria, or {@code null} if
+     *            there was no previous virtual list view response or the server
+     *            did not include a context ID in the last response.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code beforeCount} or {@code afterCount} were less than
+     *             {@code 0}.
+     * @throws NullPointerException
+     *             If {@code assertionValue} was {@code null}.
+     */
+    public static VirtualListViewRequestControl newAssertionControl(final boolean isCritical,
+            final ByteString assertionValue, final int beforeCount, final int afterCount,
+            final ByteString contextID) {
+        Reject.ifNull(assertionValue);
+        Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0");
+        Reject.ifFalse(afterCount >= 0, "afterCount is less than 0");
+
+        return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, -1, -1,
+                assertionValue, contextID);
+    }
+
+    /**
+     * Creates a new virtual list view request control that will identify the
+     * target entry by a positional offset within the complete result set.
+     *
+     * @param isCritical
+     *            {@code true} if it is unacceptable to perform the operation
+     *            without applying the semantics of this control, or
+     *            {@code false} if it can be ignored.
+     * @param offset
+     *            The positional offset of the target entry in the result set,
+     *            where {@code 1} is the first entry.
+     * @param contentCount
+     *            The content count returned by the server in the last virtual
+     *            list view response, or {@code 0} if this is the first virtual
+     *            list view request.
+     * @param beforeCount
+     *            The number of entries before the target entry to be included
+     *            in the search results.
+     * @param afterCount
+     *            The number of entries after the target entry to be included in
+     *            the search results.
+     * @param contextID
+     *            The context ID provided by the server in the last virtual list
+     *            view response for the same set of criteria, or {@code null} if
+     *            there was no previous virtual list view response or the server
+     *            did not include a context ID in the last response.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code beforeCount}, {@code afterCount}, or
+     *             {@code contentCount} were less than {@code 0}, or if
+     *             {@code offset} was less than {@code 1}.
+     */
+    public static VirtualListViewRequestControl newOffsetControl(final boolean isCritical,
+            final int offset, final int contentCount, final int beforeCount, final int afterCount,
+            final ByteString contextID) {
+        Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0");
+        Reject.ifFalse(afterCount >= 0, "afterCount is less than 0");
+        Reject.ifFalse(offset > 0, "offset is less than 1");
+        Reject.ifFalse(contentCount >= 0, "contentCount is less than 0");
+
+        return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, contentCount,
+                offset, null, contextID);
+    }
+
+    private final int beforeCount;
+
+    private final int afterCount;
+
+    private final ByteString contextID;
+
+    private final boolean isCritical;
+
+    private final int contentCount;
+
+    private final int offset;
+
+    private final ByteString assertionValue;
+
+    private VirtualListViewRequestControl(final boolean isCritical, final int beforeCount,
+            final int afterCount, final int contentCount, final int offset,
+            final ByteString assertionValue, final ByteString contextID) {
+        this.isCritical = isCritical;
+        this.beforeCount = beforeCount;
+        this.afterCount = afterCount;
+        this.contentCount = contentCount;
+        this.offset = offset;
+        this.assertionValue = assertionValue;
+        this.contextID = contextID;
+    }
+
+    /**
+     * Returns the number of entries after the target entry to be included in
+     * the search results.
+     *
+     * @return The number of entries after the target entry to be included in
+     *         the search results.
+     */
+    public int getAfterCount() {
+        return afterCount;
+    }
+
+    /**
+     * Returns the assertion value that will be used to locate the target entry,
+     * if applicable.
+     *
+     * @return The assertion value that will be used to locate the target entry,
+     *         or {@code null} if this control is using a target offset.
+     */
+    public ByteString getAssertionValue() {
+        return assertionValue;
+    }
+
+    /**
+     * Returns the assertion value that will be used to locate the target entry,
+     * if applicable, decoded as a UTF-8 string.
+     *
+     * @return The assertion value that will be used to locate the target entry
+     *         decoded as a UTF-8 string, or {@code null} if this control is
+     *         using a target offset.
+     */
+    public String getAssertionValueAsString() {
+        return assertionValue != null ? assertionValue.toString() : null;
+    }
+
+    /**
+     * Returns the number of entries before the target entry to be included in
+     * the search results.
+     *
+     * @return The number of entries before the target entry to be included in
+     *         the search results.
+     */
+    public int getBeforeCount() {
+        return beforeCount;
+    }
+
+    /**
+     * Returns the content count returned by the server in the last virtual list
+     * view response, if applicable.
+     *
+     * @return The content count returned by the server in the last virtual list
+     *         view response, which may be {@code 0} if this is the first
+     *         virtual list view request, or {@code -1} if this control is using
+     *         a target assertion.
+     */
+    public int getContentCount() {
+        return contentCount;
+    }
+
+    /**
+     * Returns the context ID provided by the server in the last virtual list
+     * view response for the same set of criteria, or {@code null} if there was
+     * no previous virtual list view response or the server did not include a
+     * context ID in the last response.
+     *
+     * @return The context ID provided by the server in the last virtual list
+     *         view response, or {@code null} if unavailable.
+     */
+    public ByteString getContextID() {
+        return contextID;
+    }
+
+    /**
+     * Returns the positional offset of the target entry in the result set, if
+     * applicable, where {@code 1} is the first entry.
+     *
+     * @return The positional offset of the target entry in the result set, or
+     *         {@code -1} if this control is using a target assertion.
+     */
+    public int getOffset() {
+        return offset;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeInteger(beforeCount);
+            writer.writeInteger(afterCount);
+            if (hasTargetOffset()) {
+                writer.writeStartSequence(TYPE_TARGET_BYOFFSET);
+                writer.writeInteger(offset);
+                writer.writeInteger(contentCount);
+                writer.writeEndSequence();
+            } else {
+                writer.writeOctetString(TYPE_TARGET_GREATERTHANOREQUAL, assertionValue);
+            }
+            if (contextID != null) {
+                writer.writeOctetString(contextID);
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this control is using a target offset, or
+     * {@code false} if this control is using a target assertion.
+     *
+     * @return {@code true} if this control is using a target offset, or
+     *         {@code false} if this control is using a target assertion.
+     */
+    public boolean hasTargetOffset() {
+        return assertionValue == null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("VirtualListViewRequestControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", beforeCount=");
+        builder.append(beforeCount);
+        builder.append(", afterCount=");
+        builder.append(afterCount);
+        if (hasTargetOffset()) {
+            builder.append(", offset=");
+            builder.append(offset);
+            builder.append(", contentCount=");
+            builder.append(contentCount);
+        } else {
+            builder.append(", greaterThanOrEqual=");
+            builder.append(assertionValue);
+        }
+        if (contextID != null) {
+            builder.append(", contextID=");
+            builder.append(contextID);
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewResponseControl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewResponseControl.java
new file mode 100644
index 0000000..908b582
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/VirtualListViewResponseControl.java
@@ -0,0 +1,322 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.controls;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_VLVRES_CONTROL_BAD_OID;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVRES_CONTROL_CANNOT_DECODE_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVRES_CONTROL_NO_VALUE;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.util.Reject;
+
+/**
+ * The virtual list view response control as defined in
+ * draft-ietf-ldapext-ldapv3-vlv. This control is included with a search result
+ * in response to a virtual list view request included with a search request.
+ * <p>
+ * If the result code included with this control indicates that the virtual list
+ * view request succeeded then the content count and target position give
+ * sufficient information for the client to update a list box slider position to
+ * match the newly retrieved entries and identify the target entry.
+ * <p>
+ * The content count and context ID should be used in a subsequent virtual list
+ * view requests.
+ * <p>
+ * The following example demonstrates use of the virtual list view controls.
+ *
+ * <pre>
+ * ByteString contextID = ByteString.empty();
+ *
+ * // Add a window of 2 entries on either side of the first sn=Jensen entry.
+ * SearchRequest request = Requests.newSearchRequest("ou=People,dc=example,dc=com",
+ *          SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName")
+ *          .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("sn")))
+ *          .addControl(VirtualListViewRequestControl.newAssertionControl(
+ *                  true, ByteString.valueOf("Jensen"), 2, 2, contextID));
+ *
+ * SearchResultHandler resultHandler = new MySearchResultHandler();
+ * Result result = connection.search(request, resultHandler);
+ *
+ * ServerSideSortResponseControl sssControl =
+ *         result.getControl(ServerSideSortResponseControl.DECODER, new DecodeOptions());
+ * if (sssControl != null &amp;&amp; sssControl.getResult() == ResultCode.SUCCESS) {
+ *     // Entries are sorted.
+ * } else {
+ *     // Entries not necessarily sorted
+ * }
+ *
+ * VirtualListViewResponseControl vlvControl =
+ *         result.getControl(VirtualListViewResponseControl.DECODER, new DecodeOptions());
+ * // Position in list: vlvControl.getTargetPosition()/vlvControl.getContentCount()
+ * </pre>
+ *
+ * The search result handler in this case displays pages of results as LDIF on
+ * standard out.
+ *
+ * <pre>
+ * private static class MySearchResultHandler implements SearchResultHandler {
+ *
+ *     {@literal @}Override
+ *     public void handleExceptionResult(LdapException error) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void handleResult(Result result) {
+ *         // Ignore.
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleEntry(SearchResultEntry entry) {
+ *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+ *         try {
+ *             writer.writeEntry(entry);
+ *             writer.flush();
+ *         } catch (final IOException e) {
+ *             // The writer could not write to System.out.
+ *         }
+ *         return true;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public boolean handleReference(SearchResultReference reference) {
+ *         System.out.println("Got a reference: " + reference.toString());
+ *         return false;
+ *     }
+ * }
+ * </pre>
+ *
+ * @see VirtualListViewRequestControl
+ * @see <a href="http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv">
+ *      draft-ietf-ldapext-ldapv3-vlv - LDAP Extensions for Scrolling View
+ *      Browsing of Search Results </a>
+ */
+public final class VirtualListViewResponseControl implements Control {
+    /** The OID for the virtual list view request control. */
+    public static final String OID = "2.16.840.1.113730.3.4.10";
+
+    /** A decoder which can be used for decoding the virtual list view response control. */
+    public static final ControlDecoder<VirtualListViewResponseControl> DECODER =
+            new ControlDecoder<VirtualListViewResponseControl>() {
+
+                @Override
+                public VirtualListViewResponseControl decodeControl(final Control control,
+                        final DecodeOptions options) throws DecodeException {
+                    Reject.ifNull(control);
+
+                    if (control instanceof VirtualListViewResponseControl) {
+                        return (VirtualListViewResponseControl) control;
+                    }
+
+                    if (!control.getOID().equals(OID)) {
+                        final LocalizableMessage message =
+                                ERR_VLVRES_CONTROL_BAD_OID.get(control.getOID(), OID);
+                        throw DecodeException.error(message);
+                    }
+
+                    if (!control.hasValue()) {
+                        // The response control must always have a value.
+                        final LocalizableMessage message = INFO_VLVRES_CONTROL_NO_VALUE.get();
+                        throw DecodeException.error(message);
+                    }
+
+                    final ASN1Reader reader = ASN1.getReader(control.getValue());
+                    try {
+                        reader.readStartSequence();
+
+                        final int targetPosition = (int) reader.readInteger();
+                        final int contentCount = (int) reader.readInteger();
+                        final ResultCode result = ResultCode.valueOf(reader.readEnumerated());
+                        ByteString contextID = null;
+                        if (reader.hasNextElement()) {
+                            contextID = reader.readOctetString();
+                        }
+
+                        return new VirtualListViewResponseControl(control.isCritical(),
+                                targetPosition, contentCount, result, contextID);
+                    } catch (final IOException e) {
+                        final LocalizableMessage message =
+                                INFO_VLVRES_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+                }
+
+                @Override
+                public String getOID() {
+                    return OID;
+                }
+            };
+
+    /**
+     * Creates a new virtual list view response control.
+     *
+     * @param targetPosition
+     *            The position of the target entry in the result set.
+     * @param contentCount
+     *            An estimate of the total number of entries in the result set.
+     * @param result
+     *            The result code indicating the outcome of the virtual list
+     *            view request.
+     * @param contextID
+     *            A server-defined octet string. If present, the contextID
+     *            should be sent back to the server by the client in a
+     *            subsequent virtual list request.
+     * @return The new control.
+     * @throws IllegalArgumentException
+     *             If {@code targetPosition} or {@code contentCount} were less
+     *             than {@code 0}.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static VirtualListViewResponseControl newControl(final int targetPosition,
+            final int contentCount, final ResultCode result, final ByteString contextID) {
+        Reject.ifNull(result);
+        Reject.ifFalse(targetPosition >= 0, "targetPosition is less than 0");
+        Reject.ifFalse(contentCount >= 0, "contentCount is less than 0");
+
+        return new VirtualListViewResponseControl(false, targetPosition, contentCount, result,
+                contextID);
+    }
+
+    private final int targetPosition;
+
+    private final int contentCount;
+
+    private final ResultCode result;
+
+    private final ByteString contextID;
+
+    private final boolean isCritical;
+
+    private VirtualListViewResponseControl(final boolean isCritical, final int targetPosition,
+            final int contentCount, final ResultCode result, final ByteString contextID) {
+        this.isCritical = isCritical;
+        this.targetPosition = targetPosition;
+        this.contentCount = contentCount;
+        this.result = result;
+        this.contextID = contextID;
+    }
+
+    /**
+     * Returns the estimated total number of entries in the result set.
+     *
+     * @return The estimated total number of entries in the result set.
+     */
+    public int getContentCount() {
+        return contentCount;
+    }
+
+    /**
+     * Returns a server-defined octet string which, if present, should be sent
+     * back to the server by the client in a subsequent virtual list request.
+     *
+     * @return A server-defined octet string which, if present, should be sent
+     *         back to the server by the client in a subsequent virtual list
+     *         request, or {@code null} if there is no context ID.
+     */
+    public ByteString getContextID() {
+        return contextID;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    /**
+     * Returns result code indicating the outcome of the virtual list view
+     * request.
+     *
+     * @return The result code indicating the outcome of the virtual list view
+     *         request.
+     */
+    public ResultCode getResult() {
+        return result;
+    }
+
+    /**
+     * Returns the position of the target entry in the result set.
+     *
+     * @return The position of the target entry in the result set.
+     */
+    public int getTargetPosition() {
+        return targetPosition;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+        try {
+            writer.writeStartSequence();
+            writer.writeInteger(targetPosition);
+            writer.writeInteger(contentCount);
+            writer.writeEnumerated(result.intValue());
+            if (contextID != null) {
+                writer.writeOctetString(contextID);
+            }
+            writer.writeEndSequence();
+            return buffer.toByteString();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public boolean isCritical() {
+        return isCritical;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("VirtualListViewResponseControl(oid=");
+        builder.append(getOID());
+        builder.append(", criticality=");
+        builder.append(isCritical());
+        builder.append(", targetPosition=");
+        builder.append(targetPosition);
+        builder.append(", contentCount=");
+        builder.append(contentCount);
+        builder.append(", result=");
+        builder.append(result);
+        if (contextID != null) {
+            builder.append(", contextID=");
+            builder.append(contextID);
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/package-info.java
new file mode 100644
index 0000000..8537df7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for common LDAP controls.
+ */
+package org.forgerock.opendj.ldap.controls;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/package-info.java
new file mode 100644
index 0000000..b2139bd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for core types including connections, entries, and
+ * attributes.
+ */
+package org.forgerock.opendj.ldap;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequest.java
new file mode 100644
index 0000000..351c4fc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequest.java
@@ -0,0 +1,64 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Abandon operation allows a client to request that the server abandon an
+ * uncompleted operation.
+ * <p>
+ * Abandon, Bind, Unbind, and StartTLS operations cannot be abandoned.
+ */
+public interface AbandonRequest extends Request {
+
+    @Override
+    AbandonRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the request ID of the request to be abandoned.
+     *
+     * @return The request ID of the request to be abandoned.
+     */
+    int getRequestID();
+
+    /**
+     * Sets the request ID of the request to be abandoned.
+     *
+     * @param id
+     *            The request ID of the request to be abandoned.
+     * @return This abandon request.
+     * @throws UnsupportedOperationException
+     *             If this abandon request does not permit the request ID to be
+     *             set.
+     */
+    AbandonRequest setRequestID(int id);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequestImpl.java
new file mode 100644
index 0000000..5f10671
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbandonRequestImpl.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Abandon request implementation.
+ */
+final class AbandonRequestImpl extends AbstractRequestImpl<AbandonRequest> implements
+        AbandonRequest {
+    private int requestID;
+
+    AbandonRequestImpl(final AbandonRequest abandonRequest) {
+        super(abandonRequest);
+        this.requestID = abandonRequest.getRequestID();
+    }
+
+    AbandonRequestImpl(final int requestID) {
+        this.requestID = requestID;
+    }
+
+    @Override
+    public int getRequestID() {
+        return requestID;
+    }
+
+    @Override
+    public AbandonRequest setRequestID(final int id) {
+        this.requestID = id;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AbandonRequest(requestID=");
+        builder.append(getRequestID());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    AbandonRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractBindRequest.java
new file mode 100644
index 0000000..89c11e7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractBindRequest.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * An abstract Bind request which can be used as the basis for implementing new
+ * authentication methods.
+ *
+ * @param <R>
+ *            The type of Bind request.
+ */
+abstract class AbstractBindRequest<R extends BindRequest> extends AbstractRequestImpl<R> implements
+        BindRequest {
+
+    AbstractBindRequest() {
+        // Nothing to do.
+    }
+
+    AbstractBindRequest(final BindRequest bindRequest) {
+        super(bindRequest);
+    }
+
+    @Override
+    public abstract String getName();
+
+    @Override
+    @SuppressWarnings("unchecked")
+    final R getThis() {
+        return (R) this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractExtendedRequest.java
new file mode 100644
index 0000000..474c9cb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractExtendedRequest.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * An abstract Extended request which can be used as the basis for implementing
+ * new Extended operations.
+ *
+ * @param <R>
+ *            The type of extended request.
+ * @param <S>
+ *            The type of result.
+ */
+public abstract class AbstractExtendedRequest<R extends ExtendedRequest<S>, S extends ExtendedResult>
+        extends AbstractRequestImpl<R> implements ExtendedRequest<S> {
+
+    /**
+     * Creates a new abstract extended request.
+     */
+    protected AbstractExtendedRequest() {
+        // Nothing to do.
+    }
+
+    /**
+     * Creates a new extended request that is an exact copy of the provided
+     * request.
+     *
+     * @param extendedRequest
+     *            The extended request to be copied.
+     * @throws NullPointerException
+     *             If {@code extendedRequest} was {@code null} .
+     */
+    protected AbstractExtendedRequest(final ExtendedRequest<S> extendedRequest) {
+        super(extendedRequest);
+    }
+
+    @Override
+    public abstract String getOID();
+
+    @Override
+    public abstract ExtendedResultDecoder<S> getResultDecoder();
+
+    @Override
+    public abstract ByteString getValue();
+
+    @Override
+    public abstract boolean hasValue();
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ExtendedRequest(requestName=");
+        builder.append(getOID());
+        if (hasValue()) {
+            builder.append(", requestValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    final R getThis() {
+        return (R) this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
new file mode 100644
index 0000000..084bc49
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
@@ -0,0 +1,95 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.util.Reject;
+
+/**
+ * Abstract request implementation.
+ *
+ * @param <R>
+ *            The type of request.
+ */
+abstract class AbstractRequestImpl<R extends Request> implements Request {
+
+    /** Used by unmodifiable implementations as well. */
+    static Control getControl(final List<Control> controls, final String oid) {
+        // Avoid creating an iterator if possible.
+        if (!controls.isEmpty()) {
+            for (final Control control : controls) {
+                if (control.getOID().equals(oid)) {
+                    return control;
+                }
+            }
+        }
+        return null;
+    }
+
+    private final List<Control> controls = new LinkedList<>();
+
+    AbstractRequestImpl() {
+        // No implementation required.
+    }
+
+    AbstractRequestImpl(final Request request) {
+        Reject.ifNull(request);
+        for (final Control control : request.getControls()) {
+            // Create defensive copy.
+            controls.add(GenericControl.newControl(control));
+        }
+    }
+
+    @Override
+    public final R addControl(final Control control) {
+        Reject.ifNull(control);
+        controls.add(control);
+        return getThis();
+    }
+
+    @Override
+    public boolean containsControl(final String oid) {
+        return getControl(controls, oid) != null;
+    }
+
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Reject.ifNull(decoder, options);
+        final Control control = getControl(controls, decoder.getOID());
+        return control != null ? decoder.decodeControl(control, options) : null;
+    }
+
+    @Override
+    public final List<Control> getControls() {
+        return controls;
+    }
+
+    @Override
+    public abstract String toString();
+
+    abstract R getThis();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractSASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractSASLBindRequest.java
new file mode 100644
index 0000000..f3a2e3b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractSASLBindRequest.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.io.LDAP;
+
+/**
+ * An abstract SASL Bind request which can be used as the basis for implementing
+ * new SASL authentication methods.
+ *
+ * @param <R>
+ *            The type of SASL Bind request.
+ */
+abstract class AbstractSASLBindRequest<R extends SASLBindRequest> extends AbstractBindRequest<R>
+        implements SASLBindRequest {
+
+    AbstractSASLBindRequest() {
+
+    }
+
+    AbstractSASLBindRequest(final SASLBindRequest saslBindRequest) {
+        super(saslBindRequest);
+    }
+
+    @Override
+    public final byte getAuthenticationType() {
+        return LDAP.TYPE_AUTHENTICATION_SASL;
+    }
+
+    @Override
+    public final String getName() {
+        return "".intern();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableBindRequest.java
new file mode 100644
index 0000000..1edbf4f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableBindRequest.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.LdapException;
+
+/**
+ * An abstract unmodifiable Bind request which can be used as the basis for
+ * implementing new unmodifiable authentication methods.
+ *
+ * @param <R>
+ *            The type of Bind request.
+ */
+abstract class AbstractUnmodifiableBindRequest<R extends BindRequest> extends
+        AbstractUnmodifiableRequest<R> implements BindRequest {
+
+    AbstractUnmodifiableBindRequest(final R impl) {
+        super(impl);
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return impl.createBindClient(serverName);
+    }
+
+    @Override
+    public byte getAuthenticationType() {
+        return impl.getAuthenticationType();
+    }
+
+    @Override
+    public String getName() {
+        return impl.getName();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableExtendedRequest.java
new file mode 100644
index 0000000..09e971e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableExtendedRequest.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * An abstract unmodifiable Extended request which can be used as the basis for
+ * implementing new unmodifiable Extended operations.
+ *
+ * @param <R>
+ *            The type of extended request.
+ * @param <S>
+ *            The type of result.
+ */
+abstract class AbstractUnmodifiableExtendedRequest<R extends ExtendedRequest<S>, S extends ExtendedResult>
+        extends AbstractUnmodifiableRequest<R> implements ExtendedRequest<S> {
+    AbstractUnmodifiableExtendedRequest(final R impl) {
+        super(impl);
+    }
+
+    @Override
+    public final String getOID() {
+        return impl.getOID();
+    }
+
+    @Override
+    public final ExtendedResultDecoder<S> getResultDecoder() {
+        return impl.getResultDecoder();
+    }
+
+    @Override
+    public final ByteString getValue() {
+        return impl.getValue();
+    }
+
+    @Override
+    public final boolean hasValue() {
+        return impl.hasValue();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
new file mode 100644
index 0000000..c744b82
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
@@ -0,0 +1,109 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Functions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Collections2;
+
+/**
+ * Unmodifiable request implementation.
+ *
+ * @param <R>
+ *            The type of request.
+ */
+abstract class AbstractUnmodifiableRequest<R extends Request> implements Request {
+
+    protected final R impl;
+
+    AbstractUnmodifiableRequest(final R impl) {
+        this.impl = impl;
+    }
+
+    @Override
+    public final R addControl(final Control control) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean containsControl(final String oid) {
+        return impl.containsControl(oid);
+    }
+
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Reject.ifNull(decoder, options);
+
+        final List<Control> controls = impl.getControls();
+        final Control control = AbstractRequestImpl.getControl(controls, decoder.getOID());
+        if (control == null) {
+            return null;
+        }
+
+        // Got a match. Return a defensive copy only if necessary.
+        final C decodedControl = decoder.decodeControl(control, options);
+        if (decodedControl != control) {
+            // This was not the original control so return it
+            // immediately.
+            return decodedControl;
+        } else if (decodedControl instanceof GenericControl) {
+            // Generic controls are immutable, so return it immediately.
+            return decodedControl;
+        } else {
+            // Re-decode to get defensive copy.
+            final GenericControl genericControl = GenericControl.newControl(control);
+            return decoder.decodeControl(genericControl, options);
+        }
+    }
+
+    @Override
+    public final List<Control> getControls() {
+        // We need to make all controls unmodifiable as well, which implies
+        // making defensive copies where necessary.
+        final Function<Control, Control, NeverThrowsException> function =
+                new Function<Control, Control, NeverThrowsException>() {
+                    @Override
+                    public Control apply(final Control value) {
+                        // Return defensive copy.
+                        return GenericControl.newControl(value);
+                    }
+                };
+
+        final List<Control> unmodifiableControls =
+                Collections2.transformedList(impl.getControls(), function, Functions
+                        .<Control> identityFunction());
+        return Collections.unmodifiableList(unmodifiableControls);
+    }
+
+    @Override
+    public final String toString() {
+        return impl.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableSASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableSASLBindRequest.java
new file mode 100644
index 0000000..3f31bfb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableSASLBindRequest.java
@@ -0,0 +1,37 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * An abstract unmodifiable SASL Bind request which can be used as the basis for
+ * implementing new unmodifiable SASL authentication methods.
+ *
+ * @param <R>
+ *            The type of SASL Bind request.
+ */
+abstract class AbstractUnmodifiableSASLBindRequest<R extends SASLBindRequest> extends
+        AbstractUnmodifiableBindRequest<R> implements SASLBindRequest {
+
+    AbstractUnmodifiableSASLBindRequest(final R impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return impl.getSASLMechanism();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequest.java
new file mode 100644
index 0000000..e7d5b65
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequest.java
@@ -0,0 +1,119 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * The Add operation allows a client to request the addition of an entry into
+ * the Directory.
+ * <p>
+ * The RDN attribute(s) may or may not be included in the Add request.
+ * NO-USER-MODIFICATION attributes such as the {@code createTimestamp} or
+ * {@code creatorsName} attributes must not be included, since the server
+ * maintains these automatically.
+ */
+public interface AddRequest extends Request, ChangeRecord, Entry {
+
+    @Override
+    <R, P> R accept(ChangeRecordVisitor<R, P> v, P p);
+
+    @Override
+    boolean addAttribute(Attribute attribute);
+
+    @Override
+    boolean addAttribute(Attribute attribute, Collection<? super ByteString> duplicateValues);
+
+    @Override
+    AddRequest addAttribute(String attributeDescription, Object... values);
+
+    @Override
+    AddRequest addControl(Control control);
+
+    @Override
+    AddRequest clearAttributes();
+
+    @Override
+    boolean containsAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    @Override
+    boolean containsAttribute(String attributeDescription, Object... values);
+
+    @Override
+    Iterable<Attribute> getAllAttributes();
+
+    @Override
+    Iterable<Attribute> getAllAttributes(AttributeDescription attributeDescription);
+
+    @Override
+    Iterable<Attribute> getAllAttributes(String attributeDescription);
+
+    @Override
+    Attribute getAttribute(AttributeDescription attributeDescription);
+
+    @Override
+    Attribute getAttribute(String attributeDescription);
+
+    @Override
+    int getAttributeCount();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    DN getName();
+
+    @Override
+    boolean removeAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    @Override
+    boolean removeAttribute(AttributeDescription attributeDescription);
+
+    @Override
+    AddRequest removeAttribute(String attributeDescription, Object... values);
+
+    @Override
+    boolean replaceAttribute(Attribute attribute);
+
+    @Override
+    AddRequest replaceAttribute(String attributeDescription, Object... values);
+
+    @Override
+    AddRequest setName(DN dn);
+
+    @Override
+    AddRequest setName(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java
new file mode 100644
index 0000000..9660f21
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java
@@ -0,0 +1,198 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * Add request implementation.
+ */
+final class AddRequestImpl extends AbstractRequestImpl<AddRequest> implements AddRequest {
+    private final Entry entry;
+
+    AddRequestImpl(final AddRequest addRequest) {
+        super(addRequest);
+        this.entry = LinkedHashMapEntry.deepCopyOfEntry(addRequest);
+    }
+
+    AddRequestImpl(final Entry entry) {
+        this.entry = entry;
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute) {
+        return entry.addAttribute(attribute);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute,
+            final Collection<? super ByteString> duplicateValues) {
+        return entry.addAttribute(attribute, duplicateValues);
+    }
+
+    @Override
+    public AddRequest addAttribute(final String attributeDescription, final Object... values) {
+        entry.addAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public AddRequest clearAttributes() {
+        entry.clearAttributes();
+        return this;
+    }
+
+    @Override
+    public boolean containsAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return entry.containsAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean containsAttribute(final String attributeDescription, final Object... values) {
+        return entry.containsAttribute(attributeDescription, values);
+    }
+
+    @Override
+    public boolean equals(final Object object) {
+        return entry.equals(object);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes() {
+        return entry.getAllAttributes();
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+        return entry.getAllAttributes(attributeDescription);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+        return entry.getAllAttributes(attributeDescription);
+    }
+
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        return entry.getAttribute(attributeDescription);
+    }
+
+    @Override
+    public Attribute getAttribute(final String attributeDescription) {
+        return entry.getAttribute(attributeDescription);
+    }
+
+    @Override
+    public int getAttributeCount() {
+        return entry.getAttributeCount();
+    }
+
+    @Override
+    public DN getName() {
+        return entry.getName();
+    }
+
+    @Override
+    public int hashCode() {
+        return entry.hashCode();
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return entry.removeAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean removeAttribute(final AttributeDescription attributeDescription) {
+        return entry.removeAttribute(attributeDescription);
+    }
+
+    @Override
+    public AddRequest removeAttribute(final String attributeDescription, final Object... values) {
+        entry.removeAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public boolean replaceAttribute(final Attribute attribute) {
+        return entry.replaceAttribute(attribute);
+    }
+
+    @Override
+    public AddRequest replaceAttribute(final String attributeDescription, final Object... values) {
+        entry.replaceAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public AddRequest setName(final DN dn) {
+        entry.setName(dn);
+        return this;
+    }
+
+    @Override
+    public AddRequest setName(final String dn) {
+        entry.setName(dn);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AddRequest(name=");
+        builder.append(getName());
+        builder.append(", attributes=");
+        builder.append(getAllAttributes());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    AddRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequest.java
new file mode 100644
index 0000000..bda1bd6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequest.java
@@ -0,0 +1,113 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The anonymous SASL bind request as defined in RFC 4505. This SASL mechanism
+ * allows a client to authenticate to the server without requiring the user to
+ * establish or otherwise disclose their identity to the server. That is, this
+ * mechanism provides an anonymous login method. This mechanism does not provide
+ * a security layer.
+ * <p>
+ * Clients should provide trace information, which has no semantic value, and
+ * can be used by administrators in order to identify the user. It should take
+ * one of two forms: an Internet email address, or an opaque string that does
+ * not contain the '@' (U+0040) character and that can be interpreted by the
+ * system administrator of the client's domain. For privacy reasons, an Internet
+ * email address or other information identifying the user should only be used
+ * with permission from the user.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4505">RFC 4505 - Anonymous Simple
+ *      Authentication and Security Layer (SASL) Mechanism </a>
+ */
+public interface AnonymousSASLBindRequest extends SASLBindRequest {
+
+    /**
+     * The name of the SASL mechanism that does not provide any authentication
+     * but rather uses anonymous access.
+     */
+    String SASL_MECHANISM_NAME = "ANONYMOUS";
+
+    @Override
+    AnonymousSASLBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Returns the trace information, which has no semantic value, and can be
+     * used by administrators in order to identify the user.
+     *
+     * @return The trace information, which has no semantic value, and can be
+     *         used by administrators in order to identify the user.
+     */
+    String getTraceString();
+
+    /**
+     * Sets the trace information, which has no semantic value, and can be used
+     * by administrators in order to identify the user.
+     *
+     * @param traceString
+     *            The trace information, which has no semantic value, and can be
+     *            used by administrators in order to identify the user.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this anonymous SASL request does not permit the trace
+     *             information to be set.
+     * @throws NullPointerException
+     *             If {@code traceString} was {@code null}.
+     */
+    AnonymousSASLBindRequest setTraceString(String traceString);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestImpl.java
new file mode 100644
index 0000000..5f858ac
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestImpl.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.util.Reject;
+
+/**
+ * Anonymous SASL bind request implementation.
+ */
+final class AnonymousSASLBindRequestImpl extends AbstractSASLBindRequest<AnonymousSASLBindRequest>
+        implements AnonymousSASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private Client(final AnonymousSASLBindRequestImpl initialBindRequest) {
+            super(initialBindRequest);
+            setNextSASLCredentials(ByteString.valueOfUtf8(initialBindRequest.getTraceString()));
+        }
+    }
+
+    private String traceString;
+
+    AnonymousSASLBindRequestImpl(final AnonymousSASLBindRequest anonymousSASLBindRequest) {
+        super(anonymousSASLBindRequest);
+        this.traceString = anonymousSASLBindRequest.getTraceString();
+    }
+
+    AnonymousSASLBindRequestImpl(final String traceString) {
+        Reject.ifNull(traceString);
+        this.traceString = traceString;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) {
+        return new Client(this);
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public String getTraceString() {
+        return traceString;
+    }
+
+    @Override
+    public AnonymousSASLBindRequest setTraceString(final String traceString) {
+        Reject.ifNull(traceString);
+        this.traceString = traceString;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AnonymousSASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        builder.append(", traceString=");
+        builder.append(traceString);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClient.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClient.java
new file mode 100644
index 0000000..fcd203b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClient.java
@@ -0,0 +1,78 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.responses.BindResult;
+
+/**
+ * An authentication client which can be used to bind to a server. Specifically,
+ * a bind client manages the state associated with multi-stage authentication
+ * attempts and responds to any challenges returned by the server.
+ */
+public interface BindClient {
+    /**
+     * Disposes of any system resources or security-sensitive information that
+     * this bind client might be using. Invoking this method invalidates this
+     * instance.
+     */
+    void dispose();
+
+    /**
+     * Evaluates the provided bind result and returns {@code true} if
+     * authentication has completed successfully, or {@code false} if additional
+     * authentication steps are required (for example during a multi-stage SASL
+     * authentication attempt).
+     * <p>
+     * If additional steps are required then implementations must update their
+     * internal state based on information contained in the bind result (for
+     * example, using the server provided SASL credentials).
+     *
+     * @param result
+     *            The bind result to be evaluated.
+     * @return {@code true} if authentication has completed successfully, of
+     *         {@code false} if additional steps are required.
+     * @throws LdapException
+     *             If the evaluation failed for some reason and authentication
+     *             cannot continue.
+     */
+    boolean evaluateResult(BindResult result) throws LdapException;
+
+    /**
+     * Returns a connection security layer, but only if this bind client has
+     * negotiated integrity and/or privacy protection for the underlying
+     * connection. This method should only be called once authentication has
+     * completed.
+     *
+     * @return A connection security layer, or {@code null} if none was
+     *         negotiated.
+     */
+    ConnectionSecurityLayer getConnectionSecurityLayer();
+
+    /**
+     * Returns the next bind request which should be used for the next stage of
+     * authentication. Initially, this will be a copy of the original bind
+     * request used to create this bind client.
+     *
+     * @return The next bind request which should be used for the next stage of
+     *         authentication.
+     */
+    GenericBindRequest nextBindRequest();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClientImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClientImpl.java
new file mode 100644
index 0000000..ba4a06b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindClientImpl.java
@@ -0,0 +1,104 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.BindResult;
+
+/**
+ * Bind client implementation.
+ */
+class BindClientImpl implements BindClient, ConnectionSecurityLayer {
+    private final GenericBindRequest nextBindRequest;
+
+    BindClientImpl(final BindRequest initialBindRequest) {
+        this.nextBindRequest =
+                new GenericBindRequestImpl(initialBindRequest.getName(), initialBindRequest
+                        .getAuthenticationType(), new byte[0], this);
+        for (final Control control : initialBindRequest.getControls()) {
+            this.nextBindRequest.addControl(control);
+        }
+    }
+
+    /**
+     * Default implementation does nothing.
+     */
+    @Override
+    public void dispose() {
+        // Do nothing.
+    }
+
+    /**
+     * Default implementation does nothing and always returns {@code true}.
+     */
+    @Override
+    public boolean evaluateResult(final BindResult result) throws LdapException {
+        return true;
+    }
+
+    /**
+     * Default implementation always returns {@code null}.
+     */
+    @Override
+    public ConnectionSecurityLayer getConnectionSecurityLayer() {
+        return null;
+    }
+
+    /**
+     * Returns the next bind request.
+     */
+    @Override
+    public final GenericBindRequest nextBindRequest() {
+        return nextBindRequest;
+    }
+
+    /**
+     * Default implementation just returns the copy of the bytes.
+     */
+    @Override
+    public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws LdapException {
+        final byte[] copy = new byte[len];
+        System.arraycopy(incoming, offset, copy, 0, len);
+        return copy;
+    }
+
+    /**
+     * Default implementation just returns the copy of the bytes.
+     */
+    @Override
+    public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws LdapException {
+        final byte[] copy = new byte[len];
+        System.arraycopy(outgoing, offset, copy, 0, len);
+        return copy;
+    }
+
+    /**
+     * Sets the authentication value to be used in the next bind request.
+     *
+     * @param authenticationValue
+     *            The authentication value to be used in the next bind request.
+     * @return A reference to this bind client.
+     */
+    final BindClient setNextAuthenticationValue(final byte[] authenticationValue) {
+        nextBindRequest.setAuthenticationValue(authenticationValue);
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindRequest.java
new file mode 100644
index 0000000..31a1c17
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/BindRequest.java
@@ -0,0 +1,96 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Bind operation allows authentication information to be exchanged between
+ * the client and server. The Bind operation should be thought of as the
+ * "authenticate" operation.
+ */
+public interface BindRequest extends Request {
+
+    /**
+     * The authentication type value (0x80) reserved for simple authentication.
+     */
+    byte AUTHENTICATION_TYPE_SIMPLE = (byte) 0x80;
+
+    /**
+     * The authentication type value (0xA3) reserved for SASL authentication.
+     */
+    byte AUTHENTICATION_TYPE_SASL = (byte) 0xA3;
+
+
+    @Override
+    BindRequest addControl(Control control);
+
+    /**
+     * Creates a new bind client which can be used to perform the authentication
+     * process. This method is called by protocol implementations and is not
+     * intended for use by applications.
+     *
+     * @param serverName
+     *            The non-null fully-qualified host name of the server to
+     *            authenticate to.
+     * @return The new bind client.
+     * @throws LdapException
+     *             If an error occurred while creating the bind client context.
+     */
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication mechanism identifier for this generic bind
+     * request as defined by the LDAP protocol. Note that the value
+     * {@link #AUTHENTICATION_TYPE_SIMPLE} ({@code 0x80}) is reserved for simple
+     * authentication and the value {@link #AUTHENTICATION_TYPE_SASL} (
+     * {@code 0xA3}) is reserved for SASL authentication.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    byte getAuthenticationType();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as. The name may be empty (but never {@code null}) when used for
+     * anonymous binds, or when using SASL authentication. The server shall not
+     * dereference any aliases in locating the named object.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    String getName();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequest.java
new file mode 100644
index 0000000..62b07d2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequest.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The CRAM-MD5 SASL bind request as defined in draft-ietf-sasl-crammd5. This
+ * SASL mechanism allows a client to perform a simple challenge-response
+ * authentication method, using a keyed MD5 digest. This mechanism does not
+ * provide a security layer.
+ * <p>
+ * The CRAM-MD5 mechanism is intended to have limited use on the Internet. The
+ * mechanism offers inadequate protection against common attacks against
+ * application-level protocols and is prone to interoperability problems.
+ * <p>
+ * The authentication identity is specified using an authorization ID, or
+ * {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ *
+ * @see <a
+ *      href="http://tools.ietf.org/html/draft-ietf-sasl-crammd5">draft-ietf-sasl-crammd5
+ *      - The CRAM-MD5 SASL Mechanism </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface CRAMMD5SASLBindRequest extends SASLBindRequest {
+
+    /**
+     * The name of the SASL mechanism based on CRAM-MD5 authentication.
+     */
+    String SASL_MECHANISM_NAME = "CRAM-MD5";
+
+    @Override
+    CRAMMD5SASLBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication ID of the user. The authentication ID usually
+     * has the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authentication ID of the user.
+     */
+    String getAuthenticationID();
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    /**
+     * Returns the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @return The password of the user that the client wishes to bind as.
+     */
+    byte[] getPassword();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Sets the authentication ID of the user. The authentication ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user.
+     * @return This bind request
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the authentication ID to
+     *             be set..
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authenticationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws NullPointerException
+     *             If {@code authenticationID} was {@code null}.
+     */
+    CRAMMD5SASLBindRequest setAuthenticationID(String authenticationID);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * provided password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as,
+     *            which may be empty.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    CRAMMD5SASLBindRequest setPassword(byte[] password);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as. The
+     * password will be converted to a UTF-8 octet string.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    CRAMMD5SASLBindRequest setPassword(char[] password);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestImpl.java
new file mode 100644
index 0000000..568c795
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestImpl.java
@@ -0,0 +1,181 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.copyOfBytes;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.responses.Responses.*;
+
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * CRAM-MD5 SASL bind request implementation.
+ */
+final class CRAMMD5SASLBindRequestImpl extends AbstractSASLBindRequest<CRAMMD5SASLBindRequest>
+        implements CRAMMD5SASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private final String authenticationID;
+        private final ByteString password;
+        private final SaslClient saslClient;
+
+        private Client(final CRAMMD5SASLBindRequestImpl initialBindRequest, final String serverName)
+                throws LdapException {
+            super(initialBindRequest);
+
+            this.authenticationID = initialBindRequest.getAuthenticationID();
+            this.password = ByteString.wrap(initialBindRequest.getPassword());
+
+            try {
+                saslClient =
+                        Sasl.createSaslClient(new String[] { SASL_MECHANISM_NAME }, null,
+                                SASL_DEFAULT_PROTOCOL, serverName, null, this);
+                if (saslClient.hasInitialResponse()) {
+                    setNextSASLCredentials(saslClient.evaluateChallenge(new byte[0]));
+                } else {
+                    setNextSASLCredentials((ByteString) null);
+                }
+            } catch (final SaslException e) {
+                throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e);
+            }
+        }
+
+        @Override
+        public void dispose() {
+            try {
+                saslClient.dispose();
+            } catch (final SaslException ignored) {
+                // Ignore the SASL exception.
+            }
+        }
+
+        @Override
+        public boolean evaluateResult(final BindResult result) throws LdapException {
+            if (saslClient.isComplete()) {
+                return true;
+            }
+
+            try {
+                setNextSASLCredentials(saslClient.evaluateChallenge(result
+                        .getServerSASLCredentials() == null ? new byte[0] : result
+                        .getServerSASLCredentials().toByteArray()));
+                return saslClient.isComplete();
+            } catch (final SaslException e) {
+                // FIXME: I18N need to have a better error message.
+                // FIXME: Is this the best result code?
+                throw newLdapException(newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR).setDiagnosticMessage(
+                        "An error occurred during multi-stage authentication").setCause(e));
+            }
+        }
+
+        @Override
+        void handle(final NameCallback callback) throws UnsupportedCallbackException {
+            callback.setName(authenticationID);
+        }
+
+        @Override
+        void handle(final PasswordCallback callback) throws UnsupportedCallbackException {
+            callback.setPassword(password.toString().toCharArray());
+        }
+    }
+
+    private String authenticationID;
+    private byte[] password;
+
+    CRAMMD5SASLBindRequestImpl(final CRAMMD5SASLBindRequest cramMD5SASLBindRequest) {
+        super(cramMD5SASLBindRequest);
+        this.authenticationID = cramMD5SASLBindRequest.getAuthenticationID();
+        this.password = copyOfBytes(cramMD5SASLBindRequest.getPassword());
+    }
+
+    CRAMMD5SASLBindRequestImpl(final String authenticationID, final byte[] password) {
+        Reject.ifNull(authenticationID, password);
+        this.authenticationID = authenticationID;
+        this.password = password;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new Client(this, serverName);
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return authenticationID;
+    }
+
+    @Override
+    public byte[] getPassword() {
+        return password;
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setAuthenticationID(final String authenticationID) {
+        Reject.ifNull(authenticationID);
+        this.authenticationID = authenticationID;
+        return this;
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setPassword(final byte[] password) {
+        Reject.ifNull(password);
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setPassword(final char[] password) {
+        Reject.ifNull(password);
+        this.password = StaticUtils.getBytes(password);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("CRAMMD5SASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        builder.append(", authenticationID=");
+        builder.append(authenticationID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequest.java
new file mode 100644
index 0000000..769da6b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequest.java
@@ -0,0 +1,94 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * The cancel extended request as defined in RFC 3909. This operation is similar
+ * to the abandon operation, except that it has a response and also requires the
+ * abandoned operation to return a response indicating it was canceled. This
+ * operation should be used instead of the abandon operation when the client
+ * needs an indication of the outcome. This operation may be used to cancel both
+ * interrogation and update operations.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3909">RFC 3909 - Lightweight
+ *      Directory Access Protocol (LDAP) Cancel Operation </a>
+ */
+public interface CancelExtendedRequest extends ExtendedRequest<ExtendedResult> {
+
+    /**
+     * A decoder which can be used to decode cancel extended operation requests.
+     */
+    ExtendedRequestDecoder<CancelExtendedRequest, ExtendedResult> DECODER =
+            new CancelExtendedRequestImpl.RequestDecoder();
+
+    /**
+     * The OID for the cancel extended operation request.
+     */
+    String OID = "1.3.6.1.1.8";
+
+    @Override
+    CancelExtendedRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getOID();
+
+    /**
+     * Returns the request ID of the request to be abandoned.
+     *
+     * @return The request ID of the request to be abandoned.
+     */
+    int getRequestID();
+
+    @Override
+    ExtendedResultDecoder<ExtendedResult> getResultDecoder();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    /**
+     * Sets the request ID of the request to be abandoned.
+     *
+     * @param id
+     *            The request ID of the request to be abandoned.
+     * @return This abandon request.
+     * @throws UnsupportedOperationException
+     *             If this abandon request does not permit the request ID to be
+     *             set.
+     */
+    CancelExtendedRequest setRequestID(int id);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestImpl.java
new file mode 100644
index 0000000..8ead8b6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestImpl.java
@@ -0,0 +1,164 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_CANCEL_CANNOT_DECODE_REQUEST_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_CANCEL_NO_REQUEST_VALUE;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+/**
+ * Cancel extended request implementation.
+ */
+final class CancelExtendedRequestImpl extends
+        AbstractExtendedRequest<CancelExtendedRequest, ExtendedResult> implements
+        CancelExtendedRequest {
+    static final class RequestDecoder implements
+            ExtendedRequestDecoder<CancelExtendedRequest, ExtendedResult> {
+        @Override
+        public CancelExtendedRequest decodeExtendedRequest(final ExtendedRequest<?> request,
+                final DecodeOptions options) throws DecodeException {
+            final ByteString requestValue = request.getValue();
+            if (requestValue == null || requestValue.length() <= 0) {
+                throw DecodeException.error(ERR_EXTOP_CANCEL_NO_REQUEST_VALUE.get());
+            }
+
+            try {
+                final ASN1Reader reader = ASN1.getReader(requestValue);
+                reader.readStartSequence();
+                final int idToCancel = (int) reader.readInteger();
+                reader.readEndSequence();
+
+                final CancelExtendedRequest newRequest = new CancelExtendedRequestImpl(idToCancel);
+
+                for (final Control control : request.getControls()) {
+                    newRequest.addControl(control);
+                }
+
+                return newRequest;
+            } catch (final IOException e) {
+                final LocalizableMessage message =
+                        ERR_EXTOP_CANCEL_CANNOT_DECODE_REQUEST_VALUE.get(getExceptionMessage(e));
+                throw DecodeException.error(message, e);
+            }
+        }
+    }
+
+    private static final class ResultDecoder extends AbstractExtendedResultDecoder<ExtendedResult> {
+        @Override
+        public ExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            // TODO: Should we check to make sure OID and value is null?
+            return result;
+        }
+
+        @Override
+        public ExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newGenericExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+    }
+
+    /** No need to expose this. */
+    private static final ExtendedResultDecoder<ExtendedResult> RESULT_DECODER = new ResultDecoder();
+
+    private int requestID;
+
+    CancelExtendedRequestImpl(final CancelExtendedRequest cancelExtendedRequest) {
+        super(cancelExtendedRequest);
+        this.requestID = cancelExtendedRequest.getRequestID();
+    }
+
+    /** Instantiation via factory. */
+    CancelExtendedRequestImpl(final int requestID) {
+        this.requestID = requestID;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public int getRequestID() {
+        return requestID;
+    }
+
+    @Override
+    public ExtendedResultDecoder<ExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder(6);
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+
+        try {
+            writer.writeStartSequence();
+            writer.writeInteger(requestID);
+            writer.writeEndSequence();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+
+        return buffer.toByteString();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public CancelExtendedRequest setRequestID(final int id) {
+        this.requestID = id;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("CancelExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", requestID=");
+        builder.append(requestID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequest.java
new file mode 100644
index 0000000..7b70cfb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequest.java
@@ -0,0 +1,176 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Compare operation allows a client to compare an assertion value with the
+ * values of a particular attribute in a particular entry in the Directory.
+ * <p>
+ * Note that some directory systems may establish access controls that permit
+ * the values of certain attributes (such as {@code userPassword} ) to be
+ * compared but not interrogated by other means.
+ * <p>
+ * The following excerpt shows how to use the Compare operation to check whether
+ * a member belongs to a (possibly large) static group.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String groupDN = ...;
+ * String memberDN = ...;
+ *
+ * CompareRequest request =
+ *          Requests.newCompareRequest(groupDN, "member", memberDN);
+ * CompareResult result = connection.compare(request);
+ * if (result.matched()) {
+ *     // The member belongs to the group.
+ * }
+ * </pre>
+ */
+public interface CompareRequest extends Request {
+
+    @Override
+    CompareRequest addControl(Control control);
+
+    /**
+     * Returns the assertion value to be compared.
+     *
+     * @return The assertion value.
+     */
+    ByteString getAssertionValue();
+
+    /**
+     * Returns the assertion value to be compared decoded as a UTF-8 string.
+     *
+     * @return The assertion value decoded as a UTF-8 string.
+     */
+    String getAssertionValueAsString();
+
+    /**
+     * Returns the name of the attribute to be compared.
+     *
+     * @return The name of the attribute.
+     */
+    AttributeDescription getAttributeDescription();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the distinguished name of the entry to be compared. The server
+     * shall not dereference any aliases in locating the entry to be compared.
+     *
+     * @return The distinguished name of the entry.
+     */
+    DN getName();
+
+    /**
+     * Sets the assertion value to be compared.
+     * <p>
+     * If the assertion value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            The assertion value to be compared.
+     * @return This compare request.
+     * @throws UnsupportedOperationException
+     *             If this compare request does not permit the assertion value
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code value} was {@code null}.
+     */
+    CompareRequest setAssertionValue(Object value);
+
+    /**
+     * Sets the name of the attribute to be compared.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be compared.
+     * @return This compare request.
+     * @throws UnsupportedOperationException
+     *             If this compare request does not permit the attribute
+     *             description to be set.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    CompareRequest setAttributeDescription(AttributeDescription attributeDescription);
+
+    /**
+     * Sets the name of the attribute to be compared.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be compared.
+     * @return This compare request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws UnsupportedOperationException
+     *             If this compare request does not permit the attribute
+     *             description to be set.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    CompareRequest setAttributeDescription(String attributeDescription);
+
+    /**
+     * Sets the distinguished name of the entry to be compared. The server shall
+     * not dereference any aliases in locating the entry to be compared.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be compared.
+     * @return This compare request.
+     * @throws UnsupportedOperationException
+     *             If this compare request does not permit the distinguished
+     *             name to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    CompareRequest setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the entry to be compared. The server shall
+     * not dereference any aliases in locating the entry to be compared.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be compared.
+     * @return This compare request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this compare request does not permit the distinguished
+     *             name to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    CompareRequest setName(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequestImpl.java
new file mode 100644
index 0000000..5d26833
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/CompareRequestImpl.java
@@ -0,0 +1,125 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Compare request implementation.
+ */
+final class CompareRequestImpl extends AbstractRequestImpl<CompareRequest> implements
+        CompareRequest {
+
+    private ByteString assertionValue;
+    private AttributeDescription attributeDescription;
+    private DN name;
+
+    CompareRequestImpl(final CompareRequest compareRequest) {
+        super(compareRequest);
+        this.name = compareRequest.getName();
+        this.attributeDescription = compareRequest.getAttributeDescription();
+        this.assertionValue = compareRequest.getAssertionValue();
+    }
+
+    CompareRequestImpl(final DN name, final AttributeDescription attributeDescription,
+            final ByteString assertionValue) {
+        this.name = name;
+        this.attributeDescription = attributeDescription;
+        this.assertionValue = assertionValue;
+    }
+
+    @Override
+    public ByteString getAssertionValue() {
+        return assertionValue;
+    }
+
+    @Override
+    public String getAssertionValueAsString() {
+        return assertionValue.toString();
+    }
+
+    @Override
+    public AttributeDescription getAttributeDescription() {
+        return attributeDescription;
+    }
+
+    @Override
+    public DN getName() {
+        return name;
+    }
+
+    @Override
+    public CompareRequest setAssertionValue(final Object value) {
+        Reject.ifNull(value);
+        this.assertionValue = ByteString.valueOfObject(value);
+        return this;
+    }
+
+    @Override
+    public CompareRequest setAttributeDescription(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        this.attributeDescription = attributeDescription;
+        return this;
+    }
+
+    @Override
+    public CompareRequest setAttributeDescription(final String attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        this.attributeDescription = AttributeDescription.valueOf(attributeDescription);
+        return this;
+    }
+
+    @Override
+    public CompareRequest setName(final DN dn) {
+        Reject.ifNull(dn);
+        this.name = dn;
+        return this;
+    }
+
+    @Override
+    public CompareRequest setName(final String dn) {
+        Reject.ifNull(dn);
+        this.name = DN.valueOf(dn);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("CompareRequest(name=");
+        builder.append(getName());
+        builder.append(", attributeDescription=");
+        builder.append(getAttributeDescription());
+        builder.append(", assertionValue=");
+        builder.append(getAssertionValueAsString());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    CompareRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequest.java
new file mode 100644
index 0000000..08ff694
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequest.java
@@ -0,0 +1,105 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * The Delete operation allows a client to request the removal of an entry from
+ * the Directory.
+ * <p>
+ * Only leaf entries (those with no subordinate entries) can be deleted with
+ * this operation. However, addition of the {@code SubtreeDeleteControl} permits
+ * whole sub-trees to be deleted using a single Delete request.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String baseDN = ...;
+ *
+ * DeleteRequest request =
+ *         Requests.newDeleteRequest(baseDN)
+ *             .addControl(SubtreeDeleteRequestControl.newControl(true));
+ * connection.delete(request);
+ * </pre>
+ */
+public interface DeleteRequest extends Request, ChangeRecord {
+
+    @Override
+    <R, P> R accept(ChangeRecordVisitor<R, P> v, P p);
+
+    @Override
+    DeleteRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the distinguished name of the entry to be deleted. The server
+     * shall not dereference any aliases in locating the entry to be deleted.
+     *
+     * @return The distinguished name of the entry.
+     */
+    @Override
+    DN getName();
+
+    /**
+     * Sets the distinguished name of the entry to be deleted. The server shall
+     * not dereference any aliases in locating the entry to be deleted.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be deleted.
+     * @return This delete request.
+     * @throws UnsupportedOperationException
+     *             If this delete request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    DeleteRequest setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the entry to be deleted. The server shall
+     * not dereference any aliases in locating the entry to be deleted.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be deleted.
+     * @return This delete request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this delete request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    DeleteRequest setName(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequestImpl.java
new file mode 100644
index 0000000..094df33
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DeleteRequestImpl.java
@@ -0,0 +1,80 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Delete request implementation.
+ */
+final class DeleteRequestImpl extends AbstractRequestImpl<DeleteRequest> implements DeleteRequest {
+    private DN name;
+
+    DeleteRequestImpl(final DeleteRequest deleteRequest) {
+        super(deleteRequest);
+        this.name = deleteRequest.getName();
+    }
+
+    DeleteRequestImpl(final DN name) {
+        this.name = name;
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public DN getName() {
+        return name;
+    }
+
+    @Override
+    public DeleteRequest setName(final DN dn) {
+        Reject.ifNull(dn);
+        this.name = dn;
+        return this;
+    }
+
+    @Override
+    public DeleteRequest setName(final String dn) {
+        Reject.ifNull(dn);
+        this.name = DN.valueOf(dn);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("DeleteRequest(name=");
+        builder.append(getName());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    DeleteRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequest.java
new file mode 100644
index 0000000..94f4814
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequest.java
@@ -0,0 +1,466 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The DIGEST-MD5 SASL bind request as defined in RFC 2831. This SASL mechanism
+ * allows a client to perform a challenge-response authentication method,
+ * similar to HTTP Digest Access Authentication. This mechanism can be used to
+ * negotiate integrity and/or privacy protection for the underlying connection.
+ * <p>
+ * Compared to CRAM-MD5, DIGEST-MD5 prevents chosen plain-text attacks, and
+ * permits the use of third party authentication servers, mutual authentication,
+ * and optimized re-authentication if a client has recently authenticated to a
+ * server.
+ * <p>
+ * The authentication and optional authorization identity is specified using an
+ * authorization ID, or {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2831">RFC 2831 - Using Digest
+ *      Authentication as a SASL Mechanism </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface DigestMD5SASLBindRequest extends SASLBindRequest {
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * high strength triple-DES cipher.
+     */
+    String CIPHER_3DES = "3des";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * medium strength DES cipher.
+     */
+    String CIPHER_DES = "des";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * strongest supported cipher, as long as the cipher is considered to be
+     * high strength.
+     */
+    String CIPHER_HIGH = "high";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * strongest supported cipher, even if the strongest cipher is considered to
+     * be medium or low strength.
+     */
+    String CIPHER_LOW = "low";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * strongest supported cipher, as long as the cipher is considered to be
+     * high or medium strength.
+     */
+    String CIPHER_MEDIUM = "medium";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * high strength 128-bit RC4 cipher.
+     */
+    String CIPHER_RC4_128 = "rc4";
+
+    /**
+     * Indicates that the client will accept connection encryption using the low
+     * strength 40-bit RC4 cipher.
+     */
+    String CIPHER_RC4_40 = "rc4-40";
+
+    /**
+     * Indicates that the client will accept connection encryption using the
+     * medium strength 56-bit RC4 cipher.
+     */
+    String CIPHER_RC4_56 = "rc4-56";
+
+    /**
+     * Indicates that the client will accept authentication only. More
+     * specifically, the underlying connection will not be protected using
+     * integrity protection or encryption, unless previously established using
+     * SSL/TLS. This is the default if no QOP option is present in the bind
+     * request.
+     */
+    String QOP_AUTH = "auth";
+
+    /**
+     * Indicates that the client will accept authentication with connection
+     * integrity protection and encryption.
+     */
+    String QOP_AUTH_CONF = "auth-conf";
+
+    /**
+     * Indicates that the client will accept authentication with connection
+     * integrity protection. More specifically, the underlying connection will
+     * not be encrypted, unless previously established using SSL/TLS.
+     */
+    String QOP_AUTH_INT = "auth-int";
+
+    /**
+     * The name of the SASL mechanism based on DIGEST-MD5 authentication.
+     */
+    String SASL_MECHANISM_NAME = "DIGEST-MD5";
+
+    /**
+     * Adds the provided additional authentication parameter to the list of
+     * parameters to be passed to the underlying mechanism implementation. This
+     * method is provided in order to allow for future extensions.
+     *
+     * @param name
+     *            The name of the additional authentication parameter.
+     * @param value
+     *            The value of the additional authentication parameter.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit additional
+     *             authentication parameters to be added.
+     * @throws NullPointerException
+     *             If {@code name} or {@code value} was {@code null}.
+     */
+    DigestMD5SASLBindRequest addAdditionalAuthParam(String name, String value);
+
+    @Override
+    DigestMD5SASLBindRequest addControl(Control control);
+
+    /**
+     * Adds the provided quality of protection (QOP) values to the ordered list
+     * of QOP values that the client is willing to accept. The order of the list
+     * specifies the preference order, high to low. Authentication will fail if
+     * no QOP values are recognized or accepted by the server.
+     * <p>
+     * By default the client will accept {@link #QOP_AUTH AUTH}.
+     *
+     * @param qopValues
+     *            The quality of protection values that the client is willing to
+     *            accept.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit QOP values to be added.
+     * @throws NullPointerException
+     *             If {@code qopValues} was {@code null}.
+     * @see #QOP_AUTH
+     * @see #QOP_AUTH_INT
+     * @see #QOP_AUTH_CONF
+     */
+    DigestMD5SASLBindRequest addQOP(String... qopValues);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns a map containing the provided additional authentication
+     * parameters to be passed to the underlying mechanism implementation. This
+     * method is provided in order to allow for future extensions.
+     *
+     * @return A map containing the provided additional authentication
+     *         parameters to be passed to the underlying mechanism
+     *         implementation.
+     */
+    Map<String, String> getAdditionalAuthParams();
+
+    /**
+     * Returns the authentication ID of the user. The authentication ID usually
+     * has the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authentication ID of the user.
+     */
+    String getAuthenticationID();
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    /**
+     * Returns the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authorization ID of the user, which may be {@code null}.
+     */
+    String getAuthorizationID();
+
+    /**
+     * Returns the cipher name or strength that the client is willing to use
+     * when connection encryption quality of protection, {@link #QOP_AUTH_CONF
+     * AUTH-CONF}, is requested.
+     * <p>
+     * By default the client will accept connection encryption using the
+     * strongest supported cipher, even if the strongest cipher is considered to
+     * be medium or low strength. This is equivalent to {@link #CIPHER_LOW}.
+     *
+     * @return The cipher that the client is willing to use if connection
+     *         encryption QOP is negotiated. May be {@code null}, indicating
+     *         that the default cipher should be used.
+     */
+    String getCipher();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the maximum size of the receive buffer in bytes. The actual
+     * maximum number of bytes will be the minimum of this number and the peer's
+     * maximum send buffer size. The default size is 65536.
+     *
+     * @return The maximum size of the receive buffer in bytes.
+     */
+    int getMaxReceiveBufferSize();
+
+    /**
+     * Returns the maximum size of the send buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * receive buffer size. The default size is 65536.
+     *
+     * @return The maximum size of the send buffer in bytes.
+     */
+    int getMaxSendBufferSize();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    /**
+     * Returns the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @return The password of the user that the client wishes to bind as.
+     */
+    byte[] getPassword();
+
+    /**
+     * Returns the ordered list of quality of protection (QOP) values that the
+     * client is willing to accept. The order of the list specifies the
+     * preference order, high to low. Authentication will fail if no QOP values
+     * are recognized or accepted by the server.
+     * <p>
+     * By default the client will accept {@link #QOP_AUTH AUTH}.
+     *
+     * @return The list of quality of protection values that the client is
+     *         willing to accept. The returned list may be empty indicating that
+     *         the default QOP will be accepted.
+     */
+    List<String> getQOPs();
+
+    /**
+     * Returns the optional realm containing the user's account.
+     *
+     * @return The name of the realm containing the user's account, which may be
+     *         {@code null}.
+     */
+    String getRealm();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Returns {@code true} if the server must authenticate to the client. The
+     * default is {@code false}.
+     *
+     * @return {@code true} if the server must authenticate to the client.
+     */
+    boolean isServerAuth();
+
+    /**
+     * Sets the authentication ID of the user. The authentication ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user.
+     * @return This bind request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authenticationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the authentication ID to
+     *             be set.
+     * @throws NullPointerException
+     *             If {@code authenticationID} was {@code null}.
+     */
+    DigestMD5SASLBindRequest setAuthenticationID(String authenticationID);
+
+    /**
+     * Sets the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authorizationID
+     *            The authorization ID of the user, which may be {@code null}.
+     * @return This bind request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the authorization ID to
+     *             be set.
+     */
+    DigestMD5SASLBindRequest setAuthorizationID(String authorizationID);
+
+    /**
+     * Sets the cipher name or strength that the client is willing to use when
+     * connection encryption quality of protection, {@link #QOP_AUTH_CONF
+     * AUTH-CONF}, is requested.
+     * <p>
+     * By default the client will accept connection encryption using the
+     * strongest supported cipher, even if the strongest cipher is considered to
+     * be medium or low strength. This is equivalent to {@link #CIPHER_LOW}.
+     *
+     * @param cipher
+     *            The cipher that the client is willing to use if connection
+     *            encryption QOP is negotiated. May be {@code null}, indicating
+     *            that the default cipher should be used.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the cipher name or
+     *             strength to be set.
+     * @see #QOP_AUTH_CONF
+     * @see #CIPHER_3DES
+     * @see #CIPHER_RC4_128
+     * @see #CIPHER_DES
+     * @see #CIPHER_RC4_56
+     * @see #CIPHER_RC4_40
+     * @see #CIPHER_HIGH
+     * @see #CIPHER_MEDIUM
+     * @see #CIPHER_LOW
+     */
+    DigestMD5SASLBindRequest setCipher(String cipher);
+
+    /**
+     * Sets the maximum size of the receive buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * send buffer size. The default size is 65536.
+     *
+     * @param size
+     *            The maximum size of the receive buffer in bytes.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the buffer size to be
+     *             set.
+     */
+    DigestMD5SASLBindRequest setMaxReceiveBufferSize(int size);
+
+    /**
+     * Sets the maximum size of the send buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * receive buffer size. The default size is 65536.
+     *
+     * @param size
+     *            The maximum size of the send buffer in bytes.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the buffer size to be
+     *             set.
+     */
+    DigestMD5SASLBindRequest setMaxSendBufferSize(int size);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * provided password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as,
+     *            which may be empty.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    DigestMD5SASLBindRequest setPassword(byte[] password);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as. The
+     * password will be converted to a UTF-8 octet string.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    DigestMD5SASLBindRequest setPassword(char[] password);
+
+    /**
+     * Sets the optional realm containing the user's account.
+     *
+     * @param realm
+     *            The name of the realm containing the user's account, which may
+     *            be {@code null}.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the realm to be set.
+     * @throws NullPointerException
+     *             If {@code realm} was {@code null}.
+     */
+    DigestMD5SASLBindRequest setRealm(String realm);
+
+    /**
+     * Specifies whether or not the server must authenticate to the client. The
+     * default is {@code false}.
+     *
+     * @param serverAuth
+     *            {@code true} if the server must authenticate to the client or
+     *            {@code false} otherwise.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit server auth to be set.
+     */
+    DigestMD5SASLBindRequest setServerAuth(boolean serverAuth);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestImpl.java
new file mode 100644
index 0000000..5ad1118
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestImpl.java
@@ -0,0 +1,395 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SASL_PROTOCOL_ERROR;
+import static com.forgerock.opendj.util.StaticUtils.copyOfBytes;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.RealmCallback;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Utils;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Digest-MD5 SASL bind request implementation.
+ */
+final class DigestMD5SASLBindRequestImpl extends AbstractSASLBindRequest<DigestMD5SASLBindRequest>
+        implements DigestMD5SASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private final String authenticationID;
+        private final ByteString password;
+        private final String realm;
+        private final SaslClient saslClient;
+
+        private Client(final DigestMD5SASLBindRequestImpl initialBindRequest,
+                final String serverName) throws LdapException {
+            super(initialBindRequest);
+
+            this.authenticationID = initialBindRequest.getAuthenticationID();
+            this.password = ByteString.wrap(initialBindRequest.getPassword());
+            this.realm = initialBindRequest.getRealm();
+
+            // Create property map containing all the parameters.
+            final Map<String, String> props = new HashMap<>();
+
+            final List<String> qopValues = initialBindRequest.getQOPs();
+            if (!qopValues.isEmpty()) {
+                props.put(Sasl.QOP, Utils.joinAsString(",", qopValues));
+            }
+
+            final String cipher = initialBindRequest.getCipher();
+            if (cipher != null) {
+                if (cipher.equalsIgnoreCase(CIPHER_LOW)) {
+                    props.put(Sasl.STRENGTH, "high,medium,low");
+                } else if (cipher.equalsIgnoreCase(CIPHER_MEDIUM)) {
+                    props.put(Sasl.STRENGTH, "high,medium");
+                } else if (cipher.equalsIgnoreCase(CIPHER_HIGH)) {
+                    props.put(Sasl.STRENGTH, "high");
+                } else {
+                    /*
+                     * Default strength allows all ciphers, so specifying a
+                     * single cipher cannot be incompatible with the strength.
+                     */
+                    props.put("com.sun.security.sasl.digest.cipher", cipher);
+                }
+            }
+
+            final Boolean serverAuth = initialBindRequest.isServerAuth();
+            if (serverAuth != null) {
+                props.put(Sasl.SERVER_AUTH, String.valueOf(serverAuth));
+            }
+
+            Integer size = initialBindRequest.getMaxReceiveBufferSize();
+            if (size != null) {
+                props.put(Sasl.MAX_BUFFER, String.valueOf(size));
+            }
+
+            size = initialBindRequest.getMaxSendBufferSize();
+            if (size != null) {
+                props.put("javax.security.sasl.sendmaxbuffer", String.valueOf(size));
+            }
+
+            for (final Map.Entry<String, String> e : initialBindRequest.getAdditionalAuthParams()
+                    .entrySet()) {
+                props.put(e.getKey(), e.getValue());
+            }
+
+            // Now create the client.
+            try {
+                saslClient =
+                        Sasl.createSaslClient(new String[] { SASL_MECHANISM_NAME },
+                                initialBindRequest.getAuthorizationID(), SASL_DEFAULT_PROTOCOL,
+                                serverName, props, this);
+                if (saslClient.hasInitialResponse()) {
+                    setNextSASLCredentials(saslClient.evaluateChallenge(new byte[0]));
+                } else {
+                    setNextSASLCredentials((ByteString) null);
+                }
+            } catch (final SaslException e) {
+                throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e);
+            }
+        }
+
+        @Override
+        public void dispose() {
+            try {
+                saslClient.dispose();
+            } catch (final SaslException ignored) {
+                // Ignore the SASL exception.
+            }
+        }
+
+        @Override
+        public boolean evaluateResult(final BindResult result) throws LdapException {
+            if (saslClient.isComplete()) {
+                return true;
+            }
+
+            try {
+                setNextSASLCredentials(saslClient.evaluateChallenge(result
+                        .getServerSASLCredentials() == null ? new byte[0] : result
+                        .getServerSASLCredentials().toByteArray()));
+                return saslClient.isComplete();
+            } catch (final SaslException e) {
+                // FIXME: I18N need to have a better error message.
+                // FIXME: Is this the best result code?
+                throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR,
+                        "An error occurred during multi-stage authentication", e);
+            }
+        }
+
+        @Override
+        public ConnectionSecurityLayer getConnectionSecurityLayer() {
+            final String qop = (String) saslClient.getNegotiatedProperty(Sasl.QOP);
+            if (qop.equalsIgnoreCase("auth-int") || qop.equalsIgnoreCase("auth-conf")) {
+                return this;
+            }
+            return null;
+        }
+
+        @Override
+        public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws LdapException {
+            try {
+                return saslClient.unwrap(incoming, offset, len);
+            } catch (final SaslException e) {
+                final LocalizableMessage msg =
+                        ERR_SASL_PROTOCOL_ERROR.get(SASL_MECHANISM_NAME, getExceptionMessage(e));
+                throw newLdapException(ResultCode.CLIENT_SIDE_DECODING_ERROR, msg.toString(), e);
+            }
+        }
+
+        @Override
+        public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws LdapException {
+            try {
+                return saslClient.wrap(outgoing, offset, len);
+            } catch (final SaslException e) {
+                final LocalizableMessage msg =
+                        ERR_SASL_PROTOCOL_ERROR.get(SASL_MECHANISM_NAME, getExceptionMessage(e));
+                throw newLdapException(ResultCode.CLIENT_SIDE_ENCODING_ERROR, msg.toString(), e);
+            }
+        }
+
+        @Override
+        void handle(final NameCallback callback) throws UnsupportedCallbackException {
+            callback.setName(authenticationID);
+        }
+
+        @Override
+        void handle(final PasswordCallback callback) throws UnsupportedCallbackException {
+            callback.setPassword(password.toString().toCharArray());
+        }
+
+        @Override
+        void handle(final RealmCallback callback) throws UnsupportedCallbackException {
+            callback.setText(realm != null ? realm : callback.getDefaultText());
+        }
+
+    }
+
+    private final Map<String, String> additionalAuthParams = new LinkedHashMap<>();
+    private String authenticationID;
+    private String authorizationID;
+
+    private String cipher;
+    private Integer maxReceiveBufferSize;
+    private Integer maxSendBufferSize;
+    private byte[] password;
+    private final List<String> qopValues = new LinkedList<>();
+    private String realm;
+    /**
+     * Do not use primitives for these so that we can distinguish between default
+     * settings (null) and values set by the caller.
+     */
+    private Boolean serverAuth;
+
+    DigestMD5SASLBindRequestImpl(final DigestMD5SASLBindRequest digestMD5SASLBindRequest) {
+        super(digestMD5SASLBindRequest);
+        this.additionalAuthParams.putAll(digestMD5SASLBindRequest.getAdditionalAuthParams());
+        this.qopValues.addAll(digestMD5SASLBindRequest.getQOPs());
+        this.cipher = digestMD5SASLBindRequest.getCipher();
+
+        this.serverAuth = digestMD5SASLBindRequest.isServerAuth();
+        this.maxReceiveBufferSize = digestMD5SASLBindRequest.getMaxReceiveBufferSize();
+        this.maxSendBufferSize = digestMD5SASLBindRequest.getMaxSendBufferSize();
+
+        this.authenticationID = digestMD5SASLBindRequest.getAuthenticationID();
+        this.authorizationID = digestMD5SASLBindRequest.getAuthorizationID();
+        this.password = copyOfBytes(digestMD5SASLBindRequest.getPassword());
+        this.realm = digestMD5SASLBindRequest.getRealm();
+    }
+
+    DigestMD5SASLBindRequestImpl(final String authenticationID, final byte[] password) {
+        Reject.ifNull(authenticationID, password);
+        this.authenticationID = authenticationID;
+        this.password = password;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest addAdditionalAuthParam(final String name, final String value) {
+        Reject.ifNull(name, value);
+        additionalAuthParams.put(name, value);
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest addQOP(final String... qopValues) {
+        for (final String qopValue : qopValues) {
+            this.qopValues.add(Reject.checkNotNull(qopValue));
+        }
+        return this;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new Client(this, serverName);
+    }
+
+    @Override
+    public Map<String, String> getAdditionalAuthParams() {
+        return additionalAuthParams;
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return authenticationID;
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getCipher() {
+        return cipher;
+    }
+
+    @Override
+    public int getMaxReceiveBufferSize() {
+        return maxReceiveBufferSize == null ? 65536 : maxReceiveBufferSize;
+    }
+
+    @Override
+    public int getMaxSendBufferSize() {
+        return maxSendBufferSize == null ? 65536 : maxSendBufferSize;
+    }
+
+    @Override
+    public byte[] getPassword() {
+        return password;
+    }
+
+    @Override
+    public List<String> getQOPs() {
+        return qopValues;
+    }
+
+    @Override
+    public String getRealm() {
+        return realm;
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public boolean isServerAuth() {
+        return serverAuth == null ? false : serverAuth;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setAuthenticationID(final String authenticationID) {
+        Reject.ifNull(authenticationID);
+        this.authenticationID = authenticationID;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setAuthorizationID(final String authorizationID) {
+        this.authorizationID = authorizationID;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setCipher(final String cipher) {
+        this.cipher = cipher;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setMaxReceiveBufferSize(final int size) {
+        maxReceiveBufferSize = size;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setMaxSendBufferSize(final int size) {
+        maxSendBufferSize = size;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setPassword(final byte[] password) {
+        Reject.ifNull(password);
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setPassword(final char[] password) {
+        Reject.ifNull(password);
+        this.password = StaticUtils.getBytes(password);
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setRealm(final String realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setServerAuth(final boolean serverAuth) {
+        this.serverAuth = serverAuth;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("DigestMD5SASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        builder.append(", authenticationID=");
+        builder.append(authenticationID);
+        builder.append(", authorizationID=");
+        builder.append(authorizationID);
+        builder.append(", realm=");
+        builder.append(realm);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequest.java
new file mode 100644
index 0000000..a3cdeaf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequest.java
@@ -0,0 +1,102 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * The Extended operation allows additional operations to be defined for
+ * services not already available in the protocol; for example, to implement an
+ * operation which installs transport layer security (see
+ * {@link StartTLSExtendedRequest}).
+ * <p>
+ * To determine whether a directory server supports a given extension, read the
+ * list of supported extensions from the root DSE to get a collection of
+ * extension OIDs, and then check for a match. For example:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * Collection&lt;String&gt; supported =
+ *     RootDSE.readRootDSE(connection).getSupportedExtendedOperations();
+ *
+ * ExtendedRequest extension = ...;
+ * String OID = extension.getOID();
+ * if (supported != null && !supported.isEmpty() && supported.contains(OID)) {
+ *     // The extension is supported. Use it here...
+ * }
+ * </pre>
+ *
+ * @param <S>
+ *            The type of result.
+ */
+public interface ExtendedRequest<S extends ExtendedResult> extends Request {
+
+    @Override
+    ExtendedRequest<S> addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the numeric OID associated with this extended request.
+     *
+     * @return The numeric OID associated with this extended request.
+     */
+    String getOID();
+
+    /**
+     * Returns a decoder which can be used to decoded responses to this extended
+     * request.
+     *
+     * @return A decoder which can be used to decoded responses to this extended
+     *         request.
+     */
+    ExtendedResultDecoder<S> getResultDecoder();
+
+    /**
+     * Returns the value, if any, associated with this extended request. Its
+     * format is defined by the specification of this extended request.
+     *
+     * @return The value associated with this extended request, or {@code null}
+     *         if there is no value.
+     */
+    ByteString getValue();
+
+    /**
+     * Returns {@code true} if this extended request has a value. In some
+     * circumstances it may be useful to determine if a extended request has a
+     * value, without actually calculating the value and incurring any
+     * performance costs.
+     *
+     * @return {@code true} if this extended request has a value, or
+     *         {@code false} if there is no value.
+     */
+    boolean hasValue();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequestDecoder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequestDecoder.java
new file mode 100644
index 0000000..8b48411
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExtendedRequestDecoder.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * A factory interface for decoding a generic extended request as an extended
+ * request of specific type.
+ *
+ * @param <R>
+ *            The type of extended request.
+ * @param <S>
+ *            The type of result.
+ */
+public interface ExtendedRequestDecoder<R extends ExtendedRequest<S>, S extends ExtendedResult> {
+    /**
+     * Decodes the provided extended operation request as an
+     * {@code ExtendedRequest} of type {@code R}.
+     *
+     * @param request
+     *            The extended operation request to be decoded.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the extended operation request.
+     * @return The decoded extended operation request.
+     * @throws DecodeException
+     *             If the provided extended operation request could not be
+     *             decoded. For example, if the request name was wrong, or if
+     *             the request value was invalid.
+     */
+    R decodeExtendedRequest(ExtendedRequest<?> request, DecodeOptions options)
+            throws DecodeException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequest.java
new file mode 100644
index 0000000..6358654
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequest.java
@@ -0,0 +1,121 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The External SASL bind request as defined in RFC 4422. This SASL mechanism
+ * allows a client to request the server to use credentials established by means
+ * external to the mechanism to authenticate the client. The external means may
+ * be, for instance, SSL or TLS.
+ * <p>
+ * A client may either request that its authorization identity be automatically
+ * derived from its authentication credentials exchanged at a lower security
+ * layer, or it may explicitly provide a desired authorization identity.
+ * <p>
+ * The optional authorization identity is specified using an authorization ID,
+ * or {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4422">RFC 4422 - Simple
+ *      Authentication and Security Layer (SASL) </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface ExternalSASLBindRequest extends SASLBindRequest {
+
+    /**
+     * The name of the SASL mechanism based on external authentication.
+     */
+    String SASL_MECHANISM_NAME = "EXTERNAL";
+
+    @Override
+    ExternalSASLBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    /**
+     * Returns the optional desired authorization ID of the user, or
+     * {@code null} if the authorization ID should derived from authentication
+     * credentials exchanged at a lower security layer. The authorization ID
+     * usually has the form "dn:" immediately followed by the distinguished name
+     * of the user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The desired authorization ID of the user, which may be
+     *         {@code null} .
+     */
+    String getAuthorizationID();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Sets the optional desired authorization ID of the user, or {@code null}
+     * if the authorization ID should derived from authentication credentials
+     * exchanged at a lower security layer. The authorization ID usually has the
+     * form "dn:" immediately followed by the distinguished name of the user, or
+     * "u:" followed by a user ID string, but other forms are permitted.
+     *
+     * @param authorizationID
+     *            The desired authorization ID of the user, which may be
+     *            {@code null}.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this external SASL request does not permit the
+     *             authorization ID to be set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     */
+    ExternalSASLBindRequest setAuthorizationID(String authorizationID);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestImpl.java
new file mode 100644
index 0000000..af07105
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestImpl.java
@@ -0,0 +1,136 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+/**
+ * External SASL bind request implementation.
+ */
+final class ExternalSASLBindRequestImpl extends AbstractSASLBindRequest<ExternalSASLBindRequest>
+        implements ExternalSASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private final SaslClient saslClient;
+
+        private Client(final ExternalSASLBindRequestImpl initialBindRequest, final String serverName)
+                throws LdapException {
+            super(initialBindRequest);
+
+            try {
+                saslClient =
+                        Sasl.createSaslClient(new String[] { SASL_MECHANISM_NAME },
+                                initialBindRequest.getAuthorizationID(), SASL_DEFAULT_PROTOCOL,
+                                serverName, null, this);
+                if (saslClient.hasInitialResponse()) {
+                    setNextSASLCredentials(saslClient.evaluateChallenge(new byte[0]));
+                } else {
+                    setNextSASLCredentials((ByteString) null);
+                }
+            } catch (final SaslException e) {
+                throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e);
+            }
+        }
+
+        @Override
+        public void dispose() {
+            try {
+                saslClient.dispose();
+            } catch (final SaslException ignored) {
+                // Ignore the SASL exception.
+            }
+        }
+
+        @Override
+        public boolean evaluateResult(final BindResult result) throws LdapException {
+            if (saslClient.isComplete()) {
+                return true;
+            }
+
+            try {
+                setNextSASLCredentials(saslClient.evaluateChallenge(result
+                        .getServerSASLCredentials() == null ? new byte[0] : result
+                        .getServerSASLCredentials().toByteArray()));
+                return saslClient.isComplete();
+            } catch (final SaslException e) {
+                // FIXME: I18N need to have a better error message.
+                // FIXME: Is this the best result code?
+                throw newLdapException(Responses.newResult(
+                        ResultCode.CLIENT_SIDE_LOCAL_ERROR).setDiagnosticMessage(
+                        "An error occurred during multi-stage authentication").setCause(e));
+            }
+        }
+    }
+
+    private String authorizationID;
+
+    ExternalSASLBindRequestImpl() {
+        // Nothing to do.
+    }
+
+    ExternalSASLBindRequestImpl(final ExternalSASLBindRequest externalSASLBindRequest) {
+        super(externalSASLBindRequest);
+        this.authorizationID = externalSASLBindRequest.getAuthorizationID();
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new Client(this, serverName);
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public ExternalSASLBindRequest setAuthorizationID(final String authorizationID) {
+        this.authorizationID = authorizationID;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ExternalSASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        builder.append(", authorizationID=");
+        builder.append(authorizationID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequest.java
new file mode 100644
index 0000000..58df417
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequest.java
@@ -0,0 +1,433 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The GSSAPI SASL bind request as defined in RFC 2831. This SASL mechanism
+ * allows a client to use the Generic Security Service Application Program
+ * Interface (GSS-API) Kerberos V5 to authenticate to the server. This mechanism
+ * can be used to negotiate integrity and/or privacy protection for the
+ * underlying connection.
+ * <p>
+ * The optional authorization identity is specified using an authorization ID,
+ * or {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4752">RFC 4752 - The Kerberos V5
+ *      ("GSSAPI") Simple Authentication and Security Layer (SASL) Mechanism
+ *      </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface GSSAPISASLBindRequest extends SASLBindRequest {
+
+    /**
+     * Indicates that the client will accept authentication only. More
+     * specifically, the underlying connection will not be protected using
+     * integrity protection or encryption, unless previously established using
+     * SSL/TLS. This is the default if no QOP option is present in the bind
+     * request.
+     */
+    String QOP_AUTH = "auth";
+
+    /**
+     * Indicates that the client will accept authentication with connection
+     * integrity protection and encryption.
+     */
+    String QOP_AUTH_CONF = "auth-conf";
+
+    /**
+     * Indicates that the client will accept authentication with connection
+     * integrity protection. More specifically, the underlying connection will
+     * not be encrypted, unless previously established using SSL/TLS.
+     */
+    String QOP_AUTH_INT = "auth-int";
+
+    /**
+     * The name of the SASL mechanism based on GSS-API authentication.
+     */
+    String SASL_MECHANISM_NAME = "GSSAPI";
+
+    /**
+     * Adds the provided additional authentication parameter to the list of
+     * parameters to be passed to the underlying mechanism implementation. This
+     * method is provided in order to allow for future extensions.
+     *
+     * @param name
+     *            The name of the additional authentication parameter.
+     * @param value
+     *            The value of the additional authentication parameter.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit additional
+     *             authentication parameters to be added.
+     * @throws NullPointerException
+     *             If {@code name} or {@code value} was {@code null}.
+     */
+    GSSAPISASLBindRequest addAdditionalAuthParam(String name, String value);
+
+    @Override
+    GSSAPISASLBindRequest addControl(Control control);
+
+    /**
+     * Adds the provided quality of protection (QOP) values to the ordered list
+     * of QOP values that the client is willing to accept. The order of the list
+     * specifies the preference order, high to low. Authentication will fail if
+     * no QOP values are recognized or accepted by the server.
+     * <p>
+     * By default the client will accept {@link #QOP_AUTH AUTH}.
+     *
+     * @param qopValues
+     *            The quality of protection values that the client is willing to
+     *            accept.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit QOP values to be added.
+     * @throws NullPointerException
+     *             If {@code qopValues} was {@code null}.
+     * @see #QOP_AUTH
+     * @see #QOP_AUTH_INT
+     * @see #QOP_AUTH_CONF
+     */
+    GSSAPISASLBindRequest addQOP(String... qopValues);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns a map containing the provided additional authentication
+     * parameters to be passed to the underlying mechanism implementation. This
+     * method is provided in order to allow for future extensions.
+     *
+     * @return A map containing the provided additional authentication
+     *         parameters to be passed to the underlying mechanism
+     *         implementation.
+     */
+    Map<String, String> getAdditionalAuthParams();
+
+    /**
+     * Returns the authentication ID of the user, which should be the user's
+     * Kerberos principal. The authentication ID usually has the form "dn:"
+     * immediately followed by the distinguished name of the user, or "u:"
+     * followed by a user ID string, but other forms are permitted.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @return The authentication ID of the user.
+     */
+    String getAuthenticationID();
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    /**
+     * Returns the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authorization ID of the user, which may be {@code null}.
+     */
+    String getAuthorizationID();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the optional address of the Kerberos KDC (Key Distribution
+     * Center).
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @return The address of the Kerberos KDC (Key Distribution Center), which
+     *         may be {@code null}.
+     */
+    String getKDCAddress();
+
+    /**
+     * Returns the maximum size of the receive buffer in bytes. The actual
+     * maximum number of bytes will be the minimum of this number and the peer's
+     * maximum send buffer size. The default size is 65536.
+     *
+     * @return The maximum size of the receive buffer in bytes.
+     */
+    int getMaxReceiveBufferSize();
+
+    /**
+     * Returns the maximum size of the send buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * receive buffer size. The default size is 65536.
+     *
+     * @return The maximum size of the send buffer in bytes.
+     */
+    int getMaxSendBufferSize();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    /**
+     * Returns the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @return The password of the user that the client wishes to bind as.
+     */
+    byte[] getPassword();
+
+    /**
+     * Returns the ordered list of quality of protection (QOP) values that the
+     * client is willing to accept. The order of the list specifies the
+     * preference order, high to low. Authentication will fail if no QOP values
+     * are recognized or accepted by the server.
+     * <p>
+     * By default the client will accept {@link #QOP_AUTH AUTH}.
+     *
+     * @return The list of quality of protection values that the client is
+     *         willing to accept. The returned list may be empty indicating that
+     *         the default QOP will be accepted.
+     */
+    List<String> getQOPs();
+
+    /**
+     * Returns the optional realm containing the user's account.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @return The name of the realm containing the user's account, which may be
+     *         {@code null}.
+     */
+    String getRealm();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Returns the Kerberos subject of the user to be authenticated.
+     * <p>
+     * <b>NOTE</b>: if a {@code Subject} is specified then the authentication
+     * ID, KDC address, password, and realm, will be ignored.
+     *
+     * @return The Kerberos subject of the user to be authenticated.
+     */
+    Subject getSubject();
+
+    /**
+     * Returns {@code true} if the server must authenticate to the client. The
+     * default is {@code false}.
+     *
+     * @return {@code true} if the server must authenticate to the client.
+     */
+    boolean isServerAuth();
+
+    /**
+     * Sets the authentication ID of the user, which should be the user's
+     * Kerberos principal. The authentication ID usually has the form "dn:"
+     * immediately followed by the distinguished name of the user, or "u:"
+     * followed by a user ID string, but other forms are permitted.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user.
+     * @return This bind request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authenticationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws NullPointerException
+     *             If {@code authenticationID} was {@code null}.
+     */
+    GSSAPISASLBindRequest setAuthenticationID(String authenticationID);
+
+    /**
+     * Sets the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authorizationID
+     *            The authorization ID of the user, which may be {@code null}.
+     * @return This bind request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     */
+    GSSAPISASLBindRequest setAuthorizationID(String authorizationID);
+
+    /**
+     * Sets the optional address of the Kerberos KDC (Key Distribution Center).
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @param address
+     *            The address of the Kerberos KDC (Key Distribution Center),
+     *            which may be {@code null}.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the KDC address to be
+     *             set.
+     * @throws NullPointerException
+     *             If {@code address} was {@code null}.
+     */
+    GSSAPISASLBindRequest setKDCAddress(String address);
+
+    /**
+     * Sets the maximum size of the receive buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * send buffer size. The default size is 65536.
+     *
+     * @param size
+     *            The maximum size of the receive buffer in bytes.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the buffer size to be
+     *             set.
+     */
+    GSSAPISASLBindRequest setMaxReceiveBufferSize(int size);
+
+    /**
+     * Sets the maximum size of the send buffer in bytes. The actual maximum
+     * number of bytes will be the minimum of this number and the peer's maximum
+     * receive buffer size. The default size is 65536.
+     *
+     * @param size
+     *            The maximum size of the send buffer in bytes.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the buffer size to be
+     *             set.
+     */
+    GSSAPISASLBindRequest setMaxSendBufferSize(int size);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * provided password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as,
+     *            which may be empty.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    GSSAPISASLBindRequest setPassword(byte[] password);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as. The
+     * password will be converted to a UTF-8 octet string.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    GSSAPISASLBindRequest setPassword(char[] password);
+
+    /**
+     * Sets the optional realm containing the user's account.
+     * <p>
+     * <b>NOTE</b>: this will not be used if a {@code Subject} is specified.
+     *
+     * @param realm
+     *            The name of the realm containing the user's account, which may
+     *            be {@code null}.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the realm to be set.
+     * @throws NullPointerException
+     *             If {@code realm} was {@code null}.
+     */
+    GSSAPISASLBindRequest setRealm(String realm);
+
+    /**
+     * Specifies whether or not the server must authenticate to the client. The
+     * default is {@code false}.
+     *
+     * @param serverAuth
+     *            {@code true} if the server must authenticate to the client or
+     *            {@code false} otherwise.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit server auth to be set.
+     */
+    GSSAPISASLBindRequest setServerAuth(boolean serverAuth);
+
+    /**
+     * Sets the Kerberos subject of the user to be authenticated.
+     * <p>
+     * <b>NOTE</b>: if a {@code Subject} is specified then the authentication
+     * ID, KDC address, password, and realm, will be ignored.
+     *
+     * @param subject
+     *            The Kerberos subject of the user to be authenticated.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the Kerberos subject to
+     *             be set.
+     * @throws NullPointerException
+     *             If {@code subject} was {@code null}.
+     */
+    GSSAPISASLBindRequest setSubject(Subject subject);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestImpl.java
new file mode 100644
index 0000000..0b5e057
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestImpl.java
@@ -0,0 +1,503 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SASL_CONTEXT_CREATE_ERROR;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SASL_PROTOCOL_ERROR;
+import static com.forgerock.opendj.util.StaticUtils.copyOfBytes;
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Utils;
+
+import com.forgerock.opendj.util.StaticUtils;
+import com.sun.security.auth.callback.TextCallbackHandler;
+import com.sun.security.auth.module.Krb5LoginModule;
+
+/**
+ * GSSAPI SASL bind request implementation.
+ */
+@SuppressWarnings("restriction")
+final class GSSAPISASLBindRequestImpl extends AbstractSASLBindRequest<GSSAPISASLBindRequest>
+        implements GSSAPISASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private static Subject kerberos5Login(final String authenticationID,
+                final ByteString password, final String realm, final String kdc) throws LdapException {
+            if (authenticationID == null) {
+                // FIXME: I18N need to have a better error message.
+                // FIXME: Is this the best result code?
+                throw newLdapException(Responses.newResult(
+                        ResultCode.CLIENT_SIDE_LOCAL_ERROR).setDiagnosticMessage(
+                        "No authentication ID specified for GSSAPI SASL authentication"));
+            }
+
+            if (password == null) {
+                // FIXME: I18N need to have a better error message.
+                // FIXME: Is this the best result code?
+                throw newLdapException(Responses.newResult(
+                        ResultCode.CLIENT_SIDE_LOCAL_ERROR).setDiagnosticMessage(
+                        "No password specified for GSSAPI SASL authentication"));
+            }
+
+            final Map<String, Object> state = new HashMap<>();
+            state.put("javax.security.auth.login.name", authenticationID);
+            state.put("javax.security.auth.login.password", password.toString().toCharArray());
+            state.put("javax.security.auth.useSubjectCredsOnly", "true");
+            state.put("java.security.krb5.realm", realm);
+            state.put("java.security.krb5.kdc", kdc);
+
+            final Map<String, Object> options = new HashMap<>();
+            options.put("tryFirstPass", "true");
+            options.put("useTicketCache", "true");
+            options.put("doNotPrompt", "true");
+            options.put("storePass", "false");
+            options.put("forwardable", "true");
+
+            final Subject subject = new Subject();
+            final Krb5LoginModule login = new Krb5LoginModule();
+            login.initialize(subject, new TextCallbackHandler(), state, options);
+            try {
+                if (login.login()) {
+                    login.commit();
+                }
+            } catch (final LoginException e) {
+                // FIXME: Is this the best result code?
+                final LocalizableMessage message =
+                        ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get(StaticUtils
+                                .getExceptionMessage(e));
+                throw newLdapException(Responses.newResult(
+                        ResultCode.CLIENT_SIDE_LOCAL_ERROR)
+                        .setDiagnosticMessage(message.toString()).setCause(e));
+            }
+            return subject;
+        }
+
+        private final String authorizationID;
+        private final PrivilegedExceptionAction<Boolean> evaluateAction =
+                new PrivilegedExceptionAction<Boolean>() {
+                    @Override
+                    public Boolean run() throws LdapException {
+                        if (saslClient.isComplete()) {
+                            return true;
+                        }
+
+                        try {
+                            setNextSASLCredentials(saslClient.evaluateChallenge(lastResult
+                                    .getServerSASLCredentials() == null ? new byte[0] : lastResult
+                                    .getServerSASLCredentials().toByteArray()));
+                            return saslClient.isComplete();
+                        } catch (final SaslException e) {
+                            // FIXME: I18N need to have a better error message.
+                            // FIXME: Is this the best result code?
+                            throw newLdapException(Responses.newResult(
+                                    ResultCode.CLIENT_SIDE_LOCAL_ERROR).setDiagnosticMessage(
+                                    "An error occurred during multi-stage authentication")
+                                    .setCause(e));
+                        }
+                    }
+                };
+        private BindResult lastResult;
+
+        private final SaslClient saslClient;
+
+        private final Subject subject;
+
+        private Client(final GSSAPISASLBindRequestImpl initialBindRequest, final String serverName)
+                throws LdapException {
+            super(initialBindRequest);
+
+            this.authorizationID = initialBindRequest.getAuthorizationID();
+            if (initialBindRequest.getSubject() != null) {
+                this.subject = initialBindRequest.getSubject();
+            } else {
+                this.subject =
+                        kerberos5Login(initialBindRequest.getAuthenticationID(), ByteString
+                                .wrap(initialBindRequest.getPassword()), initialBindRequest
+                                .getRealm(), initialBindRequest.getKDCAddress());
+            }
+
+            try {
+                this.saslClient =
+                        Subject.doAs(subject, new PrivilegedExceptionAction<SaslClient>() {
+                            @Override
+                            public SaslClient run() throws LdapException {
+                                // Create property map containing all the parameters.
+                                final Map<String, String> props = new HashMap<>();
+
+                                final List<String> qopValues = initialBindRequest.getQOPs();
+                                if (!qopValues.isEmpty()) {
+                                    props.put(Sasl.QOP, Utils.joinAsString(",", qopValues));
+                                }
+
+                                final Boolean serverAuth = initialBindRequest.isServerAuth();
+                                if (serverAuth != null) {
+                                    props.put(Sasl.SERVER_AUTH, String.valueOf(serverAuth));
+                                }
+
+                                Integer size = initialBindRequest.getMaxReceiveBufferSize();
+                                if (size != null) {
+                                    props.put(Sasl.MAX_BUFFER, String.valueOf(size));
+                                }
+
+                                size = initialBindRequest.getMaxSendBufferSize();
+                                if (size != null) {
+                                    props.put("javax.security.sasl.sendmaxbuffer", String
+                                            .valueOf(size));
+                                }
+
+                                for (final Map.Entry<String, String> e : initialBindRequest
+                                        .getAdditionalAuthParams().entrySet()) {
+                                    props.put(e.getKey(), e.getValue());
+                                }
+
+                                try {
+                                    final SaslClient saslClient =
+                                            Sasl.createSaslClient(
+                                                    new String[] { SASL_MECHANISM_NAME },
+                                                    authorizationID, SASL_DEFAULT_PROTOCOL,
+                                                    serverName, props, Client.this);
+                                    if (saslClient.hasInitialResponse()) {
+                                        setNextSASLCredentials(saslClient
+                                                .evaluateChallenge(new byte[0]));
+                                    } else {
+                                        setNextSASLCredentials((ByteString) null);
+                                    }
+                                    return saslClient;
+                                } catch (final SaslException e) {
+                                    throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e);
+                                }
+                            }
+                        });
+            } catch (final PrivilegedActionException e) {
+                if (e.getCause() instanceof LdapException) {
+                    throw (LdapException) e.getCause();
+                } else {
+                    // This should not happen. Must be a bug.
+                    final LocalizableMessage msg =
+                            ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_NAME,
+                                    getExceptionMessage(e));
+                    throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, msg.toString(), e);
+                }
+            }
+        }
+
+        @Override
+        public void dispose() {
+            try {
+                saslClient.dispose();
+            } catch (final SaslException ignored) {
+                // Ignore the SASL exception.
+            }
+        }
+
+        @Override
+        public boolean evaluateResult(final BindResult result) throws LdapException {
+            this.lastResult = result;
+            try {
+                return Subject.doAs(subject, evaluateAction);
+            } catch (final PrivilegedActionException e) {
+                if (e.getCause() instanceof LdapException) {
+                    throw (LdapException) e.getCause();
+                } else {
+                    // This should not happen. Must be a bug.
+                    final LocalizableMessage msg =
+                            ERR_SASL_PROTOCOL_ERROR
+                                    .get(SASL_MECHANISM_NAME, getExceptionMessage(e));
+                    throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, msg.toString(), e);
+                }
+            }
+        }
+
+        @Override
+        public ConnectionSecurityLayer getConnectionSecurityLayer() {
+            final String qop = (String) saslClient.getNegotiatedProperty(Sasl.QOP);
+            if ("auth-int".equalsIgnoreCase(qop) || "auth-conf".equalsIgnoreCase(qop)) {
+                return this;
+            }
+            return null;
+        }
+
+        @Override
+        public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws LdapException {
+            try {
+                return saslClient.unwrap(incoming, offset, len);
+            } catch (final SaslException e) {
+                final LocalizableMessage msg =
+                        ERR_SASL_PROTOCOL_ERROR.get(SASL_MECHANISM_NAME, getExceptionMessage(e));
+                throw newLdapException(ResultCode.CLIENT_SIDE_DECODING_ERROR, msg.toString(), e);
+            }
+        }
+
+        @Override
+        public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws LdapException {
+            try {
+                return saslClient.wrap(outgoing, offset, len);
+            } catch (final SaslException e) {
+                final LocalizableMessage msg =
+                        ERR_SASL_PROTOCOL_ERROR.get(SASL_MECHANISM_NAME, getExceptionMessage(e));
+                throw newLdapException(ResultCode.CLIENT_SIDE_ENCODING_ERROR, msg.toString(), e);
+            }
+        }
+
+    }
+
+    private final Map<String, String> additionalAuthParams = new LinkedHashMap<>();
+
+    /** Ignored if subject is non-null. */
+    private String authenticationID;
+    /** Optional authorization ID. */
+    private String authorizationID;
+    private String kdcAddress;
+
+    private Integer maxReceiveBufferSize;
+    private Integer maxSendBufferSize;
+
+    private byte[] password;
+    private final List<String> qopValues = new LinkedList<>();
+    private String realm;
+    /**
+     * Don't use primitives for these so that we can distinguish between default
+     * settings (null) and values set by the caller.
+     */
+    private Boolean serverAuth;
+    /** If null then authenticationID and password must be present. */
+    private Subject subject;
+
+    GSSAPISASLBindRequestImpl(final GSSAPISASLBindRequest gssapiSASLBindRequest) {
+        super(gssapiSASLBindRequest);
+        this.subject = gssapiSASLBindRequest.getSubject();
+
+        this.authenticationID = gssapiSASLBindRequest.getAuthenticationID();
+        this.password = copyOfBytes(gssapiSASLBindRequest.getPassword());
+        this.realm = gssapiSASLBindRequest.getRealm();
+
+        this.kdcAddress = gssapiSASLBindRequest.getKDCAddress();
+
+        this.authorizationID = gssapiSASLBindRequest.getAuthorizationID();
+
+        this.additionalAuthParams.putAll(gssapiSASLBindRequest.getAdditionalAuthParams());
+        this.qopValues.addAll(gssapiSASLBindRequest.getQOPs());
+
+        this.serverAuth = gssapiSASLBindRequest.isServerAuth();
+        this.maxReceiveBufferSize = gssapiSASLBindRequest.getMaxReceiveBufferSize();
+        this.maxSendBufferSize = gssapiSASLBindRequest.getMaxSendBufferSize();
+    }
+
+    GSSAPISASLBindRequestImpl(final String authenticationID, final byte[] password) {
+        Reject.ifNull(authenticationID, password);
+        this.authenticationID = authenticationID;
+        this.password = password;
+    }
+
+    GSSAPISASLBindRequestImpl(final Subject subject) {
+        Reject.ifNull(subject);
+        this.subject = subject;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest addAdditionalAuthParam(final String name, final String value) {
+        Reject.ifNull(name, value);
+        additionalAuthParams.put(name, value);
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest addQOP(final String... qopValues) {
+        for (final String qopValue : qopValues) {
+            this.qopValues.add(Reject.checkNotNull(qopValue));
+        }
+        return this;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new Client(this, serverName);
+    }
+
+    @Override
+    public Map<String, String> getAdditionalAuthParams() {
+        return additionalAuthParams;
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return authenticationID;
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getKDCAddress() {
+        return kdcAddress;
+    }
+
+    @Override
+    public int getMaxReceiveBufferSize() {
+        return maxReceiveBufferSize == null ? 65536 : maxReceiveBufferSize;
+    }
+
+    @Override
+    public int getMaxSendBufferSize() {
+        return maxSendBufferSize == null ? 65536 : maxSendBufferSize;
+    }
+
+    @Override
+    public byte[] getPassword() {
+        return password;
+    }
+
+    @Override
+    public List<String> getQOPs() {
+        return qopValues;
+    }
+
+    @Override
+    public String getRealm() {
+        return realm;
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public Subject getSubject() {
+        return subject;
+    }
+
+    @Override
+    public boolean isServerAuth() {
+        return serverAuth == null ? false : serverAuth;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setAuthenticationID(final String authenticationID) {
+        Reject.ifNull(authenticationID);
+        this.authenticationID = authenticationID;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setAuthorizationID(final String authorizationID) {
+        this.authorizationID = authorizationID;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setKDCAddress(final String address) {
+        this.kdcAddress = address;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setMaxReceiveBufferSize(final int size) {
+        maxReceiveBufferSize = size;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setMaxSendBufferSize(final int size) {
+        maxSendBufferSize = size;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setPassword(final byte[] password) {
+        Reject.ifNull(password);
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setPassword(final char[] password) {
+        Reject.ifNull(password);
+        this.password = StaticUtils.getBytes(password);
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setRealm(final String realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setServerAuth(final boolean serverAuth) {
+        this.serverAuth = serverAuth;
+        return this;
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setSubject(final Subject subject) {
+        this.subject = subject;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GSSAPISASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        if (subject != null) {
+            builder.append(", subject=");
+            builder.append(subject);
+        } else {
+            builder.append(", authenticationID=");
+            builder.append(authenticationID);
+            builder.append(", authorizationID=");
+            builder.append(authorizationID);
+            builder.append(", realm=");
+            builder.append(realm);
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequest.java
new file mode 100644
index 0000000..c594413
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequest.java
@@ -0,0 +1,126 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A generic Bind request which should be used for unsupported authentication
+ * methods. Servers that do not support a choice supplied by a client return a
+ * Bind response with the result code set to
+ * {@link ResultCode#AUTH_METHOD_NOT_SUPPORTED}.
+ */
+public interface GenericBindRequest extends BindRequest {
+
+    @Override
+    GenericBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    @Override
+    byte getAuthenticationType();
+
+    /**
+     * Returns the authentication information for this bind request. The content
+     * is defined by the authentication mechanism.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned byte array, allowing applications to overwrite any sensitive
+     * data such as passwords after it has been used.
+     *
+     * @return The authentication information.
+     */
+    byte[] getAuthenticationValue();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getName();
+
+    /**
+     * Sets the authentication mechanism identifier for this generic bind
+     * request. Note that value {@code 0} is reserved for simple authentication,
+     * {@code 1} and {@code 2} are reserved but unused, and {@code 3} is
+     * reserved for SASL authentication.
+     *
+     * @param type
+     *            The authentication mechanism identifier for this generic bind
+     *            request.
+     * @return This generic bind request.
+     * @throws UnsupportedOperationException
+     *             If this generic bind request does not permit the
+     *             authentication type to be set.
+     */
+    GenericBindRequest setAuthenticationType(byte type);
+
+    /**
+     * Sets the authentication information for this generic bind request in a
+     * form defined by the authentication mechanism.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned byte array, allowing applications to overwrite any sensitive
+     * data such as passwords after it has been used.
+     *
+     * @param bytes
+     *            The authentication information for this generic bind request
+     *            in a form defined by the authentication mechanism.
+     * @return This generic bind request.
+     * @throws UnsupportedOperationException
+     *             If this generic bind request does not permit the
+     *             authentication bytes to be set.
+     * @throws NullPointerException
+     *             If {@code bytes} was {@code null}.
+     */
+    GenericBindRequest setAuthenticationValue(byte[] bytes);
+
+    /**
+     * Sets the name of the Directory object that the client wishes to bind as.
+     * The name may be empty (but never {@code null} when used for of anonymous
+     * binds, or when using SASL authentication. The server shall not
+     * dereference any aliases in locating the named object.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @param name
+     *            The name of the Directory object that the client wishes to
+     *            bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    GenericBindRequest setName(String name);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequestImpl.java
new file mode 100644
index 0000000..52e553a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericBindRequestImpl.java
@@ -0,0 +1,121 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.copyOfBytes;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.LdapException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Generic bind request implementation.
+ */
+final class GenericBindRequestImpl extends AbstractBindRequest<GenericBindRequest> implements
+        GenericBindRequest {
+    private byte authenticationType;
+
+    private byte[] authenticationValue;
+
+    private final BindClient bindClient;
+
+    private String name;
+
+    GenericBindRequestImpl(final GenericBindRequest genericBindRequest) {
+        super(genericBindRequest);
+        this.name = genericBindRequest.getName();
+        this.authenticationType = genericBindRequest.getAuthenticationType();
+        this.authenticationValue = copyOfBytes(genericBindRequest.getAuthenticationValue());
+        this.bindClient = null; // Create a new bind client each time.
+    }
+
+    GenericBindRequestImpl(final String name, final byte authenticationType,
+            final byte[] authenticationValue) {
+        this.name = name;
+        this.authenticationType = authenticationType;
+        this.authenticationValue = authenticationValue;
+        this.bindClient = null; // Create a new bind client each time.
+    }
+
+    GenericBindRequestImpl(final String name, final byte authenticationType,
+            final byte[] authenticationValue, final BindClient bindClient) {
+        this.name = name;
+        this.authenticationType = authenticationType;
+        this.authenticationValue = authenticationValue;
+        this.bindClient = bindClient; // Always return same bind client.
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        if (bindClient != null) {
+            return bindClient;
+        }
+        return new BindClientImpl(this).setNextAuthenticationValue(authenticationValue);
+    }
+
+    @Override
+    public byte getAuthenticationType() {
+        return authenticationType;
+    }
+
+    @Override
+    public byte[] getAuthenticationValue() {
+        return authenticationValue;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public GenericBindRequest setAuthenticationType(final byte type) {
+        this.authenticationType = type;
+        return this;
+    }
+
+    @Override
+    public GenericBindRequest setAuthenticationValue(final byte[] bytes) {
+        Reject.ifNull(bytes);
+        this.authenticationValue = bytes;
+        return this;
+    }
+
+    @Override
+    public GenericBindRequest setName(final String name) {
+        Reject.ifNull(name);
+        this.name = name;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GenericBindRequest(name=");
+        builder.append(getName());
+        builder.append(", authenticationType=");
+        builder.append(getAuthenticationType());
+        builder.append(", authenticationValue=");
+        builder.append(ByteString.wrap(getAuthenticationValue()));
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequest.java
new file mode 100644
index 0000000..32d79d0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequest.java
@@ -0,0 +1,99 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+
+/**
+ * A generic Extended request which should be used for unsupported extended
+ * operations. Servers list the names of Extended requests they recognize in the
+ * {@code supportedExtension} attribute in the root DSE. Where the name is not
+ * recognized, the server returns
+ * {@link org.forgerock.opendj.ldap.ResultCode#PROTOCOL_ERROR} (the server may
+ * return this error in other cases).
+ */
+public interface GenericExtendedRequest extends ExtendedRequest<GenericExtendedResult> {
+    /**
+     * A decoder which can be used to decode generic extended operation
+     * requests.
+     */
+    ExtendedRequestDecoder<GenericExtendedRequest, GenericExtendedResult> DECODER =
+            new GenericExtendedRequestImpl.RequestDecoder();
+
+    @Override
+    GenericExtendedRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getOID();
+
+    @Override
+    ExtendedResultDecoder<GenericExtendedResult> getResultDecoder();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    /**
+     * Sets the numeric OID associated with this extended request.
+     *
+     * @param oid
+     *            The numeric OID associated with this extended request.
+     * @return This generic extended request.
+     * @throws UnsupportedOperationException
+     *             If this generic extended request does not permit the request
+     *             name to be set.
+     * @throws NullPointerException
+     *             If {@code oid} was {@code null}.
+     */
+    GenericExtendedRequest setOID(String oid);
+
+    /**
+     * Sets the value, if any, associated with this extended request. Its format
+     * is defined by the specification of this extended request.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            TThe value associated with this extended request, or
+     *            {@code null} if there is no value. Its format is defined by
+     *            the specification of this control.
+     * @return This generic extended request.
+     * @throws UnsupportedOperationException
+     *             If this generic extended request does not permit the request
+     *             value to be set.
+     */
+    GenericExtendedRequest setValue(Object value);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestImpl.java
new file mode 100644
index 0000000..635ed9e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestImpl.java
@@ -0,0 +1,153 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Generic extended request implementation.
+ */
+final class GenericExtendedRequestImpl extends
+        AbstractExtendedRequest<GenericExtendedRequest, GenericExtendedResult> implements
+        GenericExtendedRequest {
+
+    static final class RequestDecoder implements
+            ExtendedRequestDecoder<GenericExtendedRequest, GenericExtendedResult> {
+        @Override
+        public GenericExtendedRequest decodeExtendedRequest(final ExtendedRequest<?> request,
+                final DecodeOptions options) throws DecodeException {
+            if (request instanceof GenericExtendedRequest) {
+                return (GenericExtendedRequest) request;
+            } else {
+                final GenericExtendedRequest newRequest =
+                        new GenericExtendedRequestImpl(request.getOID()).setValue(request
+                                .getValue());
+
+                for (final Control control : request.getControls()) {
+                    newRequest.addControl(control);
+                }
+
+                return newRequest;
+            }
+        }
+    }
+
+    private static final class GenericExtendedResultDecoder extends
+            AbstractExtendedResultDecoder<GenericExtendedResult> {
+
+        @Override
+        public GenericExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            if (result instanceof GenericExtendedResult) {
+                return (GenericExtendedResult) result;
+            } else {
+                final GenericExtendedResult newResult =
+                        Responses.newGenericExtendedResult(result.getResultCode()).setMatchedDN(
+                                result.getMatchedDN()).setDiagnosticMessage(
+                                result.getDiagnosticMessage()).setOID(result.getOID()).setValue(
+                                result.getValue());
+                for (final Control control : result.getControls()) {
+                    newResult.addControl(control);
+                }
+                return newResult;
+            }
+        }
+
+        @Override
+        public GenericExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newGenericExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+    }
+
+    private static final GenericExtendedResultDecoder RESULT_DECODER =
+            new GenericExtendedResultDecoder();
+
+    private String requestName;
+    private ByteString requestValue;
+
+    GenericExtendedRequestImpl(final GenericExtendedRequest genericExtendedRequest) {
+        super(genericExtendedRequest);
+        this.requestName = genericExtendedRequest.getOID();
+        this.requestValue = genericExtendedRequest.getValue();
+    }
+
+    GenericExtendedRequestImpl(final String requestName) {
+        this.requestName = requestName;
+    }
+
+    @Override
+    public String getOID() {
+        return requestName;
+    }
+
+    @Override
+    public ExtendedResultDecoder<GenericExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return requestValue;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return requestValue != null;
+    }
+
+    @Override
+    public GenericExtendedRequest setOID(final String oid) {
+        Reject.ifNull(oid);
+        this.requestName = oid;
+        return this;
+    }
+
+    @Override
+    public GenericExtendedRequest setValue(final Object value) {
+        this.requestValue = value != null ? ByteString.valueOfObject(value) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GenericExtendedRequest(requestName=");
+        builder.append(getOID());
+        if (hasValue()) {
+            builder.append(", requestValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequest.java
new file mode 100644
index 0000000..c206ca8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequest.java
@@ -0,0 +1,220 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * The Modify DN operation allows a client to change the Relative Distinguished
+ * Name (RDN) of an entry in the Directory and/or to move a subtree of entries
+ * to a new location in the Directory.
+ */
+public interface ModifyDNRequest extends Request, ChangeRecord {
+
+    @Override
+    <R, P> R accept(ChangeRecordVisitor<R, P> v, P p);
+
+    @Override
+    ModifyDNRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the distinguished name of the entry to be renamed. This entry may
+     * or may not have subordinate entries. The server shall not dereference any
+     * aliases in locating the entry to be renamed.
+     *
+     * @return The distinguished name of the entry.
+     */
+    @Override
+    DN getName();
+
+    /**
+     * Returns the new RDN of the entry to be renamed. The value of the old RDN
+     * is supplied when moving the entry to a new superior without changing its
+     * RDN. Attribute values of the new RDN not matching any attribute value of
+     * the entry are added to the entry, and an appropriate error is returned if
+     * this fails.
+     *
+     * @return The new RDN of the entry.
+     */
+    RDN getNewRDN();
+
+    /**
+     * Returns the distinguished name of an existing entry that will become the
+     * immediate superior (parent) of the entry to be renamed. The server shall
+     * not dereference any aliases in locating the new superior entry. The
+     * default value is {@code null}, indicating that the entry is to remain
+     * under the same parent entry.
+     *
+     * @return The distinguished name of the new superior entry, or {@code null}
+     *         if the entry is to remain under the same parent entry.
+     */
+    DN getNewSuperior();
+
+    /**
+     * Indicates whether the old RDN attribute values are to be retained as
+     * attributes of the entry or deleted from the entry. The default value is
+     * {@code false}.
+     *
+     * @return {@code true} if the old RDN attribute values are to be deleted
+     *         from the entry, or {@code false} if they are to be retained.
+     */
+    boolean isDeleteOldRDN();
+
+    /**
+     * Specifies whether the old RDN attribute values are to be retained as
+     * attributes of the entry or deleted from the entry. The default value is
+     * {@code false}.
+     *
+     * @param deleteOldRDN
+     *            {@code true} if the old RDN attribute values are to be deleted
+     *            from the entry, or {@code false} if they are to be retained.
+     * @return This modify DN request.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the delete old RDN
+     *             parameter to be set.
+     */
+    ModifyDNRequest setDeleteOldRDN(boolean deleteOldRDN);
+
+    /**
+     * Sets the distinguished name of the entry to be renamed. This entry may or
+     * may not have subordinate entries. The server shall not dereference any
+     * aliases in locating the entry to be renamed.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be renamed.
+     * @return This modify DN request.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the distinguished
+     *             name to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    ModifyDNRequest setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the entry to be renamed. This entry may or
+     * may not have subordinate entries. The server shall not dereference any
+     * aliases in locating the entry to be renamed.
+     *
+     * @param dn
+     *            The distinguished name of the entry to be renamed.
+     * @return This modify DN request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the distinguished
+     *             name to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    ModifyDNRequest setName(String dn);
+
+    /**
+     * Sets the new RDN of the entry to be renamed. The value of the old RDN is
+     * supplied when moving the entry to a new superior without changing its
+     * RDN. Attribute values of the new RDN not matching any attribute value of
+     * the entry are added to the entry, and an appropriate error is returned if
+     * this fails.
+     *
+     * @param rdn
+     *            The new RDN of the entry to be renamed.
+     * @return This modify DN request.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the new RDN to be
+     *             set.
+     * @throws NullPointerException
+     *             If {@code rdn} was {@code null}.
+     */
+    ModifyDNRequest setNewRDN(RDN rdn);
+
+    /**
+     * Sets the new RDN of the entry to be renamed. The value of the old RDN is
+     * supplied when moving the entry to a new superior without changing its
+     * RDN. Attribute values of the new RDN not matching any attribute value of
+     * the entry are added to the entry, and an appropriate error is returned if
+     * this fails.
+     *
+     * @param rdn
+     *            The new RDN of the entry to be renamed.
+     * @return This modify DN request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code rdn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the new RDN to be
+     *             set.
+     * @throws NullPointerException
+     *             If {@code rdn} was {@code null}.
+     */
+    ModifyDNRequest setNewRDN(String rdn);
+
+    /**
+     * Sets the distinguished name of an existing entry that will become the
+     * immediate superior (parent) of the entry to be renamed. The server shall
+     * not dereference any aliases in locating the new superior entry. The
+     * default value is {@code null}, indicating that the entry is to remain
+     * under the same parent entry.
+     *
+     * @param dn
+     *            The distinguished name of an existing entry that will become
+     *            the immediate superior (parent) of the entry to be renamed,
+     *            may be {@code null}.
+     * @return This modify DN request.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the new superior to
+     *             be set.
+     */
+    ModifyDNRequest setNewSuperior(DN dn);
+
+    /**
+     * Sets the distinguished name of an existing entry that will become the
+     * immediate superior (parent) of the entry to be renamed. The server shall
+     * not dereference any aliases in locating the new superior entry. The
+     * default value is {@code null}, indicating that the entry is to remain
+     * under the same parent entry.
+     *
+     * @param dn
+     *            The distinguished name of an existing entry that will become
+     *            the immediate superior (parent) of the entry to be renamed,
+     *            may be {@code null}.
+     * @return This modify DN request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this modify DN request does not permit the new superior to
+     *             be set.
+     */
+    ModifyDNRequest setNewSuperior(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestImpl.java
new file mode 100644
index 0000000..351c9d5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestImpl.java
@@ -0,0 +1,142 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Modify DN request implementation.
+ */
+final class ModifyDNRequestImpl extends AbstractRequestImpl<ModifyDNRequest> implements
+        ModifyDNRequest {
+    private boolean deleteOldRDN;
+    private DN name;
+    private RDN newRDN;
+    private DN newSuperior;
+
+    ModifyDNRequestImpl(final DN name, final RDN newRDN) {
+        this.name = name;
+        this.newRDN = newRDN;
+    }
+
+    ModifyDNRequestImpl(final ModifyDNRequest modifyDNRequest) {
+        super(modifyDNRequest);
+        this.name = modifyDNRequest.getName();
+        this.newSuperior = modifyDNRequest.getNewSuperior();
+        this.newRDN = modifyDNRequest.getNewRDN();
+        this.deleteOldRDN = modifyDNRequest.isDeleteOldRDN();
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public DN getName() {
+        return name;
+    }
+
+    @Override
+    public RDN getNewRDN() {
+        return newRDN;
+    }
+
+    @Override
+    public DN getNewSuperior() {
+        return newSuperior;
+    }
+
+    @Override
+    public boolean isDeleteOldRDN() {
+        return deleteOldRDN;
+    }
+
+    @Override
+    public ModifyDNRequestImpl setDeleteOldRDN(final boolean deleteOldRDN) {
+        this.deleteOldRDN = deleteOldRDN;
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setName(final DN dn) {
+        Reject.ifNull(dn);
+        this.name = dn;
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setName(final String dn) {
+        Reject.ifNull(dn);
+        this.name = DN.valueOf(dn);
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setNewRDN(final RDN rdn) {
+        Reject.ifNull(rdn);
+        this.newRDN = rdn;
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setNewRDN(final String rdn) {
+        Reject.ifNull(rdn);
+        this.newRDN = RDN.valueOf(rdn);
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setNewSuperior(final DN dn) {
+        this.newSuperior = dn;
+        return this;
+    }
+
+    @Override
+    public ModifyDNRequest setNewSuperior(final String dn) {
+        this.newSuperior = (dn != null) ? DN.valueOf(dn) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ModifyDNRequest(name=");
+        builder.append(getName());
+        builder.append(", newRDN=");
+        builder.append(getNewRDN());
+        builder.append(", deleteOldRDN=");
+        builder.append(isDeleteOldRDN());
+        builder.append(", newSuperior=");
+        builder.append(getNewSuperior());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    ModifyDNRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequest.java
new file mode 100644
index 0000000..8066f91
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequest.java
@@ -0,0 +1,160 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * The Modify operation allows a client to request that a modification of an
+ * entry be performed on its behalf by a server.
+ * <p>
+ * The following example adds a member to a static group entry.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String groupDN = ...;
+ * String memberDN = ...;
+ *
+ * ModifyRequest addMember = Requests.newModifyRequest(groupDN)
+ *         .addModification(ModificationType.ADD, "member", memberDN);
+ * connection.modify(addMember);
+ * </pre>
+ */
+public interface ModifyRequest extends Request, ChangeRecord {
+
+    @Override
+    <R, P> R accept(ChangeRecordVisitor<R, P> v, P p);
+
+    @Override
+    ModifyRequest addControl(Control control);
+
+    /**
+     * Appends the provided modification to the list of modifications included
+     * with this modify request.
+     *
+     * @param modification
+     *            The modification to be performed.
+     * @return This modify request.
+     * @throws UnsupportedOperationException
+     *             If this modify request does not permit modifications to be
+     *             added.
+     * @throws NullPointerException
+     *             If {@code modification} was {@code null}.
+     */
+    ModifyRequest addModification(Modification modification);
+
+    /**
+     * Appends the provided modification to the list of modifications included
+     * with this modify request.
+     * <p>
+     * If the attribute value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param type
+     *            The type of modification to be performed.
+     * @param attributeDescription
+     *            The name of the attribute to be modified.
+     * @param values
+     *            The attribute values to be modified.
+     * @return This modify request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the default schema.
+     * @throws UnsupportedOperationException
+     *             If this modify request does not permit modifications to be
+     *             added.
+     * @throws NullPointerException
+     *             If {@code type}, {@code attributeDescription}, or
+     *             {@code value} was {@code null}.
+     */
+    ModifyRequest addModification(ModificationType type, String attributeDescription,
+            Object... values);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns a {@code List} containing the modifications included with this
+     * modify request. The returned {@code List} may be modified if permitted by
+     * this modify request.
+     *
+     * @return A {@code List} containing the modifications.
+     */
+    List<Modification> getModifications();
+
+    /**
+     * Returns the distinguished name of the entry to be modified. The server
+     * shall not perform any alias dereferencing in determining the object to be
+     * modified.
+     *
+     * @return The distinguished name of the entry to be modified.
+     */
+    @Override
+    DN getName();
+
+    /**
+     * Sets the distinguished name of the entry to be modified. The server shall
+     * not perform any alias dereferencing in determining the object to be
+     * modified.
+     *
+     * @param dn
+     *            The the distinguished name of the entry to be modified.
+     * @return This modify request.
+     * @throws UnsupportedOperationException
+     *             If this modify request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    ModifyRequest setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the entry to be modified. The server shall
+     * not perform any alias dereferencing in determining the object to be
+     * modified.
+     *
+     * @param dn
+     *            The the distinguished name of the entry to be modified.
+     * @return This modify request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this modify request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    ModifyRequest setName(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequestImpl.java
new file mode 100644
index 0000000..37c55e3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/ModifyRequestImpl.java
@@ -0,0 +1,118 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+import org.forgerock.util.Reject;
+
+/**
+ * Modify request implementation.
+ */
+final class ModifyRequestImpl extends AbstractRequestImpl<ModifyRequest> implements ModifyRequest {
+    private final List<Modification> changes = new LinkedList<>();
+    private DN name;
+
+    ModifyRequestImpl(final DN name) {
+        this.name = name;
+    }
+
+    ModifyRequestImpl(final ModifyRequest modifyRequest) {
+        super(modifyRequest);
+        this.name = modifyRequest.getName();
+
+        // Deep copy.
+        for (final Modification modification : modifyRequest.getModifications()) {
+            final ModificationType type = modification.getModificationType();
+            final Attribute attribute = new LinkedAttribute(modification.getAttribute());
+            final Modification copy = new Modification(type, attribute);
+            this.changes.add(copy);
+        }
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public ModifyRequest addModification(final Modification change) {
+        Reject.ifNull(change);
+        changes.add(change);
+        return this;
+    }
+
+    @Override
+    public ModifyRequest addModification(final ModificationType type,
+            final String attributeDescription, final Object... values) {
+        Reject.ifNull(type, attributeDescription);
+        Reject.ifNull(values);
+        changes.add(new Modification(type, new LinkedAttribute(attributeDescription, values)));
+        return this;
+    }
+
+    @Override
+    public List<Modification> getModifications() {
+        return changes;
+    }
+
+    @Override
+    public DN getName() {
+        return name;
+    }
+
+    @Override
+    public ModifyRequest setName(final DN dn) {
+        Reject.ifNull(dn);
+        this.name = dn;
+        return this;
+    }
+
+    @Override
+    public ModifyRequest setName(final String dn) {
+        Reject.ifNull(dn);
+        this.name = DN.valueOf(dn);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ModifyRequest(dn=");
+        builder.append(getName());
+        builder.append(", changes=");
+        builder.append(getModifications());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    ModifyRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequest.java
new file mode 100644
index 0000000..d0c6b96
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequest.java
@@ -0,0 +1,212 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+
+/**
+ * The password modify extended request as defined in RFC 3062. This operation
+ * allows directory clients to update user passwords. The user may or may not be
+ * associated with a directory entry. The user may or may not be represented as
+ * an LDAP DN. The user's password may or may not be stored in the directory. In
+ * addition, it includes support for requiring the user's current password as
+ * well as for generating a new password if none was provided.
+ *
+ * <pre>
+ * String userIdentity = ...; // For example, u:&lt;uid> or dn:&lt;DN>
+ * char[] oldPassword = ...;
+ * char[] newPassword = ...;
+ * Connection connection = ...;
+ *
+ * PasswordModifyExtendedRequest request =
+ *         Requests.newPasswordModifyExtendedRequest()
+ *             .setUserIdentity(userIdentity)
+ *             .setOldPassword(oldPassword)
+ *             .setNewPassword(newPassword);
+ *
+ * PasswordModifyExtendedResult result = connection.extendedRequest(request);
+ * if (result.isSuccess()) {
+ *     // Changed password
+ * } else {
+ *     // Use result to diagnose error.
+ * }
+ * </pre>
+ *
+ * @see PasswordModifyExtendedResult
+ * @see <a href="http://tools.ietf.org/html/rfc3062">RFC 3062 - LDAP Password
+ *      Modify Extended Operation </a>
+ */
+public interface PasswordModifyExtendedRequest extends
+        ExtendedRequest<PasswordModifyExtendedResult> {
+
+    /**
+     * A decoder which can be used to decode password modify extended operation
+     * requests.
+     */
+    ExtendedRequestDecoder<PasswordModifyExtendedRequest, PasswordModifyExtendedResult> DECODER =
+            new PasswordModifyExtendedRequestImpl.RequestDecoder();
+
+    /**
+     * The OID for the password modify extended operation request.
+     */
+    String OID = "1.3.6.1.4.1.4203.1.11.1";
+
+    @Override
+    PasswordModifyExtendedRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the desired password for the user, or {@code null} if a new
+     * password should be generated.
+     *
+     * @return The desired password for the user, or {@code null} if a new
+     *         password should be generated.
+     */
+    byte[] getNewPassword();
+
+    @Override
+    String getOID();
+
+    /**
+     * Returns the current password for the user, if known.
+     *
+     * @return The current password for the user, or {@code null} if the
+     *         password is not known.
+     */
+    byte[] getOldPassword();
+
+    @Override
+    ExtendedResultDecoder<PasswordModifyExtendedResult> getResultDecoder();
+
+    /**
+     * Returns the identity of the user whose password is to be modified, or
+     * {@code null} if the request should be applied to the user currently
+     * associated with the session. The returned identity may or may not be a
+     * distinguished name.
+     *
+     * @return The identity of the user whose password is to be modified, or
+     *         {@code null} if the request should be applied to the user
+     *         currently associated with the session.
+     */
+    ByteString getUserIdentity();
+
+    /**
+     * Returns the identity of the user whose password is to be modified decoded
+     * as a UTF-8 string, or {@code null} if the request should be applied to
+     * the user currently associated with the session. The returned identity may
+     * or may not be a distinguished name.
+     *
+     * @return The identity of the user whose password is to be modified decoded
+     *         as a UTF-8 string, or {@code null} if the request should be
+     *         applied to the user currently associated with the session.
+     */
+    String getUserIdentityAsString();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    /**
+     * Sets the desired password for the user.
+     *
+     * @param newPassword
+     *            The desired password for the user, or {@code null} if a new
+     *            password should be generated.
+     * @return This password modify request.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended request does not permit the
+     *             new password to be set.
+     */
+    PasswordModifyExtendedRequest setNewPassword(byte[] newPassword);
+
+    /**
+     * Sets the desired password for the user. The password will be converted to
+     * a UTF-8 octet string.
+     *
+     * @param newPassword
+     *            The desired password for the user, or {@code null} if a new
+     *            password should be generated.
+     * @return This password modify request.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended request does not permit the
+     *             new password to be set.
+     */
+    PasswordModifyExtendedRequest setNewPassword(char[] newPassword);
+
+    /**
+     * Sets the current password for the user.
+     *
+     * @param oldPassword
+     *            The current password for the user, or {@code null} if the
+     *            password is not known.
+     * @return This password modify request.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended request does not permit the
+     *             old password to be set.
+     */
+    PasswordModifyExtendedRequest setOldPassword(byte[] oldPassword);
+
+    /**
+     * Sets the current password for the user. The password will be converted to
+     * a UTF-8 octet string.
+     *
+     * @param oldPassword
+     *            The current password for the user, or {@code null} if the
+     *            password is not known.
+     * @return This password modify request.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended request does not permit the
+     *             old password to be set.
+     */
+    PasswordModifyExtendedRequest setOldPassword(char[] oldPassword);
+
+    /**
+     * Sets the identity of the user whose password is to be modified. The
+     * identity may or may not be a distinguished name.
+     * <p>
+     * If {@code userIdentity} is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param userIdentity
+     *            The identity of the user whose password is to be modified, or
+     *            {@code null} if the request should be applied to the user
+     *            currently associated with the session.
+     * @return This password modify request.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended request does not permit the
+     *             user identity to be set.
+     */
+    PasswordModifyExtendedRequest setUserIdentity(Object userIdentity);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestImpl.java
new file mode 100644
index 0000000..692ed57
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestImpl.java
@@ -0,0 +1,284 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Password modify extended request implementation.
+ */
+final class PasswordModifyExtendedRequestImpl extends
+        AbstractExtendedRequest<PasswordModifyExtendedRequest, PasswordModifyExtendedResult>
+        implements PasswordModifyExtendedRequest {
+    static final class RequestDecoder implements
+            ExtendedRequestDecoder<PasswordModifyExtendedRequest, PasswordModifyExtendedResult> {
+        @Override
+        public PasswordModifyExtendedRequest decodeExtendedRequest(
+                final ExtendedRequest<?> request, final DecodeOptions options)
+                throws DecodeException {
+            final PasswordModifyExtendedRequest newRequest =
+                    new PasswordModifyExtendedRequestImpl();
+            if (request.getValue() != null) {
+                try {
+                    final ASN1Reader reader = ASN1.getReader(request.getValue());
+                    reader.readStartSequence();
+                    if (reader.hasNextElement()
+                            && (reader.peekType() == TYPE_PASSWORD_MODIFY_USER_ID)) {
+                        newRequest.setUserIdentity(reader.readOctetStringAsString());
+                    }
+                    if (reader.hasNextElement()
+                            && (reader.peekType() == TYPE_PASSWORD_MODIFY_OLD_PASSWORD)) {
+                        newRequest.setOldPassword(reader.readOctetString().toByteArray());
+                    }
+                    if (reader.hasNextElement()
+                            && (reader.peekType() == TYPE_PASSWORD_MODIFY_NEW_PASSWORD)) {
+                        newRequest.setNewPassword(reader.readOctetString().toByteArray());
+                    }
+                    reader.readEndSequence();
+                } catch (final IOException e) {
+                    final LocalizableMessage message =
+                            ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get(getExceptionMessage(e));
+                    throw DecodeException.error(message, e);
+                }
+            }
+
+            for (final Control control : request.getControls()) {
+                newRequest.addControl(control);
+            }
+
+            return newRequest;
+        }
+    }
+
+    private static final class ResultDecoder extends
+            AbstractExtendedResultDecoder<PasswordModifyExtendedResult> {
+        @Override
+        public PasswordModifyExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            if (result instanceof PasswordModifyExtendedResult) {
+                return (PasswordModifyExtendedResult) result;
+            } else {
+                final ResultCode resultCode = result.getResultCode();
+
+                final PasswordModifyExtendedResult newResult =
+                        Responses.newPasswordModifyExtendedResult(resultCode).setMatchedDN(
+                                result.getMatchedDN()).setDiagnosticMessage(
+                                result.getDiagnosticMessage());
+
+                // TODO: Should we check to make sure OID is null?
+                final ByteString responseValue = result.getValue();
+                if (resultCode == ResultCode.SUCCESS && responseValue != null) {
+                    try {
+                        final ASN1Reader asn1Reader = ASN1.getReader(responseValue);
+                        asn1Reader.readStartSequence();
+                        if (asn1Reader.peekType() == TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD) {
+                            newResult.setGeneratedPassword(asn1Reader.readOctetString()
+                                    .toByteArray());
+                        }
+                        asn1Reader.readEndSequence();
+                    } catch (final IOException e) {
+                        final LocalizableMessage message =
+                                ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get(getExceptionMessage(e));
+                        throw DecodeException.error(message, e);
+                    }
+                }
+
+                for (final Control control : result.getControls()) {
+                    newResult.addControl(control);
+                }
+
+                return newResult;
+            }
+        }
+
+        @Override
+        public PasswordModifyExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newPasswordModifyExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+    }
+
+    private static final ExtendedResultDecoder<PasswordModifyExtendedResult> RESULT_DECODER =
+            new ResultDecoder();
+
+    /**
+     * The ASN.1 element type that will be used to encode the genPasswd
+     * component in a password modify extended response.
+     */
+    private static final byte TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD = (byte) 0x80;
+
+    /**
+     * The ASN.1 element type that will be used to encode the newPasswd
+     * component in a password modify extended request.
+     */
+    private static final byte TYPE_PASSWORD_MODIFY_NEW_PASSWORD = (byte) 0x82;
+
+    /**
+     * The ASN.1 element type that will be used to encode the oldPasswd
+     * component in a password modify extended request.
+     */
+    private static final byte TYPE_PASSWORD_MODIFY_OLD_PASSWORD = (byte) 0x81;
+
+    /**
+     * The ASN.1 element type that will be used to encode the userIdentity
+     * component in a password modify extended request.
+     */
+    private static final byte TYPE_PASSWORD_MODIFY_USER_ID = (byte) 0x80;
+    private byte[] newPassword;
+    private byte[] oldPassword;
+
+    private ByteString userIdentity;
+
+    /** Instantiation via factory. */
+    PasswordModifyExtendedRequestImpl() {
+
+    }
+
+    PasswordModifyExtendedRequestImpl(
+            final PasswordModifyExtendedRequest passwordModifyExtendedRequest) {
+        super(passwordModifyExtendedRequest);
+        this.userIdentity = passwordModifyExtendedRequest.getUserIdentity();
+        this.oldPassword = passwordModifyExtendedRequest.getOldPassword();
+        this.newPassword = passwordModifyExtendedRequest.getNewPassword();
+    }
+
+    @Override
+    public byte[] getNewPassword() {
+        return newPassword;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public byte[] getOldPassword() {
+        return oldPassword;
+    }
+
+    @Override
+    public ExtendedResultDecoder<PasswordModifyExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getUserIdentity() {
+        return userIdentity;
+    }
+
+    @Override
+    public String getUserIdentityAsString() {
+        return userIdentity != null ? userIdentity.toString() : null;
+    }
+
+    @Override
+    public ByteString getValue() {
+        final ByteStringBuilder buffer = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(buffer);
+
+        try {
+            writer.writeStartSequence();
+            if (userIdentity != null) {
+                writer.writeOctetString(TYPE_PASSWORD_MODIFY_USER_ID, userIdentity);
+            }
+            if (oldPassword != null) {
+                writer.writeOctetString(TYPE_PASSWORD_MODIFY_OLD_PASSWORD, oldPassword);
+            }
+            if (newPassword != null) {
+                writer.writeOctetString(TYPE_PASSWORD_MODIFY_NEW_PASSWORD, newPassword);
+            }
+            writer.writeEndSequence();
+        } catch (final IOException ioe) {
+            // This should never happen unless there is a bug somewhere.
+            throw new RuntimeException(ioe);
+        }
+
+        return buffer.toByteString();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return true;
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setNewPassword(final byte[] newPassword) {
+        this.newPassword = newPassword;
+        return this;
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setNewPassword(final char[] newPassword) {
+        this.newPassword = (newPassword != null) ? StaticUtils.getBytes(newPassword) : null;
+        return this;
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setOldPassword(final byte[] oldPassword) {
+        this.oldPassword = oldPassword;
+        return this;
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setOldPassword(final char[] oldPassword) {
+        this.oldPassword = (oldPassword != null) ? StaticUtils.getBytes(oldPassword) : null;
+        return this;
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setUserIdentity(final Object userIdentity) {
+        this.userIdentity = (userIdentity != null) ? ByteString.valueOfObject(userIdentity) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordModifyExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", userIdentity=");
+        builder.append(userIdentity);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequest.java
new file mode 100644
index 0000000..bf95492
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequest.java
@@ -0,0 +1,202 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Plain SASL bind request as defined in RFC 4616. This SASL mechanism
+ * allows a client to authenticate to the server with an authentication ID and
+ * password. This mechanism does not provide a security layer.
+ * <p>
+ * The authentication and optional authorization identity is specified using an
+ * authorization ID, or {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ *
+ * <pre>
+ * String authcid = ...;        // Authentication ID, e.g. dn:&lt;dn>, u:&lt;uid>
+ * String authzid = ...;        // Authorization ID, e.g. dn:&lt;dn>, u:&lt;uid>
+ * char[] password = ...;
+ * Connection connection = ...; // Use StartTLS to protect the request
+ *
+ * PlainSASLBindRequest request =
+ *         Requests.newPlainSASLBindRequest(authcid, password)
+ *         .setAuthorizationID(authzid);
+ *
+ * connection.bind(request);
+ * // Authenticated if the connection succeeds
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4616">RFC 4616 - The PLAIN Simple
+ *      Authentication and Security Layer (SASL) Mechanism </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface PlainSASLBindRequest extends SASLBindRequest {
+
+    /**
+     * The name of the SASL mechanism based on PLAIN authentication.
+     */
+    String SASL_MECHANISM_NAME = "PLAIN";
+
+    @Override
+    PlainSASLBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication ID of the user. The authentication ID usually
+     * has the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authentication ID of the user.
+     */
+    String getAuthenticationID();
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    /**
+     * Returns the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authorization ID of the user, which may be {@code null}.
+     */
+    String getAuthorizationID();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    /**
+     * Returns the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @return The password of the user that the client wishes to bind as.
+     */
+    byte[] getPassword();
+
+    @Override
+    String getSASLMechanism();
+
+    /**
+     * Sets the authentication ID of the user. The authentication ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the authentication ID to
+     *             be set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authenticationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws NullPointerException
+     *             If {@code authenticationID} was {@code null}.
+     */
+    PlainSASLBindRequest setAuthenticationID(String authenticationID);
+
+    /**
+     * Sets the optional authorization ID of the user which represents an
+     * alternate authorization identity which should be used for subsequent
+     * operations performed on the connection. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authorizationID
+     *            The authorization ID of the user, which may be {@code null}.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the authorization ID to
+     *             be set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     */
+    PlainSASLBindRequest setAuthorizationID(String authorizationID);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * provided password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as,
+     *            which may be empty.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    PlainSASLBindRequest setPassword(byte[] password);
+
+    /**
+     * Sets the password of the user that the client wishes to bind as. The
+     * password will be converted to a UTF-8 octet string.
+     *
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the password to be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    PlainSASLBindRequest setPassword(char[] password);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestImpl.java
new file mode 100644
index 0000000..2c8bcb2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestImpl.java
@@ -0,0 +1,182 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Plain SASL bind request implementation.
+ */
+final class PlainSASLBindRequestImpl extends AbstractSASLBindRequest<PlainSASLBindRequest>
+        implements PlainSASLBindRequest {
+    private static final class Client extends SASLBindClientImpl {
+        private final String authenticationID;
+        private final ByteString password;
+        private final SaslClient saslClient;
+
+        private Client(final PlainSASLBindRequestImpl initialBindRequest, final String serverName)
+                throws LdapException {
+            super(initialBindRequest);
+
+            this.authenticationID = initialBindRequest.getAuthenticationID();
+            this.password = ByteString.wrap(initialBindRequest.getPassword());
+
+            try {
+                saslClient =
+                        Sasl.createSaslClient(new String[] { SASL_MECHANISM_NAME },
+                                initialBindRequest.getAuthorizationID(), SASL_DEFAULT_PROTOCOL,
+                                serverName, null, this);
+
+                if (saslClient.hasInitialResponse()) {
+                    setNextSASLCredentials(saslClient.evaluateChallenge(new byte[0]));
+                } else {
+                    setNextSASLCredentials((ByteString) null);
+                }
+            } catch (final SaslException e) {
+                throw newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e);
+            }
+        }
+
+        @Override
+        public void dispose() {
+            try {
+                saslClient.dispose();
+            } catch (final SaslException ignored) {
+                // Ignore the SASL exception.
+            }
+        }
+
+        @Override
+        public boolean evaluateResult(final BindResult result) {
+            return saslClient.isComplete();
+        }
+
+        @Override
+        void handle(final NameCallback callback) throws UnsupportedCallbackException {
+            callback.setName(authenticationID);
+        }
+
+        @Override
+        void handle(final PasswordCallback callback) throws UnsupportedCallbackException {
+            callback.setPassword(password.toString().toCharArray());
+        }
+    }
+
+    private String authenticationID;
+    private String authorizationID;
+
+    private byte[] password;
+
+    PlainSASLBindRequestImpl(final PlainSASLBindRequest plainSASLBindRequest) {
+        super(plainSASLBindRequest);
+        this.authenticationID = plainSASLBindRequest.getAuthenticationID();
+        this.authorizationID = plainSASLBindRequest.getAuthorizationID();
+        this.password = StaticUtils.copyOfBytes(plainSASLBindRequest.getPassword());
+    }
+
+    PlainSASLBindRequestImpl(final String authenticationID, final byte[] password) {
+        Reject.ifNull(authenticationID, password);
+        this.authenticationID = authenticationID;
+        this.password = password;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new Client(this, serverName);
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return authenticationID;
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public byte[] getPassword() {
+        return password;
+    }
+
+    @Override
+    public String getSASLMechanism() {
+        return SASL_MECHANISM_NAME;
+    }
+
+    @Override
+    public PlainSASLBindRequest setAuthenticationID(final String authenticationID) {
+        Reject.ifNull(authenticationID);
+        this.authenticationID = authenticationID;
+        return this;
+    }
+
+    @Override
+    public PlainSASLBindRequest setAuthorizationID(final String authorizationID) {
+        this.authorizationID = authorizationID;
+        return this;
+    }
+
+    @Override
+    public PlainSASLBindRequest setPassword(final byte[] password) {
+        Reject.ifNull(password);
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public PlainSASLBindRequest setPassword(final char[] password) {
+        Reject.ifNull(password);
+        this.password = StaticUtils.getBytes(password);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PlainSASLBindRequest(bindDN=");
+        builder.append(getName());
+        builder.append(", authentication=SASL");
+        builder.append(", saslMechanism=");
+        builder.append(getSASLMechanism());
+        builder.append(", authenticationID=");
+        builder.append(authenticationID);
+        builder.append(", authorizationID=");
+        builder.append(authorizationID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Request.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
new file mode 100644
index 0000000..2004689
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The base class of all Requests provides methods for querying and manipulating
+ * the set of Controls included with a Request.
+ */
+public interface Request {
+
+    /**
+     * Adds the provided control to this request.
+     *
+     * @param control
+     *            The control to be added to this request.
+     * @return This request.
+     * @throws UnsupportedOperationException
+     *             If this request does not permit controls to be added.
+     * @throws NullPointerException
+     *             If {@code control} was {@code null}.
+     */
+    Request addControl(Control control);
+
+    /**
+     * Returns {@code true} if this request contains the specified request
+     * control.
+     *
+     * @param oid
+     *            The numeric OID of the request control.
+     * @return {@code true} if this request contains the specified request
+     *         control.
+     */
+    boolean containsControl(String oid);
+
+    /**
+     * Decodes and returns the first control in this request having an OID
+     * corresponding to the provided control decoder.
+     *
+     * @param <C>
+     *            The type of control to be decoded and returned.
+     * @param decoder
+     *            The control decoder.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the control.
+     * @return The decoded control, or {@code null} if the control is not
+     *         included with this request.
+     * @throws DecodeException
+     *             If the control could not be decoded because it was malformed
+     *             in some way (e.g. the control value was missing, or its
+     *             content could not be decoded).
+     * @throws NullPointerException
+     *             If {@code decoder} or {@code options} was {@code null}.
+     */
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    /**
+     * Returns a {@code List} containing the controls included with this
+     * request. The returned {@code List} may be modified if permitted by this
+     * request.
+     *
+     * @return A {@code List} containing the controls.
+     */
+    List<Control> getControls();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
new file mode 100644
index 0000000..0963615
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
@@ -0,0 +1,1608 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_READ_LDIF_RECORD_CHANGE_RECORD_WRONG_TYPE;
+
+import javax.net.ssl.SSLContext;
+import javax.security.auth.Subject;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.LDIFChangeRecordReader;
+import org.forgerock.util.Reject;
+
+/**
+ * This class contains various methods for creating and manipulating requests.
+ * <p>
+ * All copy constructors of the form {@code copyOfXXXRequest} perform deep
+ * copies of their request parameter. More specifically, any controls,
+ * modifications, and attributes contained within the response will be
+ * duplicated.
+ * <p>
+ * Similarly, all unmodifiable views of request returned by methods of the form
+ * {@code unmodifiableXXXRequest} return deep unmodifiable views of their
+ * request parameter. More specifically, any controls, modifications, and
+ * attributes contained within the returned request will be unmodifiable.
+ */
+public final class Requests {
+
+    // TODO: search request from LDAP URL.
+
+    // TODO: update request from persistent search result.
+
+    // TODO: synchronized requests?
+
+    /**
+     * Creates a new abandon request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The abandon request to be copied.
+     * @return The new abandon request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}
+     */
+    public static AbandonRequest copyOfAbandonRequest(final AbandonRequest request) {
+        return new AbandonRequestImpl(request);
+    }
+
+    /**
+     * Creates a new add request that is an exact copy of the provided request.
+     *
+     * @param request
+     *            The add request to be copied.
+     * @return The new add request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static AddRequest copyOfAddRequest(final AddRequest request) {
+        return new AddRequestImpl(request);
+    }
+
+    /**
+     * Creates a new anonymous SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The anonymous SASL bind request to be copied.
+     * @return The new anonymous SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static AnonymousSASLBindRequest copyOfAnonymousSASLBindRequest(
+            final AnonymousSASLBindRequest request) {
+        return new AnonymousSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new cancel extended request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The cancel extended request to be copied.
+     * @return The new cancel extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static CancelExtendedRequest copyOfCancelExtendedRequest(
+            final CancelExtendedRequest request) {
+        return new CancelExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates a new compare request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The compare request to be copied.
+     * @return The new compare request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static CompareRequest copyOfCompareRequest(final CompareRequest request) {
+        return new CompareRequestImpl(request);
+    }
+
+    /**
+     * Creates a new CRAM MD5 SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The CRAM MD5 SASL bind request to be copied.
+     * @return The new CRAM-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static CRAMMD5SASLBindRequest copyOfCRAMMD5SASLBindRequest(
+            final CRAMMD5SASLBindRequest request) {
+        return new CRAMMD5SASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new delete request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The add request to be copied.
+     * @return The new delete request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static DeleteRequest copyOfDeleteRequest(final DeleteRequest request) {
+        return new DeleteRequestImpl(request);
+    }
+
+    /**
+     * Creates a new digest MD5 SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The digest MD5 SASL bind request to be copied.
+     * @return The new DIGEST-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static DigestMD5SASLBindRequest copyOfDigestMD5SASLBindRequest(
+            final DigestMD5SASLBindRequest request) {
+        return new DigestMD5SASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new external SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The external SASL bind request to be copied.
+     * @return The new External SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ExternalSASLBindRequest copyOfExternalSASLBindRequest(
+            final ExternalSASLBindRequest request) {
+        return new ExternalSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new generic bind request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The generic bind request to be copied.
+     * @return The new generic bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static GenericBindRequest copyOfGenericBindRequest(final GenericBindRequest request) {
+        return new GenericBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new generic extended request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The generic extended request to be copied.
+     * @return The new generic extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static GenericExtendedRequest copyOfGenericExtendedRequest(
+            final GenericExtendedRequest request) {
+        return new GenericExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates a new GSSAPI SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The GSSAPI SASL bind request to be copied.
+     * @return The new GSSAPI SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static GSSAPISASLBindRequest copyOfGSSAPISASLBindRequest(
+            final GSSAPISASLBindRequest request) {
+        return new GSSAPISASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new modify DN request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The modify DN request to be copied.
+     * @return The new modify DN request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ModifyDNRequest copyOfModifyDNRequest(final ModifyDNRequest request) {
+        return new ModifyDNRequestImpl(request);
+    }
+
+    /**
+     * Creates a new modify request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The modify request to be copied.
+     * @return The new modify request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ModifyRequest copyOfModifyRequest(final ModifyRequest request) {
+        return new ModifyRequestImpl(request);
+    }
+
+    /**
+     * Creates a new password modify extended request that is an exact copy of
+     * the provided request.
+     *
+     * @param request
+     *            The password modify extended request to be copied.
+     * @return The new password modify extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static PasswordModifyExtendedRequest copyOfPasswordModifyExtendedRequest(
+            final PasswordModifyExtendedRequest request) {
+        return new PasswordModifyExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates a new plain SASL bind request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The plain SASL bind request to be copied.
+     * @return The new Plain SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static PlainSASLBindRequest copyOfPlainSASLBindRequest(final PlainSASLBindRequest request) {
+        return new PlainSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new search request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The search request to be copied.
+     * @return The new search request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static SearchRequest copyOfSearchRequest(final SearchRequest request) {
+        return new SearchRequestImpl(request);
+    }
+
+    /**
+     * Creates a new simple bind request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The simple bind request to be copied.
+     * @return The new simple bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static SimpleBindRequest copyOfSimpleBindRequest(final SimpleBindRequest request) {
+        return new SimpleBindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new startTLS extended request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The startTLS extended request to be copied.
+     * @return The new start TLS extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static StartTLSExtendedRequest copyOfStartTLSExtendedRequest(
+            final StartTLSExtendedRequest request) {
+        return new StartTLSExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates a new unbind request that is an exact copy of the provided
+     * request.
+     *
+     * @param request
+     *            The unbind request to be copied.
+     * @return The new unbind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static UnbindRequest copyOfUnbindRequest(final UnbindRequest request) {
+        return new UnbindRequestImpl(request);
+    }
+
+    /**
+     * Creates a new Who Am I extended request that is an exact copy of the
+     * provided request.
+     *
+     * @param request
+     *            The who Am I extended request to be copied.
+     * @return The new Who Am I extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static WhoAmIExtendedRequest copyOfWhoAmIExtendedRequest(
+            final WhoAmIExtendedRequest request) {
+        return new WhoAmIExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates a new abandon request using the provided message ID.
+     *
+     * @param requestID
+     *            The request ID of the request to be abandoned.
+     * @return The new abandon request.
+     */
+    public static AbandonRequest newAbandonRequest(final int requestID) {
+        return new AbandonRequestImpl(requestID);
+    }
+
+    /**
+     * Creates a new add request using the provided distinguished name.
+     *
+     * @param name
+     *            The distinguished name of the entry to be added.
+     * @return The new add request.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static AddRequest newAddRequest(final DN name) {
+        final Entry entry = new LinkedHashMapEntry().setName(name);
+        return new AddRequestImpl(entry);
+    }
+
+    /**
+     * Creates a new add request backed by the provided entry. Modifications
+     * made to {@code entry} will be reflected in the returned add request. The
+     * returned add request supports updates to its list of controls, as well as
+     * updates to the name and attributes if the underlying entry allows.
+     *
+     * @param entry
+     *            The entry to be added.
+     * @return The new add request.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null} .
+     */
+    public static AddRequest newAddRequest(final Entry entry) {
+        Reject.ifNull(entry);
+        return new AddRequestImpl(entry);
+    }
+
+    /**
+     * Creates a new add request using the provided distinguished name decoded
+     * using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the entry to be added.
+     * @return The new add request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static AddRequest newAddRequest(final String name) {
+        final Entry entry = new LinkedHashMapEntry().setName(name);
+        return new AddRequestImpl(entry);
+    }
+
+    /**
+     * Creates a new add request using the provided lines of LDIF decoded using
+     * the default schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing an LDIF add change record or an LDIF
+     *            entry record.
+     * @return The new add request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public static AddRequest newAddRequest(final String... ldifLines) {
+        // LDIF change record reader is tolerant to missing change types.
+        final ChangeRecord record = LDIFChangeRecordReader.valueOfLDIFChangeRecord(ldifLines);
+
+        if (record instanceof AddRequest) {
+            return (AddRequest) record;
+        } else {
+            // Wrong change type.
+            final LocalizableMessage message =
+                    WARN_READ_LDIF_RECORD_CHANGE_RECORD_WRONG_TYPE.get("add");
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Creates a new anonymous SASL bind request having the provided trace
+     * string.
+     *
+     * @param traceString
+     *            The trace information, which has no semantic value, and can be
+     *            used by administrators in order to identify the user.
+     * @return The new anonymous SASL bind request.
+     * @throws NullPointerException
+     *             If {@code traceString} was {@code null}.
+     */
+    public static AnonymousSASLBindRequest newAnonymousSASLBindRequest(final String traceString) {
+        return new AnonymousSASLBindRequestImpl(traceString);
+    }
+
+    /**
+     * Creates a new cancel extended request using the provided message ID.
+     *
+     * @param requestID
+     *            The request ID of the request to be abandoned.
+     * @return The new cancel extended request.
+     */
+    public static CancelExtendedRequest newCancelExtendedRequest(final int requestID) {
+        return new CancelExtendedRequestImpl(requestID);
+    }
+
+    /**
+     * Creates a new change record (an add, delete, modify, or modify DN
+     * request) using the provided lines of LDIF decoded using the default
+     * schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing an LDIF change record or an LDIF
+     *            entry record.
+     * @return The new change record.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public static ChangeRecord newChangeRecord(final String... ldifLines) {
+        // LDIF change record reader is tolerant to missing change types.
+        return LDIFChangeRecordReader.valueOfLDIFChangeRecord(ldifLines);
+    }
+
+    /**
+     * Creates a new compare request using the provided distinguished name,
+     * attribute name, and assertion value.
+     * <p>
+     * If the assertion value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param name
+     *            The distinguished name of the entry to be compared.
+     * @param attributeDescription
+     *            The name of the attribute to be compared.
+     * @param assertionValue
+     *            The assertion value to be compared.
+     * @return The new compare request.
+     * @throws NullPointerException
+     *             If {@code name}, {@code attributeDescription}, or
+     *             {@code assertionValue} was {@code null}.
+     */
+    public static CompareRequest newCompareRequest(final DN name,
+            final AttributeDescription attributeDescription, final Object assertionValue) {
+        Reject.ifNull(name, attributeDescription, assertionValue);
+        return new CompareRequestImpl(name, attributeDescription, ByteString
+                .valueOfObject(assertionValue));
+    }
+
+    /**
+     * Creates a new compare request using the provided distinguished name,
+     * attribute name, and assertion value decoded using the default schema.
+     * <p>
+     * If the assertion value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param name
+     *            The distinguished name of the entry to be compared.
+     * @param attributeDescription
+     *            The name of the attribute to be compared.
+     * @param assertionValue
+     *            The assertion value to be compared.
+     * @return The new compare request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} or {@code attributeDescription} could not be
+     *             decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code name}, {@code attributeDescription}, or
+     *             {@code assertionValue} was {@code null}.
+     */
+    public static CompareRequest newCompareRequest(final String name,
+            final String attributeDescription, final Object assertionValue) {
+        Reject.ifNull(name, attributeDescription, assertionValue);
+        return new CompareRequestImpl(DN.valueOf(name), AttributeDescription
+                .valueOf(attributeDescription), ByteString.valueOfObject(assertionValue));
+    }
+
+    /**
+     * Creates a new CRAM-MD5 SASL bind request having the provided
+     * authentication ID and password.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return The new CRAM-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static CRAMMD5SASLBindRequest newCRAMMD5SASLBindRequest(final String authenticationID,
+            final byte[] password) {
+        return new CRAMMD5SASLBindRequestImpl(authenticationID, password);
+    }
+
+    /**
+     * Creates a new CRAM-MD5 SASL bind request having the provided
+     * authentication ID and password.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     *            The password will be converted to a UTF-8 octet string.
+     * @return The new CRAM-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static CRAMMD5SASLBindRequest newCRAMMD5SASLBindRequest(final String authenticationID,
+            final char[] password) {
+        return new CRAMMD5SASLBindRequestImpl(authenticationID, getBytes(password));
+    }
+
+    /**
+     * Creates a new delete request using the provided distinguished name.
+     *
+     * @param name
+     *            The distinguished name of the entry to be deleted.
+     * @return The new delete request.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static DeleteRequest newDeleteRequest(final DN name) {
+        Reject.ifNull(name);
+        return new DeleteRequestImpl(name);
+    }
+
+    /**
+     * Creates a new delete request using the provided distinguished name
+     * decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the entry to be deleted.
+     * @return The new delete request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static DeleteRequest newDeleteRequest(final String name) {
+        Reject.ifNull(name);
+        return new DeleteRequestImpl(DN.valueOf(name));
+    }
+
+    /**
+     * Creates a new DIGEST-MD5 SASL bind request having the provided
+     * authentication ID and password, but no realm or authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return The new DIGEST-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static DigestMD5SASLBindRequest newDigestMD5SASLBindRequest(
+            final String authenticationID, final byte[] password) {
+        return new DigestMD5SASLBindRequestImpl(authenticationID, password);
+    }
+
+    /**
+     * Creates a new DIGEST-MD5 SASL bind request having the provided
+     * authentication ID and password, but no realm or authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     *            The password will be converted to a UTF-8 octet string.
+     * @return The new DIGEST-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static DigestMD5SASLBindRequest newDigestMD5SASLBindRequest(
+            final String authenticationID, final char[] password) {
+        return new DigestMD5SASLBindRequestImpl(authenticationID, getBytes(password));
+    }
+
+    /**
+     * Creates a new External SASL bind request with no authorization ID.
+     *
+     * @return The new External SASL bind request.
+     */
+    public static ExternalSASLBindRequest newExternalSASLBindRequest() {
+        return new ExternalSASLBindRequestImpl();
+    }
+
+    /**
+     * Creates a new generic bind request using an empty distinguished name,
+     * authentication type, and authentication information.
+     *
+     * @param authenticationType
+     *            The authentication mechanism identifier for this generic bind
+     *            request.
+     * @param authenticationValue
+     *            The authentication information for this generic bind request
+     *            in a form defined by the authentication mechanism.
+     * @return The new generic bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationValue} was {@code null}.
+     */
+    public static GenericBindRequest newGenericBindRequest(final byte authenticationType,
+            final byte[] authenticationValue) {
+        Reject.ifNull(authenticationValue);
+        return new GenericBindRequestImpl("", authenticationType, authenticationValue);
+    }
+
+    /**
+     * Creates a new generic bind request using the provided name,
+     * authentication type, and authentication information.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @param name
+     *            The name of the Directory object that the client wishes to
+     *            bind as (may be empty).
+     * @param authenticationType
+     *            The authentication mechanism identifier for this generic bind
+     *            request.
+     * @param authenticationValue
+     *            The authentication information for this generic bind request
+     *            in a form defined by the authentication mechanism.
+     * @return The new generic bind request.
+     * @throws NullPointerException
+     *             If {@code name} or {@code authenticationValue} was
+     *             {@code null}.
+     */
+    public static GenericBindRequest newGenericBindRequest(final String name,
+            final byte authenticationType, final byte[] authenticationValue) {
+        Reject.ifNull(name, authenticationValue);
+        return new GenericBindRequestImpl(name, authenticationType, authenticationValue);
+    }
+
+    /**
+     * Creates a new generic extended request using the provided name and no
+     * value.
+     *
+     * @param requestName
+     *            The dotted-decimal representation of the unique OID
+     *            corresponding to this extended request.
+     * @return The new generic extended request.
+     * @throws NullPointerException
+     *             If {@code requestName} was {@code null}.
+     */
+    public static GenericExtendedRequest newGenericExtendedRequest(final String requestName) {
+        Reject.ifNull(requestName);
+        return new GenericExtendedRequestImpl(requestName);
+    }
+
+    /**
+     * Creates a new generic extended request using the provided name and
+     * optional value.
+     * <p>
+     * If the request value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param requestName
+     *            The dotted-decimal representation of the unique OID
+     *            corresponding to this extended request.
+     * @param requestValue
+     *            The content of this generic extended request in a form defined
+     *            by the extended operation, or {@code null} if there is no
+     *            content.
+     * @return The new generic extended request.
+     * @throws NullPointerException
+     *             If {@code requestName} was {@code null}.
+     */
+    public static GenericExtendedRequest newGenericExtendedRequest(final String requestName,
+            final Object requestValue) {
+        Reject.ifNull(requestName);
+        return new GenericExtendedRequestImpl(requestName).setValue(requestValue);
+    }
+
+    /**
+     * Creates a new GSSAPI SASL bind request having the provided authentication
+     * ID and password, but no realm, KDC address, or authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return The new GSSAPI SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static GSSAPISASLBindRequest newGSSAPISASLBindRequest(final String authenticationID,
+            final byte[] password) {
+        return new GSSAPISASLBindRequestImpl(authenticationID, password);
+    }
+
+    /**
+     * Creates a new GSSAPI SASL bind request having the provided authentication
+     * ID and password, but no realm, KDC address, or authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     *            The password will be converted to a UTF-8 octet string.
+     * @return The new GSSAPI SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static GSSAPISASLBindRequest newGSSAPISASLBindRequest(final String authenticationID,
+            final char[] password) {
+        return new GSSAPISASLBindRequestImpl(authenticationID, getBytes(password));
+    }
+
+    /**
+     * Creates a new GSSAPI SASL bind request having the provided subject, but
+     * no authorization ID.
+     *
+     * @param subject
+     *            The Kerberos subject of the user to be authenticated.
+     * @return The new GSSAPI SASL bind request.
+     * @throws NullPointerException
+     *             If {@code subject} was {@code null}.
+     */
+    public static GSSAPISASLBindRequest newGSSAPISASLBindRequest(final Subject subject) {
+        return new GSSAPISASLBindRequestImpl(subject);
+    }
+
+    /**
+     * Creates a new modify DN request using the provided distinguished name and
+     * new RDN. The new superior will be {@code null}, indicating that the
+     * renamed entry will remain under the same parent entry, and the old RDN
+     * attribute values will not be deleted.
+     *
+     * @param name
+     *            The distinguished name of the entry to be renamed.
+     * @param newRDN
+     *            The new RDN of the entry.
+     * @return The new modify DN request.
+     * @throws NullPointerException
+     *             If {@code name} or {@code newRDN} was {@code null}.
+     */
+    public static ModifyDNRequest newModifyDNRequest(final DN name, final RDN newRDN) {
+        Reject.ifNull(name);
+        Reject.ifNull(newRDN);
+        return new ModifyDNRequestImpl(name, newRDN);
+    }
+
+    /**
+     * Creates a new modify DN request using the provided distinguished name and
+     * new RDN decoded using the default schema. The new superior will be
+     * {@code null}, indicating that the renamed entry will remain under the
+     * same parent entry, and the old RDN attribute values will not be deleted.
+     *
+     * @param name
+     *            The distinguished name of the entry to be renamed.
+     * @param newRDN
+     *            The new RDN of the entry.
+     * @return The new modify DN request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} or {@code newRDN} could not be decoded using
+     *             the default schema.
+     * @throws NullPointerException
+     *             If {@code name} or {@code newRDN} was {@code null}.
+     */
+    public static ModifyDNRequest newModifyDNRequest(final String name, final String newRDN) {
+        Reject.ifNull(name, newRDN);
+        return new ModifyDNRequestImpl(DN.valueOf(name), RDN.valueOf(newRDN));
+    }
+
+    /**
+     * Creates a new modify request using the provided distinguished name.
+     *
+     * @param name
+     *            The distinguished name of the entry to be modified.
+     * @return The new modify request.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static ModifyRequest newModifyRequest(final DN name) {
+        Reject.ifNull(name);
+        return new ModifyRequestImpl(name);
+    }
+
+    /**
+     * Creates a new modify request containing a list of modifications which can
+     * be used to transform {@code fromEntry} into entry {@code toEntry}.
+     * <p>
+     * The changes will be generated using a default set of
+     * {@link org.forgerock.opendj.ldap.Entries.DiffOptions options}. More
+     * specifically, only user attributes will be compared, attributes will be
+     * compared using their matching rules, and all generated changes will be
+     * reversible: it will contain only modifications of type
+     * {@link ModificationType#DELETE DELETE} then {@link ModificationType#ADD
+     * ADD}.
+     * <p>
+     * Finally, the modify request will use the distinguished name taken from
+     * {@code fromEntry}. Moreover, this method will not check to see if both
+     * {@code fromEntry} and {@code toEntry} have the same distinguished name.
+     * <p>
+     * This method is equivalent to:
+     *
+     * <pre>
+     * ModifyRequest request = Entries.diffEntries(fromEntry, toEntry);
+     * </pre>
+     *
+     * Or:
+     *
+     * <pre>
+     * ModifyRequest request = Entries.diffEntries(fromEntry, toEntry, Entries.diffOptions());
+     * </pre>
+     *
+     * @param fromEntry
+     *            The source entry.
+     * @param toEntry
+     *            The destination entry.
+     * @return A modify request containing a list of modifications which can be
+     *         used to transform {@code fromEntry} into entry {@code toEntry}.
+     *         The returned request will always be non-{@code null} but may not
+     *         contain any modifications.
+     * @throws NullPointerException
+     *             If {@code fromEntry} or {@code toEntry} were {@code null}.
+     * @see Entries#diffEntries(Entry, Entry)
+     */
+    public static ModifyRequest newModifyRequest(final Entry fromEntry, final Entry toEntry) {
+        return Entries.diffEntries(fromEntry, toEntry);
+    }
+
+    /**
+     * Creates a new modify request using the provided distinguished name
+     * decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the entry to be modified.
+     * @return The new modify request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static ModifyRequest newModifyRequest(final String name) {
+        Reject.ifNull(name);
+        return new ModifyRequestImpl(DN.valueOf(name));
+    }
+
+    /**
+     * Creates a new modify request using the provided lines of LDIF decoded
+     * using the default schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing a single LDIF modify change record.
+     * @return The new modify request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public static ModifyRequest newModifyRequest(final String... ldifLines) {
+        // LDIF change record reader is tolerant to missing change types.
+        final ChangeRecord record = LDIFChangeRecordReader.valueOfLDIFChangeRecord(ldifLines);
+
+        if (record instanceof ModifyRequest) {
+            return (ModifyRequest) record;
+        } else {
+            // Wrong change type.
+            final LocalizableMessage message =
+                    WARN_READ_LDIF_RECORD_CHANGE_RECORD_WRONG_TYPE.get("modify");
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Creates a new password modify extended request, with no user identity,
+     * old password, or new password.
+     *
+     * @return The new password modify extended request.
+     */
+    public static PasswordModifyExtendedRequest newPasswordModifyExtendedRequest() {
+        return new PasswordModifyExtendedRequestImpl();
+    }
+
+    /**
+     * Creates a new Plain SASL bind request having the provided authentication
+     * ID and password, but no authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     * @return The new Plain SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static PlainSASLBindRequest newPlainSASLBindRequest(final String authenticationID,
+            final byte[] password) {
+        return new PlainSASLBindRequestImpl(authenticationID, password);
+    }
+
+    /**
+     * Creates a new Plain SASL bind request having the provided authentication
+     * ID and password, but no authorization ID.
+     *
+     * @param authenticationID
+     *            The authentication ID of the user. The authentication ID
+     *            usually has the form "dn:" immediately followed by the
+     *            distinguished name of the user, or "u:" followed by a user ID
+     *            string, but other forms are permitted.
+     * @param password
+     *            The password of the user that the client wishes to bind as.
+     *            The password will be converted to a UTF-8 octet string.
+     * @return The new Plain SASL bind request.
+     * @throws NullPointerException
+     *             If {@code authenticationID} or {@code password} was
+     *             {@code null}.
+     */
+    public static PlainSASLBindRequest newPlainSASLBindRequest(final String authenticationID,
+            final char[] password) {
+        return new PlainSASLBindRequestImpl(authenticationID, getBytes(password));
+    }
+
+    /**
+     * Creates a new search request using the provided distinguished name,
+     * scope, and filter.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSearchRequest(final DN name, final SearchScope scope,
+            final Filter filter, final String... attributeDescriptions) {
+        Reject.ifNull(name, scope, filter);
+        final SearchRequest request = new SearchRequestImpl(name, scope, filter);
+        for (final String attributeDescription : attributeDescriptions) {
+            request.addAttribute(attributeDescription);
+        }
+        return request;
+    }
+
+    /**
+     * Creates a new search request using the provided distinguished name,
+     * scope, and filter, decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema, or if {@code filter} is not a valid LDAP string
+     *             representation of a filter.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSearchRequest(final String name, final SearchScope scope,
+            final String filter, final String... attributeDescriptions) {
+        Reject.ifNull(name, scope, filter);
+        final SearchRequest request =
+                new SearchRequestImpl(DN.valueOf(name), scope, Filter.valueOf(filter));
+        for (final String attributeDescription : attributeDescriptions) {
+            request.addAttribute(attributeDescription);
+        }
+        return request;
+    }
+
+    /**
+     * Creates a new search request for a single entry, using the provided distinguished name,
+     * scope, and filter.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSingleEntrySearchRequest(final DN name, final SearchScope scope,
+            final Filter filter, final String... attributeDescriptions) {
+        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
+    }
+
+    /**
+     * Creates a new search request for a single entry, using the provided distinguished name,
+     * scope, and filter, decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema, or if {@code filter} is not a valid LDAP string
+     *             representation of a filter.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSingleEntrySearchRequest(final String name, final SearchScope scope,
+            final String filter, final String... attributeDescriptions) {
+        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
+    }
+
+    /**
+     * Creates a new simple bind request having an empty name and password
+     * suitable for anonymous authentication.
+     *
+     * @return The new simple bind request.
+     */
+    public static SimpleBindRequest newSimpleBindRequest() {
+        return new SimpleBindRequestImpl("", EMPTY_BYTES);
+    }
+
+    /**
+     * Creates a new simple bind request having the provided name and password
+     * suitable for name/password authentication. The name will be decoded using
+     * the default schema.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @param name
+     *            The name of the Directory object that the client wishes to
+     *            bind as, which may be empty.
+     * @param password
+     *            The password of the Directory object that the client wishes to
+     *            bind as, which may be empty indicating that an unauthenticated
+     *            bind is to be performed.
+     * @return The new simple bind request.
+     * @throws NullPointerException
+     *             If {@code name} or {@code password} was {@code null}.
+     */
+    public static SimpleBindRequest newSimpleBindRequest(final String name, final byte[] password) {
+        Reject.ifNull(name, password);
+        return new SimpleBindRequestImpl(name, password);
+    }
+
+    /**
+     * Creates a new simple bind request having the provided name and password
+     * suitable for name/password authentication. The name will be decoded using
+     * the default schema.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @param name
+     *            The name of the Directory object that the client wishes to
+     *            bind as, which may be empty.
+     * @param password
+     *            The password of the Directory object that the client wishes to
+     *            bind as, which may be empty indicating that an unauthenticated
+     *            bind is to be performed. The password will be converted to a
+     *            UTF-8 octet string.
+     * @return The new simple bind request.
+     * @throws NullPointerException
+     *             If {@code name} or {@code password} was {@code null}.
+     */
+    public static SimpleBindRequest newSimpleBindRequest(final String name, final char[] password) {
+        Reject.ifNull(name, password);
+        return new SimpleBindRequestImpl(name, getBytes(password));
+    }
+
+    /**
+     * Creates a new start TLS extended request which will use the provided SSL
+     * context.
+     *
+     * @param sslContext
+     *            The SSLContext that should be used when installing the TLS
+     *            layer.
+     * @return The new start TLS extended request.
+     * @throws NullPointerException
+     *             If {@code sslContext} was {@code null}.
+     */
+    public static StartTLSExtendedRequest newStartTLSExtendedRequest(final SSLContext sslContext) {
+        return new StartTLSExtendedRequestImpl(sslContext);
+    }
+
+    /**
+     * Creates a new unbind request.
+     *
+     * @return The new unbind request.
+     */
+    public static UnbindRequest newUnbindRequest() {
+        return new UnbindRequestImpl();
+    }
+
+    /**
+     * Creates a new Who Am I extended request.
+     *
+     * @return The new Who Am I extended request.
+     */
+    public static WhoAmIExtendedRequest newWhoAmIExtendedRequest() {
+        return new WhoAmIExtendedRequestImpl();
+    }
+
+    /**
+     * Creates an unmodifiable abandon request of the provided request.
+     *
+     * @param request
+     *            The abandon request to be copied.
+     * @return The new abandon request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}
+     */
+    public static AbandonRequest unmodifiableAbandonRequest(final AbandonRequest request) {
+        if (request instanceof UnmodifiableAbandonRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableAbandonRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable add request of the provided request.
+     *
+     * @param request
+     *            The add request to be copied.
+     * @return The new add request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static AddRequest unmodifiableAddRequest(final AddRequest request) {
+        if (request instanceof UnmodifiableAddRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableAddRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable anonymous SASL bind request of the provided
+     * request.
+     *
+     * @param request
+     *            The anonymous SASL bind request to be copied.
+     * @return The new anonymous SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static AnonymousSASLBindRequest unmodifiableAnonymousSASLBindRequest(
+            final AnonymousSASLBindRequest request) {
+        if (request instanceof UnmodifiableAnonymousSASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableAnonymousSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable cancel extended request of the provided request.
+     *
+     * @param request
+     *            The cancel extended request to be copied.
+     * @return The new cancel extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static CancelExtendedRequest unmodifiableCancelExtendedRequest(
+            final CancelExtendedRequest request) {
+        if (request instanceof UnmodifiableCancelExtendedRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableCancelExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable compare request of the provided request.
+     *
+     * @param request
+     *            The compare request to be copied.
+     * @return The new compare request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static CompareRequest unmodifiableCompareRequest(final CompareRequest request) {
+        if (request instanceof UnmodifiableCompareRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableCompareRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable CRAM MD5 SASL bind request of the provided
+     * request.
+     * <p>
+     * The returned bind request creates defensive copies of the password in
+     * order to maintain immutability.
+     *
+     * @param request
+     *            The CRAM MD5 SASL bind request to be copied.
+     * @return The new CRAM-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static CRAMMD5SASLBindRequest unmodifiableCRAMMD5SASLBindRequest(
+            final CRAMMD5SASLBindRequest request) {
+        if (request instanceof UnmodifiableCRAMMD5SASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableCRAMMD5SASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable delete request of the provided request.
+     *
+     * @param request
+     *            The add request to be copied.
+     * @return The new delete request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static DeleteRequest unmodifiableDeleteRequest(final DeleteRequest request) {
+        if (request instanceof UnmodifiableDeleteRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableDeleteRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable digest MD5 SASL bind request of the provided
+     * request.
+     * <p>
+     * The returned bind request creates defensive copies of the password in
+     * order to maintain immutability.
+     *
+     * @param request
+     *            The digest MD5 SASL bind request to be copied.
+     * @return The new DIGEST-MD5 SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static DigestMD5SASLBindRequest unmodifiableDigestMD5SASLBindRequest(
+            final DigestMD5SASLBindRequest request) {
+        if (request instanceof UnmodifiableDigestMD5SASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableDigestMD5SASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable external SASL bind request of the provided
+     * request.
+     *
+     * @param request
+     *            The external SASL bind request to be copied.
+     * @return The new External SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ExternalSASLBindRequest unmodifiableExternalSASLBindRequest(
+            final ExternalSASLBindRequest request) {
+        if (request instanceof UnmodifiableExternalSASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableExternalSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable generic bind request of the provided request.
+     * <p>
+     * The returned bind request creates defensive copies of the authentication
+     * value in order to maintain immutability.
+     *
+     * @param request
+     *            The generic bind request to be copied.
+     * @return The new generic bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static GenericBindRequest unmodifiableGenericBindRequest(final GenericBindRequest request) {
+        if (request instanceof UnmodifiableGenericBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableGenericBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable generic extended request of the provided request.
+     *
+     * @param request
+     *            The generic extended request to be copied.
+     * @return The new generic extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static GenericExtendedRequest unmodifiableGenericExtendedRequest(
+            final GenericExtendedRequest request) {
+        if (request instanceof UnmodifiableGenericExtendedRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableGenericExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable GSSAPI SASL bind request of the provided request.
+     * <p>
+     * The returned bind request creates defensive copies of the password in
+     * order to maintain immutability.
+     *
+     * @param request
+     *            The GSSAPI SASL bind request to be copied.
+     * @return The new GSSAPI SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null}.
+     */
+    public static GSSAPISASLBindRequest unmodifiableGSSAPISASLBindRequest(
+            final GSSAPISASLBindRequest request) {
+        if (request instanceof UnmodifiableGSSAPISASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableGSSAPISASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable modify DN request of the provided request.
+     *
+     * @param request
+     *            The modify DN request to be copied.
+     * @return The new modify DN request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ModifyDNRequest unmodifiableModifyDNRequest(final ModifyDNRequest request) {
+        if (request instanceof UnmodifiableModifyDNRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableModifyDNRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable modify request of the provided request.
+     *
+     * @param request
+     *            The modify request to be copied.
+     * @return The new modify request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static ModifyRequest unmodifiableModifyRequest(final ModifyRequest request) {
+        if (request instanceof UnmodifiableModifyRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableModifyRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable password modify extended request of the provided
+     * request.
+     *
+     * @param request
+     *            The password modify extended request to be copied.
+     * @return The new password modify extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static PasswordModifyExtendedRequest unmodifiablePasswordModifyExtendedRequest(
+            final PasswordModifyExtendedRequest request) {
+        if (request instanceof UnmodifiablePasswordModifyExtendedRequestImpl) {
+            return request;
+        }
+        return new UnmodifiablePasswordModifyExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable plain SASL bind request of the provided request.
+     * <p>
+     * The returned bind request creates defensive copies of the password in
+     * order to maintain immutability.
+     *
+     * @param request
+     *            The plain SASL bind request to be copied.
+     * @return The new Plain SASL bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static PlainSASLBindRequest unmodifiablePlainSASLBindRequest(
+            final PlainSASLBindRequest request) {
+        if (request instanceof UnmodifiablePlainSASLBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiablePlainSASLBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable search request of the provided request.
+     *
+     * @param request
+     *            The search request to be copied.
+     * @return The new search request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static SearchRequest unmodifiableSearchRequest(final SearchRequest request) {
+        if (request instanceof UnmodifiableSearchRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableSearchRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable simple bind request of the provided request.
+     * <p>
+     * The returned bind request creates defensive copies of the password in
+     * order to maintain immutability.
+     *
+     * @param request
+     *            The simple bind request to be copied.
+     * @return The new simple bind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static SimpleBindRequest unmodifiableSimpleBindRequest(final SimpleBindRequest request) {
+        if (request instanceof UnmodifiableSimpleBindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableSimpleBindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable startTLS extended request of the provided
+     * request.
+     *
+     * @param request
+     *            The startTLS extended request to be copied.
+     * @return The new start TLS extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static StartTLSExtendedRequest unmodifiableStartTLSExtendedRequest(
+            final StartTLSExtendedRequest request) {
+        if (request instanceof UnmodifiableStartTLSExtendedRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableStartTLSExtendedRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable unbind request of the provided request.
+     *
+     * @param request
+     *            The unbind request to be copied.
+     * @return The new unbind request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static UnbindRequest unmodifiableUnbindRequest(final UnbindRequest request) {
+        if (request instanceof UnmodifiableUnbindRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableUnbindRequestImpl(request);
+    }
+
+    /**
+     * Creates an unmodifiable new Who Am I extended request of the provided
+     * request.
+     *
+     * @param request
+     *            The who Am I extended request to be copied.
+     * @return The new Who Am I extended request.
+     * @throws NullPointerException
+     *             If {@code request} was {@code null} .
+     */
+    public static WhoAmIExtendedRequest unmodifiableWhoAmIExtendedRequest(
+            final WhoAmIExtendedRequest request) {
+        if (request instanceof UnmodifiableWhoAmIExtendedRequestImpl) {
+            return request;
+        }
+        return new UnmodifiableWhoAmIExtendedRequestImpl(request);
+    }
+
+    private Requests() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindClientImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindClientImpl.java
new file mode 100644
index 0000000..a82efcf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindClientImpl.java
@@ -0,0 +1,194 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.ldap.CoreMessages.INFO_SASL_UNSUPPORTED_CALLBACK;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.ChoiceCallback;
+import javax.security.auth.callback.ConfirmationCallback;
+import javax.security.auth.callback.LanguageCallback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.TextInputCallback;
+import javax.security.auth.callback.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.AuthorizeCallback;
+import javax.security.sasl.RealmCallback;
+import javax.security.sasl.RealmChoiceCallback;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+
+/**
+ * SASL bind client implementation.
+ */
+class SASLBindClientImpl extends BindClientImpl implements CallbackHandler {
+    /**
+     * The name of the default protocol used.
+     */
+    static final String SASL_DEFAULT_PROTOCOL = "ldap";
+
+    private final String saslMechanism;
+
+    /**
+     * Creates a new abstract SASL bind client. The next bind request will be a
+     * copy of the provided initial bind request which should be updated in
+     * subsequent bind requests forming part of this authentication.
+     *
+     * @param initialBindRequest
+     *            The initial bind request.
+     */
+    SASLBindClientImpl(final SASLBindRequest initialBindRequest) {
+        super(initialBindRequest);
+        this.saslMechanism = initialBindRequest.getSASLMechanism();
+    }
+
+    @Override
+    public final void handle(final Callback[] callbacks) throws IOException,
+            UnsupportedCallbackException {
+        for (final Callback callback : callbacks) {
+            if (callback instanceof NameCallback) {
+                handle((NameCallback) callback);
+            } else if (callback instanceof PasswordCallback) {
+                handle((PasswordCallback) callback);
+            } else if (callback instanceof AuthorizeCallback) {
+                handle((AuthorizeCallback) callback);
+            } else if (callback instanceof RealmCallback) {
+                handle((RealmCallback) callback);
+            } else if (callback instanceof RealmChoiceCallback) {
+                handle((RealmChoiceCallback) callback);
+            } else if (callback instanceof ChoiceCallback) {
+                handle((ChoiceCallback) callback);
+            } else if (callback instanceof ConfirmationCallback) {
+                handle((ConfirmationCallback) callback);
+            } else if (callback instanceof LanguageCallback) {
+                handle((LanguageCallback) callback);
+            } else if (callback instanceof TextInputCallback) {
+                handle((TextInputCallback) callback);
+            } else if (callback instanceof TextOutputCallback) {
+                handle((TextOutputCallback) callback);
+            } else {
+                final org.forgerock.i18n.LocalizableMessage message =
+                        INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+                throw new UnsupportedCallbackException(callback, message.toString());
+            }
+        }
+    }
+
+    void handle(final AuthorizeCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final ChoiceCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final ConfirmationCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final LanguageCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final NameCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final PasswordCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final RealmCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final RealmChoiceCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final TextInputCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    void handle(final TextOutputCallback callback) throws UnsupportedCallbackException {
+        final org.forgerock.i18n.LocalizableMessage message =
+                INFO_SASL_UNSUPPORTED_CALLBACK.get(saslMechanism, String.valueOf(callback));
+        throw new UnsupportedCallbackException(callback, message.toString());
+    }
+
+    /**
+     * Sets the SASL credentials to be used in the next bind request.
+     *
+     * @param saslCredentials
+     *            The SASL credentials to be used in the next bind request.
+     * @return A reference to this SASL bind client.
+     */
+    final BindClient setNextSASLCredentials(final byte[] saslCredentials) {
+        final ByteString value =
+                (saslCredentials != null) ? ByteString.wrap(saslCredentials) : null;
+        return setNextSASLCredentials(value);
+    }
+
+    /**
+     * Sets the SASL credentials to be used in the next bind request.
+     *
+     * @param saslCredentials
+     *            The SASL credentials to be used in the next bind request.
+     * @return A reference to this SASL bind client.
+     */
+    final BindClient setNextSASLCredentials(final ByteString saslCredentials) {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        final ASN1Writer writer = ASN1.getWriter(builder);
+
+        try {
+            writer.writeOctetString(saslMechanism);
+            if (saslCredentials != null) {
+                writer.writeOctetString(saslCredentials);
+            }
+        } catch (final IOException ioe) {
+            throw new RuntimeException("Error encoding SaslCredentials");
+        }
+
+        return setNextAuthenticationValue(builder.toByteString().toByteArray());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindRequest.java
new file mode 100644
index 0000000..8bb67db
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SASLBindRequest.java
@@ -0,0 +1,79 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The SASL authentication method of the Bind operation allows clients to
+ * authenticate using one of the SASL authentication methods defined in RFC
+ * 4513.
+ * <p>
+ * <TODO>finish doc.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface SASLBindRequest extends BindRequest {
+
+    @Override
+    SASLBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication mechanism identifier for this SASL bind
+     * request as defined by the LDAP protocol, which is always {@code 0xA3}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the name of the Directory object that the client wishes to bind
+     * as, which is always the empty string for SASL authentication.
+     *
+     * @return The name of the Directory object that the client wishes to bind
+     *         as.
+     */
+    @Override
+    String getName();
+
+    /**
+     * Returns the SASL mechanism for this SASL bind request.
+     *
+     * @return The SASL mechanism for this bind request.
+     */
+    String getSASLMechanism();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
new file mode 100644
index 0000000..6bef41d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
@@ -0,0 +1,324 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Search operation is used to request a server to return, subject to access
+ * controls and other restrictions, a set of entries matching a complex search
+ * criterion. This can be used to read attributes from a single entry, from
+ * entries immediately subordinate to a particular entry, or from a whole
+ * subtree of entries.
+ * <p>
+ * Use {@link Requests#newSearchRequest(DN, SearchScope, Filter, String...)} or
+ * {@link Requests#newSearchRequest(String, SearchScope, String, String...)} to
+ * create a new search request.
+ *
+ * <pre>
+ * SearchRequest request = Requests.newSearchRequest(&quot;dc=example,dc=com&quot;, SearchScope.WHOLE_SUBTREE,
+ *         &quot;(sn=Jensen)&quot;, &quot;cn&quot;);
+ * </pre>
+ *
+ * Alternatively, use the
+ * {@link org.forgerock.opendj.ldap.Connection#search(String, SearchScope, String, String...)
+ * Connection.search()} method to specify the arguments directly.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * ConnectionEntryReader reader = connection.search(
+ *          "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn");
+ * </pre>
+ */
+public interface SearchRequest extends Request {
+    /**
+     * Adds the provided attribute name(s) to the list of attributes to be
+     * included with each entry that matches the search criteria. Attributes
+     * that are sub-types of listed attributes are implicitly included.
+     *
+     * @param attributeDescriptions
+     *            The name(s) of the attribute to be included with each entry.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit attribute names to be
+     *             added.
+     * @throws NullPointerException
+     *             If {@code attributeDescriptions} was {@code null}.
+     */
+    SearchRequest addAttribute(String... attributeDescriptions);
+
+    @Override
+    SearchRequest addControl(Control control);
+
+    /**
+     * Returns a {@code List} containing the list of attributes to be included
+     * with each entry that matches the search criteria. Attributes that are
+     * sub-types of listed attributes are implicitly included. The returned
+     * {@code List} may be modified if permitted by this search request.
+     *
+     * @return A {@code List} containing the list of attributes.
+     */
+    List<String> getAttributes();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns an indication as to whether or not alias entries are to be
+     * dereferenced during the search.
+     *
+     * @return The alias dereferencing policy.
+     */
+    DereferenceAliasesPolicy getDereferenceAliasesPolicy();
+
+    /**
+     * Returns the filter that defines the conditions that must be fulfilled in
+     * order for an entry to be returned.
+     *
+     * @return The search filter.
+     */
+    Filter getFilter();
+
+    /**
+     * Returns the distinguished name of the base entry relative to which the
+     * search is to be performed.
+     *
+     * @return The distinguished name of the base entry.
+     */
+    DN getName();
+
+    /**
+     * Returns the scope of the search.
+     *
+     * @return The search scope.
+     */
+    SearchScope getScope();
+
+    /**
+     * Returns the size limit that should be used in order to restrict the
+     * maximum number of entries returned by the search.
+     * <p>
+     * A value of zero (the default) in this field indicates that no
+     * client-requested size limit restrictions are in effect. Servers may also
+     * enforce a maximum number of entries to return.
+     *
+     * @return The size limit that should be used in order to restrict the
+     *         maximum number of entries returned by the search.
+     */
+    int getSizeLimit();
+
+    /**
+     * Indicates whether search result is expected to be limited to a single entry.
+     * <p>
+     * It is the case if size limit is equal to 1 or if scope is equal to <code>SearchScope.BASE_OBJECT</code>.
+     * <p>
+     * If search results contain more than one entry, the search operation will throw
+     * a <code>MultipleEntriesFoundException</code>.
+     *
+     * @return {@code true} if the search is limited to a single entry result,
+     *         or {@code false} (the default) otherwise.
+     */
+    boolean isSingleEntrySearch();
+
+    /**
+     * Returns the time limit that should be used in order to restrict the
+     * maximum time (in seconds) allowed for the search.
+     * <p>
+     * A value of zero (the default) in this field indicates that no
+     * client-requested time limit restrictions are in effect for the search.
+     * Servers may also enforce a maximum time limit for the search.
+     *
+     * @return The time limit that should be used in order to restrict the
+     *         maximum time (in seconds) allowed for the search.
+     */
+    int getTimeLimit();
+
+    /**
+     * Indicates whether search results are to contain both attribute
+     * descriptions and values, or just attribute descriptions.
+     *
+     * @return {@code true} if only attribute descriptions (and not values) are
+     *         to be returned, or {@code false} (the default) if both attribute
+     *         descriptions and values are to be returned.
+     */
+    boolean isTypesOnly();
+
+    /**
+     * Sets the alias dereferencing policy to be used during the search.
+     *
+     * @param policy
+     *            The alias dereferencing policy to be used during the search.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the alias
+     *             dereferencing policy to be set.
+     * @throws NullPointerException
+     *             If {@code policy} was {@code null}.
+     */
+    SearchRequest setDereferenceAliasesPolicy(DereferenceAliasesPolicy policy);
+
+    /**
+     * Sets the filter that defines the conditions that must be fulfilled in
+     * order for an entry to be returned.
+     *
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the filter to be set.
+     * @throws NullPointerException
+     *             If {@code filter} was {@code null}.
+     */
+    SearchRequest setFilter(Filter filter);
+
+    /**
+     * Sets the filter that defines the conditions that must be fulfilled in
+     * order for an entry to be returned.
+     *
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the filter to be set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code filter} is not a valid LDAP string representation
+     *             of a filter.
+     * @throws NullPointerException
+     *             If {@code filter} was {@code null}.
+     */
+    SearchRequest setFilter(String filter);
+
+    /**
+     * Sets the distinguished name of the base entry relative to which the
+     * search is to be performed.
+     *
+     * @param dn
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    SearchRequest setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the base entry relative to which the
+     * search is to be performed.
+     *
+     * @param dn
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @return This search request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    SearchRequest setName(String dn);
+
+    /**
+     * Sets the scope of the search.
+     *
+     * @param scope
+     *            The scope of the search.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the scope to be set.
+     * @throws NullPointerException
+     *             If {@code scope} was {@code null}.
+     */
+    SearchRequest setScope(SearchScope scope);
+
+    /**
+     * Sets the size limit that should be used in order to restrict the maximum
+     * number of entries returned by the search.
+     * <p>
+     * A value of zero (the default) in this field indicates that no
+     * client-requested size limit restrictions are in effect. Servers may also
+     * enforce a maximum number of entries to return.
+     *
+     * @param limit
+     *            The size limit that should be used in order to restrict the
+     *            maximum number of entries returned by the search.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the size limit to be
+     *             set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code limit} was negative.
+     */
+    SearchRequest setSizeLimit(int limit);
+
+    /**
+     * Sets the time limit that should be used in order to restrict the maximum
+     * time (in seconds) allowed for the search.
+     * <p>
+     * A value of zero (the default) in this field indicates that no
+     * client-requested time limit restrictions are in effect for the search.
+     * Servers may also enforce a maximum time limit for the search.
+     *
+     * @param limit
+     *            The time limit that should be used in order to restrict the
+     *            maximum time (in seconds) allowed for the search.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the time limit to be
+     *             set.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code limit} was negative.
+     */
+    SearchRequest setTimeLimit(int limit);
+
+    /**
+     * Specifies whether search results are to contain both attribute
+     * descriptions and values, or just attribute descriptions.
+     *
+     * @param typesOnly
+     *            {@code true} if only attribute descriptions (and not values)
+     *            are to be returned, or {@code false} (the default) if both
+     *            attribute descriptions and values are to be returned.
+     * @return This search request.
+     * @throws UnsupportedOperationException
+     *             If this search request does not permit the types-only
+     *             parameter to be set.
+     */
+    SearchRequest setTypesOnly(boolean typesOnly);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
new file mode 100644
index 0000000..e59d452
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
@@ -0,0 +1,210 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.util.Reject;
+
+/**
+ * Search request implementation.
+ */
+final class SearchRequestImpl extends AbstractRequestImpl<SearchRequest> implements SearchRequest {
+    private final List<String> attributes = new LinkedList<>();
+    private DereferenceAliasesPolicy dereferenceAliasesPolicy = DereferenceAliasesPolicy.NEVER;
+    private Filter filter;
+    private DN name;
+    private SearchScope scope;
+    private int sizeLimit;
+    private int timeLimit;
+    private boolean typesOnly;
+
+    SearchRequestImpl(final DN name, final SearchScope scope, final Filter filter) {
+        this.name = name;
+        this.scope = scope;
+        this.filter = filter;
+    }
+
+    SearchRequestImpl(final SearchRequest searchRequest) {
+        super(searchRequest);
+        this.attributes.addAll(searchRequest.getAttributes());
+        this.name = searchRequest.getName();
+        this.dereferenceAliasesPolicy = searchRequest.getDereferenceAliasesPolicy();
+        this.filter = searchRequest.getFilter();
+        this.scope = searchRequest.getScope();
+        this.sizeLimit = searchRequest.getSizeLimit();
+        this.timeLimit = searchRequest.getTimeLimit();
+        this.typesOnly = searchRequest.isTypesOnly();
+    }
+
+    @Override
+    public SearchRequest addAttribute(final String... attributeDescriptions) {
+        for (final String attributeDescription : attributeDescriptions) {
+            attributes.add(Reject.checkNotNull(attributeDescription));
+        }
+        return this;
+    }
+
+    @Override
+    public List<String> getAttributes() {
+        return attributes;
+    }
+
+    @Override
+    public DereferenceAliasesPolicy getDereferenceAliasesPolicy() {
+        return dereferenceAliasesPolicy;
+    }
+
+    @Override
+    public Filter getFilter() {
+        return filter;
+    }
+
+    @Override
+    public DN getName() {
+        return name;
+    }
+
+    @Override
+    public SearchScope getScope() {
+        return scope;
+    }
+
+    @Override
+    public int getSizeLimit() {
+        return sizeLimit;
+    }
+
+    @Override
+    public boolean isSingleEntrySearch() {
+        return sizeLimit == 1 || SearchScope.BASE_OBJECT.equals(scope);
+    }
+
+    @Override
+    public int getTimeLimit() {
+        return timeLimit;
+    }
+
+    @Override
+    public boolean isTypesOnly() {
+        return typesOnly;
+    }
+
+    @Override
+    public SearchRequest setDereferenceAliasesPolicy(final DereferenceAliasesPolicy policy) {
+        Reject.ifNull(policy);
+
+        this.dereferenceAliasesPolicy = policy;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setFilter(final Filter filter) {
+        Reject.ifNull(filter);
+
+        this.filter = filter;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setFilter(final String filter) {
+        this.filter = Filter.valueOf(filter);
+        return this;
+    }
+
+    @Override
+    public SearchRequest setName(final DN dn) {
+        Reject.ifNull(dn);
+
+        this.name = dn;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setName(final String dn) {
+        Reject.ifNull(dn);
+
+        this.name = DN.valueOf(dn);
+        return this;
+    }
+
+    @Override
+    public SearchRequest setScope(final SearchScope scope) {
+        Reject.ifNull(scope);
+
+        this.scope = scope;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setSizeLimit(final int limit) {
+        Reject.ifFalse(limit >= 0, "negative size limit");
+
+        this.sizeLimit = limit;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setTimeLimit(final int limit) {
+        Reject.ifFalse(limit >= 0, "negative time limit");
+
+        this.timeLimit = limit;
+        return this;
+    }
+
+    @Override
+    public SearchRequest setTypesOnly(final boolean typesOnly) {
+        this.typesOnly = typesOnly;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SearchRequest(name=");
+        builder.append(getName());
+        builder.append(", scope=");
+        builder.append(getScope());
+        builder.append(", dereferenceAliasesPolicy=");
+        builder.append(getDereferenceAliasesPolicy());
+        builder.append(", sizeLimit=");
+        builder.append(getSizeLimit());
+        builder.append(", timeLimit=");
+        builder.append(getTimeLimit());
+        builder.append(", typesOnly=");
+        builder.append(isTypesOnly());
+        builder.append(", filter=");
+        builder.append(getFilter());
+        builder.append(", attributes=");
+        builder.append(getAttributes());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    SearchRequest getThis() {
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequest.java
new file mode 100644
index 0000000..50dbd4f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequest.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The simple authentication method of the Bind Operation provides three
+ * authentication mechanisms:
+ * <ul>
+ * <li>An anonymous authentication mechanism, in which both the name and
+ * password are zero length.
+ * <li>An unauthenticated authentication mechanism using credentials consisting
+ * of a name and a zero length password.
+ * <li>A name/password authentication mechanism using credentials consisting of
+ * a name and a password.
+ * </ul>
+ * {@link Requests} has methods to create a {@code SimpleBindRequest}.
+ *
+ * <pre>
+ * String bindDN = ...;
+ * char[] bindPassword = ...;
+ *
+ * SimpleBindRequest sbr = Requests.newSimpleBindRequest(bindDN, bindPassword);
+ * </pre>
+ *
+ * Alternatively, use
+ * {@link org.forgerock.opendj.ldap.Connection#bind(String, char[])
+ * Connection.bind}.
+ *
+ * <pre>
+ * Connection connection;
+ * String bindDN = ...;
+ * char[] bindPassword = ...;
+ *
+ * connection.bind(bindDN, bindPassword);
+ * </pre>
+ */
+public interface SimpleBindRequest extends BindRequest {
+
+    @Override
+    SimpleBindRequest addControl(Control control);
+
+    @Override
+    BindClient createBindClient(String serverName) throws LdapException;
+
+    /**
+     * Returns the authentication mechanism identifier for this simple bind
+     * request as defined by the LDAP protocol, which is always {@code 0x80}.
+     *
+     * @return The authentication mechanism identifier.
+     */
+    @Override
+    byte getAuthenticationType();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getName();
+
+    /**
+     * Returns the password of the Directory object that the client wishes to
+     * bind as. The password may be empty (but never {@code null}) when used for
+     * of anonymous or unauthenticated binds.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * returned password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @return The password of the Directory object that the client wishes to
+     *         bind as.
+     */
+    byte[] getPassword();
+
+    /**
+     * Sets the name of the Directory object that the client wishes to bind as.
+     * The name may be empty (but never {@code null} when used for of anonymous
+     * binds, or when using SASL authentication. The server shall not
+     * dereference any aliases in locating the named object.
+     * <p>
+     * The LDAP protocol defines the Bind name to be a distinguished name,
+     * however some LDAP implementations have relaxed this constraint and allow
+     * other identities to be used, such as the user's email address.
+     *
+     * @param name
+     *            The name of the Directory object that the client wishes to
+     *            bind as.
+     * @return This bind request.
+     * @throws UnsupportedOperationException
+     *             If this bind request does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    SimpleBindRequest setName(String name);
+
+    /**
+     * Sets the password of the Directory object that the client wishes to bind
+     * as. The password may be empty (but never {@code null}) when used for of
+     * anonymous or unauthenticated binds.
+     * <p>
+     * Unless otherwise indicated, implementations will store a reference to the
+     * provided password byte array, allowing applications to overwrite the
+     * password after it has been used.
+     *
+     * @param password
+     *            The password of the Directory object that the client wishes to
+     *            bind as, which may be empty.
+     * @return This simple bind request.
+     * @throws UnsupportedOperationException
+     *             If this simple bind request does not permit the password to
+     *             be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    SimpleBindRequest setPassword(byte[] password);
+
+    /**
+     * Sets the password of the Directory object that the client wishes to bind
+     * as. The password will be converted to a UTF-8 octet string. The password
+     * may be empty (but never {@code null}) when used for of anonymous or
+     * unauthenticated binds. Subsequent modifications to the {@code password}
+     * array will not alter this bind request.
+     *
+     * @param password
+     *            The password of the Directory object that the client wishes to
+     *            bind as, which may be empty.
+     * @return This simple bind request.
+     * @throws UnsupportedOperationException
+     *             If this simple bind request does not permit the password to
+     *             be set.
+     * @throws NullPointerException
+     *             If {@code password} was {@code null}.
+     */
+    SimpleBindRequest setPassword(char[] password);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestImpl.java
new file mode 100644
index 0000000..483513c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestImpl.java
@@ -0,0 +1,97 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.LdapException;
+
+import com.forgerock.opendj.util.StaticUtils;
+import org.forgerock.util.Reject;
+
+/**
+ * Simple bind request implementation.
+ */
+final class SimpleBindRequestImpl extends AbstractBindRequest<SimpleBindRequest> implements
+        SimpleBindRequest {
+    private String name = "".intern();
+    private byte[] password = new byte[0];
+
+    SimpleBindRequestImpl(final SimpleBindRequest simpleBindRequest) {
+        super(simpleBindRequest);
+        this.name = simpleBindRequest.getName();
+        this.password = StaticUtils.copyOfBytes(simpleBindRequest.getPassword());
+    }
+
+    SimpleBindRequestImpl(final String name, final byte[] password) {
+        this.name = name;
+        this.password = password;
+    }
+
+    @Override
+    public BindClient createBindClient(final String serverName) throws LdapException {
+        return new BindClientImpl(this).setNextAuthenticationValue(password);
+    }
+
+    @Override
+    public byte getAuthenticationType() {
+        return LDAP.TYPE_AUTHENTICATION_SIMPLE;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public byte[] getPassword() {
+        return password;
+    }
+
+    @Override
+    public SimpleBindRequest setName(final String name) {
+        Reject.ifNull(name);
+        this.name = name;
+        return this;
+    }
+
+    @Override
+    public SimpleBindRequest setPassword(final byte[] password) {
+        Reject.ifNull(password);
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public SimpleBindRequest setPassword(final char[] password) {
+        Reject.ifNull(password);
+        this.password = StaticUtils.getBytes(password);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SimpleBindRequest(name=");
+        builder.append(getName());
+        builder.append(", authentication=simple");
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java
new file mode 100644
index 0000000..8de4d22
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java
@@ -0,0 +1,194 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+
+/**
+ * The start TLS extended request as defined in RFC 4511. The Start Transport
+ * Layer Security (StartTLS) operation's purpose is to initiate installation of
+ * a TLS layer.
+ * <p>
+ * Use an {@link org.forgerock.opendj.ldap.SSLContextBuilder SSLContextBuilder}
+ * when setting up LDAP options needed to use StartTLS.
+ * {@link org.forgerock.opendj.ldap.TrustManagers TrustManagers} has methods you
+ * can use to set the trust manager for the SSL context builder.
+ *
+ * <pre>
+ * LDAPOptions options = new LDAPOptions();
+ * SSLContext sslContext =
+ *         new SSLContextBuilder().setTrustManager(...).getSSLContext();
+ * options.setSSLContext(sslContext);
+ * options.setUseStartTLS(true);
+ *
+ * String host = ...;
+ * int port = ...;
+ * LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
+ * Connection connection = factory.getConnection();
+ * // Connection uses StartTLS...
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
+ *      Directory Access Protocol (LDAP): The Protocol </a>
+ */
+public interface StartTLSExtendedRequest extends ExtendedRequest<ExtendedResult> {
+
+    /**
+     * A decoder which can be used to decode start TLS extended operation
+     * requests.
+     */
+    ExtendedRequestDecoder<StartTLSExtendedRequest, ExtendedResult> DECODER =
+            new StartTLSExtendedRequestImpl.RequestDecoder();
+
+    /**
+     * The OID for the start TLS extended operation request.
+     */
+    String OID = "1.3.6.1.4.1.1466.20037";
+
+    @Override
+    StartTLSExtendedRequest addControl(Control control);
+
+    /**
+     * Adds the cipher suites enabled for secure connections with the Directory
+     * Server. The suites must be supported by the SSLContext specified in
+     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
+     * method, only the suites listed in the protocols parameter are enabled for
+     * use.
+     *
+     * @param suites
+     *            Names of all the suites to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled cipher suites to be set.
+     */
+    StartTLSExtendedRequest addEnabledCipherSuite(String... suites);
+
+    /**
+     * Adds the cipher suites enabled for secure connections with the Directory
+     * Server. The suites must be supported by the SSLContext specified in
+     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
+     * method, only the suites listed in the protocols parameter are enabled for
+     * use.
+     *
+     * @param suites
+     *            Names of all the suites to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled cipher suites to be set.
+     */
+    StartTLSExtendedRequest addEnabledCipherSuite(Collection<String> suites);
+
+    /**
+     * Adds the protocol versions enabled for secure connections with the
+     * Directory Server. The protocols must be supported by the SSLContext
+     * specified in {@link #setSSLContext(SSLContext)}. Following a successful
+     * call to this method, only the protocols listed in the protocols parameter
+     * are enabled for use.
+     *
+     * @param protocols
+     *            Names of all the protocols to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled protocols to be set.
+     */
+    StartTLSExtendedRequest addEnabledProtocol(String... protocols);
+
+    /**
+     * Adds the protocol versions enabled for secure connections with the
+     * Directory Server. The protocols must be supported by the SSLContext
+     * specified in {@link #setSSLContext(SSLContext)}. Following a successful
+     * call to this method, only the protocols listed in the protocols parameter
+     * are enabled for use.
+     *
+     * @param protocols
+     *            Names of all the protocols to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled protocols to be set.
+     */
+    StartTLSExtendedRequest addEnabledProtocol(Collection<String> protocols);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the names of the protocol versions which are currently enabled
+     * for secure connections with the Directory Server.
+     *
+     * @return an array of protocols or empty set if the default protocols are
+     *         to be used.
+     */
+    List<String> getEnabledCipherSuites();
+
+    /**
+     * Returns the names of the protocol versions which are currently enabled
+     * for secure connections with the Directory Server.
+     *
+     * @return an array of protocols or empty set if the default protocols are
+     *         to be used.
+     */
+    List<String> getEnabledProtocols();
+
+    @Override
+    String getOID();
+
+    @Override
+    ExtendedResultDecoder<ExtendedResult> getResultDecoder();
+
+    /**
+     * Returns the SSLContext that should be used when installing the TLS layer.
+     *
+     * @return The SSLContext that should be used when installing the TLS layer.
+     */
+    SSLContext getSSLContext();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    /**
+     * Sets the SSLContext that should be used when installing the TLS layer.
+     *
+     * @param sslContext
+     *            The SSLContext that should be used when installing the TLS
+     *            layer.
+     * @return This startTLS request.
+     */
+    StartTLSExtendedRequest setSSLContext(SSLContext sslContext);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java
new file mode 100644
index 0000000..c0ea5bb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java
@@ -0,0 +1,185 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Start TLS extended request implementation.
+ */
+final class StartTLSExtendedRequestImpl extends
+        AbstractExtendedRequest<StartTLSExtendedRequest, ExtendedResult> implements
+        StartTLSExtendedRequest {
+    static final class RequestDecoder implements
+            ExtendedRequestDecoder<StartTLSExtendedRequest, ExtendedResult> {
+
+        @Override
+        public StartTLSExtendedRequest decodeExtendedRequest(final ExtendedRequest<?> request,
+                final DecodeOptions options) throws DecodeException {
+            // TODO: Check the OID and that the value is not present.
+            final StartTLSExtendedRequest newRequest = new StartTLSExtendedRequestImpl();
+            for (final Control control : request.getControls()) {
+                newRequest.addControl(control);
+            }
+            return newRequest;
+        }
+    }
+
+    private static final class ResultDecoder extends AbstractExtendedResultDecoder<ExtendedResult> {
+        @Override
+        public ExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            // TODO: Should we check oid is NOT null and matches but
+            // value is null?
+            return result;
+        }
+
+        @Override
+        public GenericExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newGenericExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+    }
+
+    /** No need to expose this. */
+    private static final ExtendedResultDecoder<ExtendedResult> RESULT_DECODER = new ResultDecoder();
+
+    /** The list of cipher suite. */
+    private final List<String> enabledCipherSuites = new LinkedList<>();
+
+    /** The list of protocols. */
+    private final List<String> enabledProtocols = new LinkedList<>();
+
+    private SSLContext sslContext;
+
+    StartTLSExtendedRequestImpl(final SSLContext sslContext) {
+        Reject.ifNull(sslContext);
+        this.sslContext = sslContext;
+    }
+
+    StartTLSExtendedRequestImpl(final StartTLSExtendedRequest startTLSExtendedRequest) {
+        super(startTLSExtendedRequest);
+        this.sslContext = startTLSExtendedRequest.getSSLContext();
+        this.enabledCipherSuites.addAll(startTLSExtendedRequest.getEnabledCipherSuites());
+        this.enabledProtocols.addAll(startTLSExtendedRequest.getEnabledProtocols());
+    }
+
+    /** Prevent instantiation. */
+    private StartTLSExtendedRequestImpl() {
+        // Nothing to do.
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final String... suites) {
+        return addEnabledCipherSuite(Arrays.asList(suites));
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final Collection<String> suites) {
+        for (final String suite : suites) {
+            this.enabledCipherSuites.add(Reject.checkNotNull(suite));
+        }
+        return this;
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final String... protocols) {
+        return addEnabledProtocol(Arrays.asList(protocols));
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final Collection<String> protocols) {
+        for (final String protocol : protocols) {
+            this.enabledProtocols.add(Reject.checkNotNull(protocol));
+        }
+        return this;
+    }
+
+    @Override
+    public List<String> getEnabledCipherSuites() {
+        return this.enabledCipherSuites;
+    }
+
+    @Override
+    public List<String> getEnabledProtocols() {
+        return this.enabledProtocols;
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ExtendedResultDecoder<ExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public SSLContext getSSLContext() {
+        return sslContext;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public StartTLSExtendedRequest setSSLContext(final SSLContext sslContext) {
+        Reject.ifNull(sslContext);
+        this.sslContext = sslContext;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("StartTLSExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequest.java
new file mode 100644
index 0000000..0fcab4f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequest.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The Unbind operation allows a client to terminate an LDAP session.
+ */
+public interface UnbindRequest extends Request {
+
+    @Override
+    UnbindRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequestImpl.java
new file mode 100644
index 0000000..3bd3337
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnbindRequestImpl.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Unbind request implementation.
+ */
+final class UnbindRequestImpl extends AbstractRequestImpl<UnbindRequest> implements UnbindRequest {
+
+    UnbindRequestImpl() {
+        // Do nothing.
+    }
+
+    UnbindRequestImpl(final UnbindRequest unbindRequest) {
+        super(unbindRequest);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("UnbindRequest(controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    UnbindRequest getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAbandonRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAbandonRequestImpl.java
new file mode 100644
index 0000000..c1265b7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAbandonRequestImpl.java
@@ -0,0 +1,37 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Unmodifiable abandon request implementation.
+ */
+final class UnmodifiableAbandonRequestImpl extends AbstractUnmodifiableRequest<AbandonRequest>
+        implements AbandonRequest {
+    UnmodifiableAbandonRequestImpl(final AbandonRequest request) {
+        super(request);
+    }
+
+    @Override
+    public int getRequestID() {
+        return impl.getRequestID();
+    }
+
+    @Override
+    public AbandonRequest setRequestID(final int id) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java
new file mode 100644
index 0000000..cc98f18
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java
@@ -0,0 +1,180 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+import com.forgerock.opendj.util.Iterables;
+
+/**
+ * Unmodifiable add request implementation.
+ */
+final class UnmodifiableAddRequestImpl extends AbstractUnmodifiableRequest<AddRequest> implements
+        AddRequest {
+    private static final Function<Attribute, Attribute, NeverThrowsException> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
+            new Function<Attribute, Attribute, NeverThrowsException>() {
+                @Override
+                public Attribute apply(final Attribute value) {
+                    return Attributes.unmodifiableAttribute(value);
+                }
+            };
+
+    UnmodifiableAddRequestImpl(final AddRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute,
+            final Collection<? super ByteString> duplicateValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest addAttribute(final String attributeDescription, final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest clearAttributes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean containsAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return impl.containsAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean containsAttribute(final String attributeDescription, final Object... values) {
+        return impl.containsAttribute(attributeDescription, values);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes() {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(
+                impl.getAllAttributes(), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(impl
+                .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(impl
+                .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        final Attribute attribute = impl.getAttribute(attributeDescription);
+        if (attribute != null) {
+            return Attributes.unmodifiableAttribute(attribute);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Attribute getAttribute(final String attributeDescription) {
+        final Attribute attribute = impl.getAttribute(attributeDescription);
+        if (attribute != null) {
+            return Attributes.unmodifiableAttribute(attribute);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public int getAttributeCount() {
+        return impl.getAttributeCount();
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean removeAttribute(final AttributeDescription attributeDescription) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest removeAttribute(final String attributeDescription, final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean replaceAttribute(final Attribute attribute) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest replaceAttribute(final String attributeDescription, final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AddRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAnonymousSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAnonymousSASLBindRequestImpl.java
new file mode 100644
index 0000000..1d9df29
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAnonymousSASLBindRequestImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Unmodifiable anonymous SASL bind request implementation.
+ */
+final class UnmodifiableAnonymousSASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<AnonymousSASLBindRequest> implements
+        AnonymousSASLBindRequest {
+    UnmodifiableAnonymousSASLBindRequestImpl(final AnonymousSASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getTraceString() {
+        return impl.getTraceString();
+    }
+
+    @Override
+    public AnonymousSASLBindRequest setTraceString(final String traceString) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCRAMMD5SASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCRAMMD5SASLBindRequestImpl.java
new file mode 100644
index 0000000..863f6eb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCRAMMD5SASLBindRequestImpl.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable CRAM-MD5 SASL bind request implementation.
+ */
+final class UnmodifiableCRAMMD5SASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<CRAMMD5SASLBindRequest> implements
+        CRAMMD5SASLBindRequest {
+    UnmodifiableCRAMMD5SASLBindRequestImpl(final CRAMMD5SASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return impl.getAuthenticationID();
+    }
+
+    @Override
+    public byte[] getPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getPassword());
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setAuthenticationID(final String authenticationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CRAMMD5SASLBindRequest setPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCancelExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCancelExtendedRequestImpl.java
new file mode 100644
index 0000000..d83e43a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCancelExtendedRequestImpl.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * Unmodifiable cancel extended request implementation.
+ */
+final class UnmodifiableCancelExtendedRequestImpl extends
+        AbstractUnmodifiableExtendedRequest<CancelExtendedRequest, ExtendedResult> implements
+        CancelExtendedRequest {
+    UnmodifiableCancelExtendedRequestImpl(final CancelExtendedRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public int getRequestID() {
+        return impl.getRequestID();
+    }
+
+    @Override
+    public CancelExtendedRequest setRequestID(final int id) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCompareRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCompareRequestImpl.java
new file mode 100644
index 0000000..c897005
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableCompareRequestImpl.java
@@ -0,0 +1,77 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+
+/**
+ * Unmodifiable compare request implementation.
+ */
+final class UnmodifiableCompareRequestImpl extends AbstractUnmodifiableRequest<CompareRequest>
+        implements CompareRequest {
+    UnmodifiableCompareRequestImpl(final CompareRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public ByteString getAssertionValue() {
+        return impl.getAssertionValue();
+    }
+
+    @Override
+    public String getAssertionValueAsString() {
+        return impl.getAssertionValueAsString();
+    }
+
+    @Override
+    public AttributeDescription getAttributeDescription() {
+        return impl.getAttributeDescription();
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public CompareRequest setAssertionValue(final Object value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CompareRequest setAttributeDescription(final AttributeDescription attributeDescription) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CompareRequest setAttributeDescription(final String attributeDescription) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CompareRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CompareRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDeleteRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDeleteRequestImpl.java
new file mode 100644
index 0000000..d67f45d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDeleteRequestImpl.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * Unmodifiable delete request implementation.
+ */
+final class UnmodifiableDeleteRequestImpl extends AbstractUnmodifiableRequest<DeleteRequest>
+        implements DeleteRequest {
+    UnmodifiableDeleteRequestImpl(final DeleteRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public DeleteRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DeleteRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDigestMD5SASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDigestMD5SASLBindRequestImpl.java
new file mode 100644
index 0000000..dcf7c47
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableDigestMD5SASLBindRequestImpl.java
@@ -0,0 +1,141 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable digest-MD5 SASL bind request implementation.
+ */
+final class UnmodifiableDigestMD5SASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<DigestMD5SASLBindRequest> implements
+        DigestMD5SASLBindRequest {
+    UnmodifiableDigestMD5SASLBindRequestImpl(final DigestMD5SASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest addAdditionalAuthParam(final String name, final String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest addQOP(final String... qopValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<String, String> getAdditionalAuthParams() {
+        return Collections.unmodifiableMap(impl.getAdditionalAuthParams());
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return impl.getAuthenticationID();
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return impl.getAuthorizationID();
+    }
+
+    @Override
+    public String getCipher() {
+        return impl.getCipher();
+    }
+
+    @Override
+    public int getMaxReceiveBufferSize() {
+        return impl.getMaxReceiveBufferSize();
+    }
+
+    @Override
+    public int getMaxSendBufferSize() {
+        return impl.getMaxSendBufferSize();
+    }
+
+    @Override
+    public byte[] getPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getPassword());
+    }
+
+    @Override
+    public List<String> getQOPs() {
+        return Collections.unmodifiableList(impl.getQOPs());
+    }
+
+    @Override
+    public String getRealm() {
+        return impl.getRealm();
+    }
+
+    @Override
+    public boolean isServerAuth() {
+        return impl.isServerAuth();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setAuthenticationID(final String authenticationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setAuthorizationID(final String authorizationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setCipher(final String cipher) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setMaxReceiveBufferSize(final int size) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setMaxSendBufferSize(final int size) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setRealm(final String realm) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DigestMD5SASLBindRequest setServerAuth(final boolean serverAuth) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableExternalSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableExternalSASLBindRequestImpl.java
new file mode 100644
index 0000000..2e70fcc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableExternalSASLBindRequestImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Unmodifiable external SASL bind request implementation.
+ */
+final class UnmodifiableExternalSASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<ExternalSASLBindRequest> implements
+        ExternalSASLBindRequest {
+    UnmodifiableExternalSASLBindRequestImpl(final ExternalSASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return impl.getAuthorizationID();
+    }
+
+    @Override
+    public ExternalSASLBindRequest setAuthorizationID(final String authorizationID) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGSSAPISASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGSSAPISASLBindRequestImpl.java
new file mode 100644
index 0000000..59f12cc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGSSAPISASLBindRequestImpl.java
@@ -0,0 +1,152 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable GSSAPI SASL bind request implementation.
+ */
+final class UnmodifiableGSSAPISASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<GSSAPISASLBindRequest> implements GSSAPISASLBindRequest {
+    UnmodifiableGSSAPISASLBindRequestImpl(final GSSAPISASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public GSSAPISASLBindRequest addAdditionalAuthParam(final String name, final String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest addQOP(final String... qopValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<String, String> getAdditionalAuthParams() {
+        return Collections.unmodifiableMap(impl.getAdditionalAuthParams());
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return impl.getAuthenticationID();
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return impl.getAuthorizationID();
+    }
+
+    @Override
+    public String getKDCAddress() {
+        return impl.getKDCAddress();
+    }
+
+    @Override
+    public int getMaxReceiveBufferSize() {
+        return impl.getMaxReceiveBufferSize();
+    }
+
+    @Override
+    public int getMaxSendBufferSize() {
+        return impl.getMaxSendBufferSize();
+    }
+
+    @Override
+    public byte[] getPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getPassword());
+    }
+
+    @Override
+    public List<String> getQOPs() {
+        return Collections.unmodifiableList(impl.getQOPs());
+    }
+
+    @Override
+    public String getRealm() {
+        return impl.getRealm();
+    }
+
+    @Override
+    public Subject getSubject() {
+        return impl.getSubject();
+    }
+
+    @Override
+    public boolean isServerAuth() {
+        return impl.isServerAuth();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setAuthenticationID(final String authenticationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setAuthorizationID(final String authorizationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setKDCAddress(final String address) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setMaxReceiveBufferSize(final int size) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setMaxSendBufferSize(final int size) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setRealm(final String realm) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setServerAuth(final boolean serverAuth) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GSSAPISASLBindRequest setSubject(final Subject subject) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericBindRequestImpl.java
new file mode 100644
index 0000000..4cde72b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericBindRequestImpl.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable generic bind request implementation.
+ */
+final class UnmodifiableGenericBindRequestImpl extends
+        AbstractUnmodifiableBindRequest<GenericBindRequest> implements GenericBindRequest {
+    UnmodifiableGenericBindRequestImpl(final GenericBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public byte[] getAuthenticationValue() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getAuthenticationValue());
+    }
+
+    @Override
+    public GenericBindRequest setAuthenticationType(final byte type) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GenericBindRequest setAuthenticationValue(final byte[] bytes) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GenericBindRequest setName(final String name) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericExtendedRequestImpl.java
new file mode 100644
index 0000000..9d91b68
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableGenericExtendedRequestImpl.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+
+/**
+ * Unmodifiable generic extended request implementation.
+ */
+final class UnmodifiableGenericExtendedRequestImpl extends
+        AbstractUnmodifiableExtendedRequest<GenericExtendedRequest, GenericExtendedResult>
+        implements GenericExtendedRequest {
+    UnmodifiableGenericExtendedRequestImpl(final GenericExtendedRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public GenericExtendedRequest setOID(final String oid) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GenericExtendedRequest setValue(final Object value) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyDNRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyDNRequestImpl.java
new file mode 100644
index 0000000..93934d6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyDNRequestImpl.java
@@ -0,0 +1,92 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+
+/**
+ * Unmodifiable modify DN request implementation.
+ */
+final class UnmodifiableModifyDNRequestImpl extends AbstractUnmodifiableRequest<ModifyDNRequest>
+        implements ModifyDNRequest {
+    UnmodifiableModifyDNRequestImpl(final ModifyDNRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public RDN getNewRDN() {
+        return impl.getNewRDN();
+    }
+
+    @Override
+    public DN getNewSuperior() {
+        return impl.getNewSuperior();
+    }
+
+    @Override
+    public boolean isDeleteOldRDN() {
+        return impl.isDeleteOldRDN();
+    }
+
+    @Override
+    public ModifyDNRequest setDeleteOldRDN(final boolean deleteOldRDN) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setNewRDN(final RDN rdn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setNewRDN(final String rdn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setNewSuperior(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyDNRequest setNewSuperior(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyRequestImpl.java
new file mode 100644
index 0000000..e49031e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableModifyRequestImpl.java
@@ -0,0 +1,92 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Functions;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Collections2;
+
+/**
+ * Unmodifiable modify request implementation.
+ */
+final class UnmodifiableModifyRequestImpl extends AbstractUnmodifiableRequest<ModifyRequest>
+        implements ModifyRequest {
+    UnmodifiableModifyRequestImpl(final ModifyRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public <R, P> R accept(final ChangeRecordVisitor<R, P> v, final P p) {
+        return v.visitChangeRecord(p, this);
+    }
+
+    @Override
+    public ModifyRequest addModification(final Modification modification) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyRequest addModification(final ModificationType type,
+            final String attributeDescription, final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<Modification> getModifications() {
+        // We need to make all attributes unmodifiable as well.
+        final Function<Modification, Modification, NeverThrowsException> function =
+                new Function<Modification, Modification, NeverThrowsException>() {
+                    @Override
+                    public Modification apply(final Modification value) {
+                        final ModificationType type = value.getModificationType();
+                        final Attribute attribute = Attributes.unmodifiableAttribute(value.getAttribute());
+                        return new Modification(type, attribute);
+                    }
+                };
+
+        final List<Modification> unmodifiableModifications =
+                Collections2.transformedList(impl.getModifications(), function, Functions
+                        .<Modification> identityFunction());
+        return Collections.unmodifiableList(unmodifiableModifications);
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public ModifyRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ModifyRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePasswordModifyExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePasswordModifyExtendedRequestImpl.java
new file mode 100644
index 0000000..c8bd634
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePasswordModifyExtendedRequestImpl.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable password modify extended request implementation.
+ */
+final class UnmodifiablePasswordModifyExtendedRequestImpl
+        extends
+        AbstractUnmodifiableExtendedRequest<PasswordModifyExtendedRequest, PasswordModifyExtendedResult>
+        implements PasswordModifyExtendedRequest {
+    UnmodifiablePasswordModifyExtendedRequestImpl(final PasswordModifyExtendedRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public byte[] getNewPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getNewPassword());
+    }
+
+    @Override
+    public byte[] getOldPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getOldPassword());
+    }
+
+    @Override
+    public ByteString getUserIdentity() {
+        return impl.getUserIdentity();
+    }
+
+    @Override
+    public String getUserIdentityAsString() {
+        return impl.getUserIdentityAsString();
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setNewPassword(final byte[] newPassword) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setNewPassword(final char[] newPassword) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setOldPassword(final byte[] oldPassword) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setOldPassword(final char[] oldPassword) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PasswordModifyExtendedRequest setUserIdentity(final Object userIdentity) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePlainSASLBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePlainSASLBindRequestImpl.java
new file mode 100644
index 0000000..25a19df
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiablePlainSASLBindRequestImpl.java
@@ -0,0 +1,66 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable plain SASL bind request implementation.
+ */
+final class UnmodifiablePlainSASLBindRequestImpl extends
+        AbstractUnmodifiableSASLBindRequest<PlainSASLBindRequest> implements PlainSASLBindRequest {
+    UnmodifiablePlainSASLBindRequestImpl(final PlainSASLBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getAuthenticationID() {
+        return impl.getAuthenticationID();
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return impl.getAuthorizationID();
+    }
+
+    @Override
+    public byte[] getPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getPassword());
+    }
+
+    @Override
+    public PlainSASLBindRequest setAuthenticationID(final String authenticationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PlainSASLBindRequest setAuthorizationID(final String authorizationID) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PlainSASLBindRequest setPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PlainSASLBindRequest setPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
new file mode 100644
index 0000000..a2b2d78
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
@@ -0,0 +1,131 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+
+/**
+ * Unmodifiable search request implementation.
+ */
+final class UnmodifiableSearchRequestImpl extends AbstractUnmodifiableRequest<SearchRequest>
+        implements SearchRequest {
+    UnmodifiableSearchRequestImpl(final SearchRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public SearchRequest addAttribute(final String... attributeDescriptions) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getAttributes() {
+        return Collections.unmodifiableList(impl.getAttributes());
+    }
+
+    @Override
+    public DereferenceAliasesPolicy getDereferenceAliasesPolicy() {
+        return impl.getDereferenceAliasesPolicy();
+    }
+
+    @Override
+    public Filter getFilter() {
+        return impl.getFilter();
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public SearchScope getScope() {
+        return impl.getScope();
+    }
+
+    @Override
+    public int getSizeLimit() {
+        return impl.getSizeLimit();
+    }
+
+    @Override
+    public boolean isSingleEntrySearch() {
+        return impl.isSingleEntrySearch();
+    }
+
+    @Override
+    public int getTimeLimit() {
+        return impl.getTimeLimit();
+    }
+
+    @Override
+    public boolean isTypesOnly() {
+        return impl.isTypesOnly();
+    }
+
+    @Override
+    public SearchRequest setDereferenceAliasesPolicy(final DereferenceAliasesPolicy policy) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setFilter(final Filter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setFilter(final String filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setScope(final SearchScope scope) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setSizeLimit(final int limit) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setTimeLimit(final int limit) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchRequest setTypesOnly(final boolean typesOnly) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSimpleBindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSimpleBindRequestImpl.java
new file mode 100644
index 0000000..f577bb4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSimpleBindRequestImpl.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Unmodifiable simple bind request implementation.
+ */
+final class UnmodifiableSimpleBindRequestImpl extends
+        AbstractUnmodifiableBindRequest<SimpleBindRequest> implements SimpleBindRequest {
+    UnmodifiableSimpleBindRequestImpl(final SimpleBindRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public byte[] getPassword() {
+        // Defensive copy.
+        return StaticUtils.copyOfBytes(impl.getPassword());
+    }
+
+    @Override
+    public SimpleBindRequest setName(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SimpleBindRequest setPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SimpleBindRequest setPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java
new file mode 100644
index 0000000..85762a2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java
@@ -0,0 +1,77 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * Unmodifiable start TLS extended request implementation.
+ */
+final class UnmodifiableStartTLSExtendedRequestImpl extends
+        AbstractUnmodifiableExtendedRequest<StartTLSExtendedRequest, ExtendedResult> implements
+        StartTLSExtendedRequest {
+    UnmodifiableStartTLSExtendedRequestImpl(final StartTLSExtendedRequest impl) {
+        super(impl);
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final Collection<String> suites) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final String... suites) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final Collection<String> protocols) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final String... protocols) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getEnabledCipherSuites() {
+        return Collections.unmodifiableList(impl.getEnabledCipherSuites());
+    }
+
+    @Override
+    public List<String> getEnabledProtocols() {
+        return Collections.unmodifiableList(impl.getEnabledProtocols());
+    }
+
+    @Override
+    public SSLContext getSSLContext() {
+        return impl.getSSLContext();
+    }
+
+    @Override
+    public StartTLSExtendedRequest setSSLContext(final SSLContext sslContext) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableUnbindRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableUnbindRequestImpl.java
new file mode 100644
index 0000000..c82efc0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableUnbindRequestImpl.java
@@ -0,0 +1,27 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+/**
+ * Unmodifiable unbind request implementation.
+ */
+final class UnmodifiableUnbindRequestImpl extends AbstractUnmodifiableRequest<UnbindRequest>
+        implements UnbindRequest {
+    UnmodifiableUnbindRequestImpl(final UnbindRequest impl) {
+        super(impl);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableWhoAmIExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableWhoAmIExtendedRequestImpl.java
new file mode 100644
index 0000000..3c8383c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableWhoAmIExtendedRequestImpl.java
@@ -0,0 +1,30 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.opendj.ldap.responses.WhoAmIExtendedResult;
+
+/**
+ * Unmodifiable Who Am I extended request implementation.
+ */
+final class UnmodifiableWhoAmIExtendedRequestImpl extends
+        AbstractUnmodifiableExtendedRequest<WhoAmIExtendedRequest, WhoAmIExtendedResult> implements
+        WhoAmIExtendedRequest {
+    UnmodifiableWhoAmIExtendedRequestImpl(final WhoAmIExtendedRequest impl) {
+        super(impl);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequest.java
new file mode 100644
index 0000000..f1ad75c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequest.java
@@ -0,0 +1,105 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.WhoAmIExtendedResult;
+
+/**
+ * The who am I extended request as defined in RFC 4532. This operation allows
+ * clients to obtain the primary authorization identity, in its primary form,
+ * that the server has associated with the user or application entity.
+ * <p>
+ * The following example demonstrates use of the Who Am I? request and response.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String name = ...;
+ * char[] password = ...;
+ *
+ * Result result = connection.bind(name, password);
+ * if (result.isSuccess()) {
+ *     WhoAmIExtendedRequest request = Requests.newWhoAmIExtendedRequest();
+ *     WhoAmIExtendedResult extResult = connection.extendedRequest(request);
+ *
+ *     if (extResult.isSuccess()) {
+ *         // Authz ID: "  + extResult.getAuthorizationID());
+ *     }
+ * }
+ * </pre>
+ *
+ * This operation may preferable to the Authorization Identity Controls
+ * mechanism defined in RFC 3829, which uses Bind request and response controls
+ * to request and return the authorization identity. Bind controls are not
+ * protected by security layers established by the Bind operation that includes
+ * them. While it is possible to establish security layers using StartTLS prior
+ * to the Bind operation, it is often desirable to use security layers
+ * established by the Bind operation. An extended operation sent after a Bind
+ * operation is protected by the security layers established by the Bind
+ * operation.
+ *
+ * @see WhoAmIExtendedResult
+ * @see org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl
+ * @see <a href="http://tools.ietf.org/html/rfc4532">RFC 4532 - Lightweight
+ *      Directory Access Protocol (LDAP) "Who am I?" Operation </a>
+ * @see <a href="http://tools.ietf.org/html/rfc3829">RFC 3829 - Lightweight
+ *      Directory Access Protocol (LDAP) Authorization Identity Request and
+ *      Response Controls </a>
+ */
+public interface WhoAmIExtendedRequest extends ExtendedRequest<WhoAmIExtendedResult> {
+
+    /**
+     * A decoder which can be used to decode who am I extended operation
+     * requests.
+     */
+    ExtendedRequestDecoder<WhoAmIExtendedRequest, WhoAmIExtendedResult> DECODER =
+            new WhoAmIExtendedRequestImpl.RequestDecoder();
+
+    /**
+     * The OID for the who am I extended operation request.
+     */
+    String OID = "1.3.6.1.4.1.4203.1.11.3";
+
+    @Override
+    WhoAmIExtendedRequest addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getOID();
+
+    @Override
+    ExtendedResultDecoder<WhoAmIExtendedResult> getResultDecoder();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestImpl.java
new file mode 100644
index 0000000..3d284c4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestImpl.java
@@ -0,0 +1,135 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.WhoAmIExtendedResult;
+
+/**
+ * Who Am I extended request implementation.
+ */
+final class WhoAmIExtendedRequestImpl extends
+        AbstractExtendedRequest<WhoAmIExtendedRequest, WhoAmIExtendedResult> implements
+        WhoAmIExtendedRequest {
+
+    static final class RequestDecoder implements
+            ExtendedRequestDecoder<WhoAmIExtendedRequest, WhoAmIExtendedResult> {
+
+        @Override
+        public WhoAmIExtendedRequest decodeExtendedRequest(final ExtendedRequest<?> request,
+                final DecodeOptions options) throws DecodeException {
+            // TODO: Check the OID and that the value is not present.
+            final WhoAmIExtendedRequest newRequest = new WhoAmIExtendedRequestImpl();
+            for (final Control control : request.getControls()) {
+                newRequest.addControl(control);
+            }
+            return newRequest;
+        }
+    }
+
+    private static final class ResultDecoder extends
+            AbstractExtendedResultDecoder<WhoAmIExtendedResult> {
+        @Override
+        public WhoAmIExtendedResult decodeExtendedResult(final ExtendedResult result,
+                final DecodeOptions options) throws DecodeException {
+            if (result instanceof WhoAmIExtendedResult) {
+                return (WhoAmIExtendedResult) result;
+            } else {
+                final WhoAmIExtendedResult newResult =
+                        Responses.newWhoAmIExtendedResult(result.getResultCode()).setMatchedDN(
+                                result.getMatchedDN()).setDiagnosticMessage(
+                                result.getDiagnosticMessage());
+
+                final ByteString responseValue = result.getValue();
+                if (responseValue != null) {
+                    try {
+                        newResult.setAuthorizationID(responseValue.toString());
+                    } catch (final LocalizedIllegalArgumentException e) {
+                        throw DecodeException.error(e.getMessageObject());
+                    }
+                }
+
+                for (final Control control : result.getControls()) {
+                    newResult.addControl(control);
+                }
+
+                return newResult;
+            }
+        }
+
+        @Override
+        public WhoAmIExtendedResult newExtendedErrorResult(final ResultCode resultCode,
+                final String matchedDN, final String diagnosticMessage) {
+            return Responses.newWhoAmIExtendedResult(resultCode).setMatchedDN(matchedDN)
+                    .setDiagnosticMessage(diagnosticMessage);
+        }
+    }
+
+    /** No need to expose this. */
+    private static final ExtendedResultDecoder<WhoAmIExtendedResult> RESULT_DECODER =
+            new ResultDecoder();
+
+    /** Prevent instantiation. */
+    WhoAmIExtendedRequestImpl() {
+        // Nothing to do.
+    }
+
+    WhoAmIExtendedRequestImpl(final WhoAmIExtendedRequest whoAmIExtendedRequest) {
+        super(whoAmIExtendedRequest);
+    }
+
+    @Override
+    public String getOID() {
+        return OID;
+    }
+
+    @Override
+    public ExtendedResultDecoder<WhoAmIExtendedResult> getResultDecoder() {
+        return RESULT_DECODER;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("WhoAmIExtendedRequest(requestName=");
+        builder.append(getOID());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/package-info.java
new file mode 100644
index 0000000..437be6f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for core LDAP requests.
+ */
+package org.forgerock.opendj.ldap.requests;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResult.java
new file mode 100644
index 0000000..9583f82
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResult.java
@@ -0,0 +1,95 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * An abstract Extended result which can be used as the basis for implementing
+ * new Extended operations.
+ *
+ * @param <S>
+ *            The type of Extended result.
+ */
+public abstract class AbstractExtendedResult<S extends ExtendedResult> extends
+        AbstractResultImpl<S> implements ExtendedResult {
+
+    /**
+     * Creates a new extended result that is an exact copy of the provided
+     * result.
+     *
+     * @param extendedResult
+     *            The extended result to be copied.
+     * @throws NullPointerException
+     *             If {@code extendedResult} was {@code null} .
+     */
+    protected AbstractExtendedResult(final ExtendedResult extendedResult) {
+        super(extendedResult);
+    }
+
+    /**
+     * Creates a new extended result using the provided result code.
+     *
+     * @param resultCode
+     *            The result code.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    protected AbstractExtendedResult(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public abstract String getOID();
+
+    @Override
+    public abstract ByteString getValue();
+
+    @Override
+    public abstract boolean hasValue();
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ExtendedResult(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", responseName=");
+        builder.append(getOID() == null ? "" : getOID());
+        if (hasValue()) {
+            builder.append(", responseValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    final S getThis() {
+        return (S) this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResultDecoder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResultDecoder.java
new file mode 100644
index 0000000..63152ff
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractExtendedResultDecoder.java
@@ -0,0 +1,93 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+
+/**
+ * This class provides a skeletal implementation of the
+ * {@code ExtendedResultDecoder} interface, to minimize the effort required to
+ * implement this interface.
+ *
+ * @param <S>
+ *            The type of result.
+ */
+public abstract class AbstractExtendedResultDecoder<S extends ExtendedResult> implements
+        ExtendedResultDecoder<S> {
+    /**
+     * Creates a new abstract extended result decoder.
+     */
+    protected AbstractExtendedResultDecoder() {
+        // Nothing to do.
+    }
+
+    @Override
+    public S adaptDecodeException(final DecodeException exception) {
+        final S adaptedResult =
+                newExtendedErrorResult(ResultCode.PROTOCOL_ERROR, "", exception.getMessage());
+        adaptedResult.setCause(exception.getCause());
+        return adaptedResult;
+    }
+
+    @Override
+    public <R extends ExtendedResult> LdapResultHandler<S> adaptExtendedResultHandler(
+            final ExtendedRequest<R> request, final LdapResultHandler<? super R> resultHandler,
+            final DecodeOptions options) {
+        return new LdapResultHandler<S>() {
+
+            @Override
+            public void handleException(final LdapException error) {
+                final Result result = error.getResult();
+                final R adaptedResult =
+                        request.getResultDecoder().newExtendedErrorResult(result.getResultCode(),
+                                result.getMatchedDN(), result.getDiagnosticMessage());
+                adaptedResult.setCause(result.getCause());
+                resultHandler.handleException(newLdapException(adaptedResult));
+            }
+
+            @Override
+            public void handleResult(final S result) {
+                try {
+                    final R adaptedResult =
+                            request.getResultDecoder().decodeExtendedResult(result, options);
+                    resultHandler.handleResult(adaptedResult);
+                } catch (final DecodeException e) {
+                    final R adaptedResult = request.getResultDecoder().adaptDecodeException(e);
+                    resultHandler.handleException(newLdapException(adaptedResult));
+                }
+            }
+
+        };
+    }
+
+    @Override
+    public abstract S decodeExtendedResult(ExtendedResult result, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    public abstract S newExtendedErrorResult(ResultCode resultCode, String matchedDN,
+            String diagnosticMessage);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractIntermediateResponse.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractIntermediateResponse.java
new file mode 100644
index 0000000..b6d3010
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractIntermediateResponse.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * An abstract Intermediate response which can be used as the basis for
+ * implementing new Intermediate responses.
+ *
+ * @param <S>
+ *            The type of Intermediate response.
+ */
+public abstract class AbstractIntermediateResponse<S extends IntermediateResponse> extends
+        AbstractResponseImpl<S> implements IntermediateResponse {
+
+    /**
+     * Creates a new intermediate response.
+     */
+    protected AbstractIntermediateResponse() {
+        // Nothing to do.
+    }
+
+    /**
+     * Creates a new intermediate response that is an exact copy of the provided
+     * response.
+     *
+     * @param intermediateResponse
+     *            The intermediate response to be copied.
+     * @throws NullPointerException
+     *             If {@code intermediateResponse} was {@code null} .
+     */
+    protected AbstractIntermediateResponse(final IntermediateResponse intermediateResponse) {
+        super(intermediateResponse);
+    }
+
+    @Override
+    public abstract String getOID();
+
+    @Override
+    public abstract ByteString getValue();
+
+    @Override
+    public abstract boolean hasValue();
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("IntermediateResponse(responseName=");
+        builder.append(getOID() == null ? "" : getOID());
+        if (hasValue()) {
+            builder.append(", responseValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    final S getThis() {
+        return (S) this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
new file mode 100644
index 0000000..ba4dd50
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
@@ -0,0 +1,93 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.util.Reject;
+
+/**
+ * Modifiable response implementation.
+ *
+ * @param <S>
+ *            The type of response.
+ */
+abstract class AbstractResponseImpl<S extends Response> implements Response {
+    /** Used by unmodifiable implementations as well. */
+    static Control getControl(final List<Control> controls, final String oid) {
+        // Avoid creating an iterator if possible.
+        if (!controls.isEmpty()) {
+            for (final Control control : controls) {
+                if (control.getOID().equals(oid)) {
+                    return control;
+                }
+            }
+        }
+        return null;
+    }
+
+    private final List<Control> controls = new LinkedList<>();
+
+    AbstractResponseImpl() {
+        // No implementation required.
+    }
+
+    AbstractResponseImpl(final Response response) {
+        Reject.ifNull(response);
+        for (final Control control : response.getControls()) {
+            // Create defensive copy.
+            controls.add(GenericControl.newControl(control));
+        }
+    }
+
+    @Override
+    public final S addControl(final Control control) {
+        Reject.ifNull(control);
+        controls.add(control);
+        return getThis();
+    }
+
+    @Override
+    public boolean containsControl(final String oid) {
+        return getControl(controls, oid) != null;
+    }
+
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Reject.ifNull(decoder, options);
+        final Control control = getControl(controls, decoder.getOID());
+        return control != null ? decoder.decodeControl(control, options) : null;
+    }
+
+    @Override
+    public final List<Control> getControls() {
+        return controls;
+    }
+
+    @Override
+    public abstract String toString();
+
+    abstract S getThis();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResultImpl.java
new file mode 100644
index 0000000..ddcd8cf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResultImpl.java
@@ -0,0 +1,126 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ResultCode;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Modifiable result implementation.
+ *
+ * @param <S>
+ *            The type of result.
+ */
+abstract class AbstractResultImpl<S extends Result> extends AbstractResponseImpl<S> implements
+        Result {
+    /** For local errors caused by internal exceptions. */
+    private Throwable cause;
+    private String diagnosticMessage = "";
+    private String matchedDN = "";
+    private final List<String> referralURIs = new LinkedList<>();
+    private ResultCode resultCode;
+
+    AbstractResultImpl(final Result result) {
+        super(result);
+        this.cause = result.getCause();
+        this.diagnosticMessage = result.getDiagnosticMessage();
+        this.matchedDN = result.getMatchedDN();
+        this.referralURIs.addAll(result.getReferralURIs());
+        this.resultCode = result.getResultCode();
+    }
+
+    AbstractResultImpl(final ResultCode resultCode) {
+        this.resultCode = resultCode;
+    }
+
+    @Override
+    public final S addReferralURI(final String uri) {
+        Reject.ifNull(uri);
+
+        referralURIs.add(uri);
+        return getThis();
+    }
+
+    @Override
+    public final Throwable getCause() {
+        return cause;
+    }
+
+    @Override
+    public final String getDiagnosticMessage() {
+        return diagnosticMessage;
+    }
+
+    @Override
+    public final String getMatchedDN() {
+        return matchedDN;
+    }
+
+    @Override
+    public final List<String> getReferralURIs() {
+        return referralURIs;
+    }
+
+    @Override
+    public final ResultCode getResultCode() {
+        return resultCode;
+    }
+
+    @Override
+    public final boolean isReferral() {
+        final ResultCode code = getResultCode();
+        return code.equals(ResultCode.REFERRAL);
+    }
+
+    @Override
+    public final boolean isSuccess() {
+        final ResultCode code = getResultCode();
+        return !code.isExceptional();
+    }
+
+    @Override
+    public final S setCause(final Throwable cause) {
+        this.cause = cause;
+        return getThis();
+    }
+
+    @Override
+    public final S setDiagnosticMessage(final String message) {
+        this.diagnosticMessage = message != null ? message : "";
+        return getThis();
+    }
+
+    @Override
+    public final S setMatchedDN(final String dn) {
+        this.matchedDN = dn != null ? dn : "";
+        return getThis();
+    }
+
+    @Override
+    public final S setResultCode(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+
+        this.resultCode = resultCode;
+        return getThis();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableExtendedResultImpl.java
new file mode 100644
index 0000000..ed6521a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableExtendedResultImpl.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * An abstract unmodifiable Extended result which can be used as the basis for
+ * implementing new unmodifiable Extended operations.
+ *
+ * @param <S>
+ *            The type of Extended result.
+ */
+abstract class AbstractUnmodifiableExtendedResultImpl<S extends ExtendedResult> extends
+        AbstractUnmodifiableResultImpl<S> implements ExtendedResult {
+    AbstractUnmodifiableExtendedResultImpl(final S impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getOID() {
+        return impl.getOID();
+    }
+
+    @Override
+    public ByteString getValue() {
+        return impl.getValue();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return impl.hasValue();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableIntermediateResponseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableIntermediateResponseImpl.java
new file mode 100644
index 0000000..adc9520
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableIntermediateResponseImpl.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * An abstract unmodifiable Intermediate response which can be used as the basis
+ * for implementing new unmodifiable Intermediate responses.
+ *
+ * @param <S>
+ *            The type of Intermediate response.
+ */
+abstract class AbstractUnmodifiableIntermediateResponseImpl<S extends IntermediateResponse> extends
+        AbstractUnmodifiableResponseImpl<S> implements IntermediateResponse {
+    AbstractUnmodifiableIntermediateResponseImpl(final S impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getOID() {
+        return impl.getOID();
+    }
+
+    @Override
+    public ByteString getValue() {
+        return impl.getValue();
+    }
+
+    @Override
+    public boolean hasValue() {
+        return impl.hasValue();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
new file mode 100644
index 0000000..e97fe0a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Functions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Collections2;
+
+/**
+ * Unmodifiable response implementation.
+ *
+ * @param <S>
+ *            The type of response.
+ */
+abstract class AbstractUnmodifiableResponseImpl<S extends Response> implements Response {
+
+    protected final S impl;
+
+    AbstractUnmodifiableResponseImpl(final S impl) {
+        Reject.ifNull(impl);
+        this.impl = impl;
+    }
+
+    @Override
+    public final S addControl(final Control control) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean containsControl(final String oid) {
+        return impl.containsControl(oid);
+    }
+
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Reject.ifNull(decoder, options);
+
+        final List<Control> controls = impl.getControls();
+        final Control control = AbstractResponseImpl.getControl(controls, decoder.getOID());
+        if (control != null) {
+            // Got a match. Return a defensive copy only if necessary.
+            final C decodedControl = decoder.decodeControl(control, options);
+            if (decodedControl != control) {
+                /*
+                 * This was not the original control so return it immediately.
+                 */
+                return decodedControl;
+            } else if (decodedControl instanceof GenericControl) {
+                // Generic controls are immutable, so return it immediately.
+                return decodedControl;
+            } else {
+                // Re-decode to get defensive copy.
+                final GenericControl genericControl = GenericControl.newControl(control);
+                return decoder.decodeControl(genericControl, options);
+            }
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public final List<Control> getControls() {
+        /*
+         * We need to make all controls unmodifiable as well, which implies
+         * making defensive copies where necessary.
+         */
+        final Function<Control, Control, NeverThrowsException> function =
+                new Function<Control, Control, NeverThrowsException>() {
+                    @Override
+                    public Control apply(final Control value) {
+                        // Return defensive copy.
+                        return GenericControl.newControl(value);
+                    }
+                };
+
+        final List<Control> unmodifiableControls =
+                Collections2.transformedList(impl.getControls(), function, Functions
+                        .<Control> identityFunction());
+        return Collections.unmodifiableList(unmodifiableControls);
+    }
+
+    @Override
+    public final String toString() {
+        return impl.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResultImpl.java
new file mode 100644
index 0000000..c9936c6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResultImpl.java
@@ -0,0 +1,98 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * Unmodifiable result implementation.
+ *
+ * @param <S>
+ *            The type of result.
+ */
+abstract class AbstractUnmodifiableResultImpl<S extends Result> extends
+        AbstractUnmodifiableResponseImpl<S> implements Result {
+
+    AbstractUnmodifiableResultImpl(final S impl) {
+        super(impl);
+    }
+
+    @Override
+    public final S addReferralURI(final String uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final Throwable getCause() {
+        return impl.getCause();
+    }
+
+    @Override
+    public final String getDiagnosticMessage() {
+        return impl.getDiagnosticMessage();
+    }
+
+    @Override
+    public final String getMatchedDN() {
+        return impl.getMatchedDN();
+    }
+
+    @Override
+    public final List<String> getReferralURIs() {
+        return Collections.unmodifiableList(impl.getReferralURIs());
+    }
+
+    @Override
+    public final ResultCode getResultCode() {
+        return impl.getResultCode();
+    }
+
+    @Override
+    public final boolean isReferral() {
+        return impl.isReferral();
+    }
+
+    @Override
+    public final boolean isSuccess() {
+        return impl.isSuccess();
+    }
+
+    @Override
+    public final S setCause(final Throwable cause) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final S setDiagnosticMessage(final String message) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final S setMatchedDN(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final S setResultCode(final ResultCode resultCode) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResult.java
new file mode 100644
index 0000000..ded8f50
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResult.java
@@ -0,0 +1,132 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Bind result indicates the status of the client's request for
+ * authentication.
+ * <p>
+ * A successful Bind operation is indicated by a Bind result with a result code
+ * set to {@link ResultCode#SUCCESS} and can be determined by invoking the
+ * {@link #isSuccess} method.
+ * <p>
+ * The server SASL credentials field is used as part of a SASL-defined bind
+ * mechanism to allow the client to authenticate the server to which it is
+ * communicating, or to perform "challenge-response" authentication. If the
+ * client bound using a form of simple authentication, or the SASL mechanism
+ * does not require the server to return information to the client, then this
+ * field shall not be included in the Bind result.
+ * <p>
+ * If the server requires the client to send a new SASL Bind request in order to
+ * continue the authentication process then the result code is set to
+ * {@link ResultCode#SASL_BIND_IN_PROGRESS} and can be determined by invoking
+ * the {@link #isSASLBindInProgress} method.
+ */
+public interface BindResult extends Result {
+
+    @Override
+    BindResult addControl(Control control);
+
+    @Override
+    BindResult addReferralURI(String uri);
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    @Override
+    String getMatchedDN();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    /**
+     * Returns the server SASL credentials associated with this bind result.
+     *
+     * @return The server SASL credentials, or {@code null} indicating that none
+     *         was provided.
+     */
+    ByteString getServerSASLCredentials();
+
+    @Override
+    boolean isReferral();
+
+    /**
+     * Indicates whether or not the server requires the client to send a new
+     * SASL Bind request with the same SASL mechanism in order to continue the
+     * authentication process. This typically occurs during multi-stage
+     * (challenge response) authentication.
+     * <p>
+     * Specifically, this method returns {@code true} if the result code is
+     * equal to {@link ResultCode#SASL_BIND_IN_PROGRESS}.
+     *
+     * @return {@code true} if the server requires the client to send a new SASL
+     *         Bind request, otherwise {@code false}.
+     */
+    boolean isSASLBindInProgress();
+
+    @Override
+    boolean isSuccess();
+
+    @Override
+    BindResult setCause(Throwable cause);
+
+    @Override
+    BindResult setDiagnosticMessage(String message);
+
+    @Override
+    BindResult setMatchedDN(String dn);
+
+    @Override
+    BindResult setResultCode(ResultCode resultCode);
+
+    /**
+     * Sets the server SASL credentials associated with this bind result.
+     *
+     * @param credentials
+     *            The server SASL credentials associated with this bind result,
+     *            which may be {@code null} indicating that none was provided.
+     * @return This bind result.
+     * @throws UnsupportedOperationException
+     *             If this bind result does not permit the server SASL
+     *             credentials to be set.
+     */
+    BindResult setServerSASLCredentials(ByteString credentials);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResultImpl.java
new file mode 100644
index 0000000..cc9ea6c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/BindResultImpl.java
@@ -0,0 +1,80 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * Bind result implementation.
+ */
+final class BindResultImpl extends AbstractResultImpl<BindResult> implements BindResult {
+    private ByteString credentials;
+
+    BindResultImpl(final BindResult bindResult) {
+        super(bindResult);
+        this.credentials = bindResult.getServerSASLCredentials();
+    }
+
+    BindResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public ByteString getServerSASLCredentials() {
+        return credentials;
+    }
+
+    @Override
+    public boolean isSASLBindInProgress() {
+        final ResultCode code = getResultCode();
+        return code.equals(ResultCode.SASL_BIND_IN_PROGRESS);
+    }
+
+    @Override
+    public BindResult setServerSASLCredentials(final ByteString credentials) {
+        this.credentials = credentials;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("BindResult(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", serverSASLCreds=");
+        builder.append(getServerSASLCredentials() == null ? ByteString.empty()
+                : getServerSASLCredentials());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    BindResult getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResult.java
new file mode 100644
index 0000000..2dabbac
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResult.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * An Compare result indicates the final status of an Compare operation.
+ * <p>
+ * If the attribute value assertion in the Compare request matched a value of
+ * the attribute or sub-type according to the attribute's equality matching rule
+ * then the result code is set to {@link ResultCode#COMPARE_TRUE} and can be
+ * determined by invoking the {@link #matched} method.
+ * <p>
+ * The following excerpt shows how to use the Compare operation to check whether
+ * a member belongs to a (possibly large) static group.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String groupDN = ...;
+ * String memberDN = ...;
+ *
+ * CompareRequest request =
+ *         Requests.newCompareRequest(groupDN, "member", memberDN);
+ * CompareResult result = connection.compare(request);
+ * if (result.matched()) {
+ *     // The member belongs to the group.
+ * }
+ * </pre>
+ */
+public interface CompareResult extends Result {
+
+    @Override
+    CompareResult addControl(Control control);
+
+    @Override
+    CompareResult addReferralURI(String uri);
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    @Override
+    String getMatchedDN();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    @Override
+    boolean isReferral();
+
+    @Override
+    boolean isSuccess();
+
+    /**
+     * Indicates whether or not the attribute value assertion in the Compare
+     * request matched a value of the attribute or sub-type according to the
+     * attribute's equality matching rule.
+     * <p>
+     * Specifically, this method returns {@code true} if the result code is
+     * equal to {@link ResultCode#COMPARE_TRUE}.
+     *
+     * @return {@code true} if the attribute value assertion matched, otherwise
+     *         {@code false}.
+     */
+    boolean matched();
+
+    @Override
+    CompareResult setCause(Throwable cause);
+
+    @Override
+    CompareResult setDiagnosticMessage(String message);
+
+    @Override
+    CompareResult setMatchedDN(String dn);
+
+    @Override
+    CompareResult setResultCode(ResultCode resultCode);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResultImpl.java
new file mode 100644
index 0000000..d5e795a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/CompareResultImpl.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * Compare result implementation.
+ */
+final class CompareResultImpl extends AbstractResultImpl<CompareResult> implements CompareResult {
+
+    CompareResultImpl(final CompareResult compareResult) {
+        super(compareResult);
+    }
+
+    CompareResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public boolean matched() {
+        final ResultCode code = getResultCode();
+        return code.equals(ResultCode.COMPARE_TRUE);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("CompareResult(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    CompareResult getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResult.java
new file mode 100644
index 0000000..b005803
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResult.java
@@ -0,0 +1,110 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Extended result indicates the status of an Extended operation and any
+ * additional information associated with the Extended operation, including the
+ * optional response name and value. These can be retrieved using the
+ * {@link #getOID} and {@link #getValue} methods respectively.
+ */
+public interface ExtendedResult extends Result {
+
+    @Override
+    ExtendedResult addControl(Control control);
+
+    @Override
+    ExtendedResult addReferralURI(String uri);
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    @Override
+    String getMatchedDN();
+
+    /**
+     * Returns the numeric OID, if any, associated with this extended result.
+     *
+     * @return The numeric OID associated with this extended result, or
+     *         {@code null} if there is no OID.
+     */
+    String getOID();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    /**
+     * Returns the value, if any, associated with this extended result. Its
+     * format is defined by the specification of this extended result.
+     *
+     * @return The value associated with this extended result, or {@code null}
+     *         if there is no value.
+     */
+    ByteString getValue();
+
+    /**
+     * Returns {@code true} if this extended result has a value. In some
+     * circumstances it may be useful to determine if a extended result has a
+     * value, without actually calculating the value and incurring any
+     * performance costs.
+     *
+     * @return {@code true} if this extended result has a value, or
+     *         {@code false} if there is no value.
+     */
+    boolean hasValue();
+
+    @Override
+    boolean isReferral();
+
+    @Override
+    boolean isSuccess();
+
+    @Override
+    ExtendedResult setCause(Throwable cause);
+
+    @Override
+    ExtendedResult setDiagnosticMessage(String message);
+
+    @Override
+    ExtendedResult setMatchedDN(String dn);
+
+    @Override
+    ExtendedResult setResultCode(ResultCode resultCode);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResultDecoder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResultDecoder.java
new file mode 100644
index 0000000..a7a1411
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ExtendedResultDecoder.java
@@ -0,0 +1,111 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+
+/**
+ * A factory interface for decoding a generic extended result as an extended
+ * result of specific type.
+ *
+ * @param <S>
+ *            The type of result.
+ */
+public interface ExtendedResultDecoder<S extends ExtendedResult> {
+
+    /**
+     * Creates a new extended operation error result using the provided decoding
+     * exception. This method should be used to adapt {@code DecodeException}
+     * encountered while decoding an extended request or result. The returned
+     * error result will have the result code {@link ResultCode#PROTOCOL_ERROR}.
+     *
+     * @param exception
+     *            The decoding exception to be adapted.
+     * @return An extended operation error result representing the decoding
+     *         exception.
+     * @throws NullPointerException
+     *             If {@code exception} was {@code null}.
+     */
+    S adaptDecodeException(DecodeException exception);
+
+    /**
+     * Adapts the provided extended result handler into a result handler which
+     * is compatible with this extended result decoder. Extended results handled
+     * by the returned handler will be automatically converted and passed to the
+     * provided result handler. Decoding errors encountered while decoding the
+     * extended result will be converted into protocol errors.
+     *
+     * @param <R>
+     *            The type of result handler to be adapted.
+     * @param request
+     *            The extended request whose result handler is to be adapted.
+     * @param resultHandler
+     *            The extended result handler which is to be adapted.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the extended operation result.
+     * @return A result handler which is compatible with this extended result
+     *         decoder.
+     */
+    <R extends ExtendedResult> LdapResultHandler<S> adaptExtendedResultHandler(
+            ExtendedRequest<R> request, LdapResultHandler<? super R> resultHandler, DecodeOptions options);
+
+    /**
+     * Decodes the provided extended operation result as a {@code Result} of
+     * type {@code S}. This method is called when an extended result is received
+     * from the server. The result may indicate success or failure of the
+     * extended request.
+     *
+     * @param result
+     *            The extended operation result to be decoded.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the extended operation result.
+     * @return The decoded extended operation result.
+     * @throws DecodeException
+     *             If the provided extended operation result could not be
+     *             decoded. For example, if the request name was wrong, or if
+     *             the request value was invalid.
+     */
+    S decodeExtendedResult(ExtendedResult result, DecodeOptions options) throws DecodeException;
+
+    /**
+     * Creates a new extended error result using the provided result code,
+     * matched DN, and diagnostic message. This method is called when a generic
+     * failure occurs, such as a connection failure, and the error result needs
+     * to be converted to a {@code Result} of type {@code S}.
+     *
+     * @param resultCode
+     *            The result code.
+     * @param matchedDN
+     *            The matched DN, which may be empty if none was provided.
+     * @param diagnosticMessage
+     *            The diagnostic message, which may be empty if none was
+     *            provided.
+     * @return The decoded extended operation error result.
+     * @throws NullPointerException
+     *             If {@code resultCode}, {@code matchedDN}, or
+     *             {@code diagnosticMessage} were {@code null}.
+     */
+    S newExtendedErrorResult(ResultCode resultCode, String matchedDN, String diagnosticMessage);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResult.java
new file mode 100644
index 0000000..e2ae9be
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResult.java
@@ -0,0 +1,120 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Generic Extended result indicates the final status of an Generic Extended
+ * operation.
+ */
+public interface GenericExtendedResult extends ExtendedResult {
+
+    @Override
+    GenericExtendedResult addControl(Control control);
+
+    @Override
+    GenericExtendedResult addReferralURI(String uri);
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    @Override
+    String getMatchedDN();
+
+    @Override
+    String getOID();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    @Override
+    boolean isReferral();
+
+    @Override
+    boolean isSuccess();
+
+    @Override
+    GenericExtendedResult setCause(Throwable cause);
+
+    @Override
+    GenericExtendedResult setDiagnosticMessage(String message);
+
+    @Override
+    GenericExtendedResult setMatchedDN(String dn);
+
+    /**
+     * Sets the numeric OID, if any, associated with this extended result.
+     *
+     * @param oid
+     *            The numeric OID associated with this extended result, or
+     *            {@code null} if there is no value.
+     * @return This generic extended result.
+     * @throws UnsupportedOperationException
+     *             If this generic extended result does not permit the result
+     *             name to be set.
+     */
+    GenericExtendedResult setOID(String oid);
+
+    @Override
+    GenericExtendedResult setResultCode(ResultCode resultCode);
+
+    /**
+     * Sets the value, if any, associated with this extended result. Its format
+     * is defined by the specification of this extended result.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            The value associated with this extended result, or
+     *            {@code null} if there is no value.
+     * @return This generic extended result.
+     * @throws UnsupportedOperationException
+     *             If this generic extended result does not permit the result
+     *             value to be set.
+     */
+    GenericExtendedResult setValue(Object value);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResultImpl.java
new file mode 100644
index 0000000..dc1402c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericExtendedResultImpl.java
@@ -0,0 +1,91 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * Generic extended result implementation.
+ */
+final class GenericExtendedResultImpl extends AbstractExtendedResult<GenericExtendedResult>
+        implements ExtendedResult, GenericExtendedResult {
+
+    private String responseName;
+    private ByteString responseValue;
+
+    GenericExtendedResultImpl(final GenericExtendedResult genericExtendedResult) {
+        super(genericExtendedResult);
+        this.responseName = genericExtendedResult.getOID();
+        this.responseValue = genericExtendedResult.getValue();
+    }
+
+    GenericExtendedResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public String getOID() {
+        return responseName;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return responseValue;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return responseValue != null;
+    }
+
+    @Override
+    public GenericExtendedResult setOID(final String oid) {
+        this.responseName = oid;
+        return this;
+    }
+
+    @Override
+    public GenericExtendedResult setValue(final Object value) {
+        this.responseValue = value != null ? ByteString.valueOfObject(value) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GenericExtendedResult(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", responseName=");
+        builder.append(getOID() == null ? "" : getOID());
+        if (hasValue()) {
+            builder.append(", responseValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponse.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponse.java
new file mode 100644
index 0000000..8948aa4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponse.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Generic Intermediate response provides a mechanism for communicating
+ * unrecognized or unsupported Intermediate responses to the client.
+ */
+public interface GenericIntermediateResponse extends IntermediateResponse {
+
+    @Override
+    GenericIntermediateResponse addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getOID();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    /**
+     * Sets the numeric OID, if any, associated with this intermediate response.
+     *
+     * @param oid
+     *            The numeric OID associated with this intermediate response, or
+     *            {@code null} if there is no value.
+     * @return This generic intermediate response.
+     * @throws UnsupportedOperationException
+     *             If this intermediate response does not permit the response
+     *             name to be set.
+     */
+    GenericIntermediateResponse setOID(String oid);
+
+    /**
+     * Sets the value, if any, associated with this intermediate response. Its
+     * format is defined by the specification of this intermediate response.
+     * <p>
+     * If {@code value} is not an instance of {@code ByteString} then it will be
+     * converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param value
+     *            The value associated with this intermediate response, or
+     *            {@code null} if there is no value.
+     * @return This generic intermediate response.
+     * @throws UnsupportedOperationException
+     *             If this intermediate response does not permit the response
+     *             value to be set.
+     */
+    GenericIntermediateResponse setValue(Object value);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponseImpl.java
new file mode 100644
index 0000000..a68769d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/GenericIntermediateResponseImpl.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * Generic intermediate response implementation.
+ */
+final class GenericIntermediateResponseImpl extends
+        AbstractIntermediateResponse<GenericIntermediateResponse> implements
+        GenericIntermediateResponse {
+
+    private String responseName;
+    private ByteString responseValue;
+
+    GenericIntermediateResponseImpl() {
+        // Nothing to do.
+    }
+
+    GenericIntermediateResponseImpl(final GenericIntermediateResponse genericIntermediateResponse) {
+        super(genericIntermediateResponse);
+        this.responseName = genericIntermediateResponse.getOID();
+        this.responseValue = genericIntermediateResponse.getValue();
+    }
+
+    @Override
+    public String getOID() {
+        return responseName;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return responseValue;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return responseValue != null;
+    }
+
+    @Override
+    public GenericIntermediateResponse setOID(final String oid) {
+        this.responseName = oid;
+        return this;
+    }
+
+    @Override
+    public GenericIntermediateResponse setValue(final Object value) {
+        this.responseValue = value != null ? ByteString.valueOfObject(value) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("GenericIntermediateResponse(responseName=");
+        builder.append(getOID() == null ? "" : getOID());
+        if (hasValue()) {
+            builder.append(", requestValue=");
+            builder.append(getValue().toHexPlusAsciiString(4));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/IntermediateResponse.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/IntermediateResponse.java
new file mode 100644
index 0000000..2d5aa79
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/IntermediateResponse.java
@@ -0,0 +1,84 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * An Intermediate response provides a general mechanism for defining
+ * single-request/multiple-response operations. This response is intended to be
+ * used in conjunction with the Extended operation to define new
+ * single-request/multiple-response operations or in conjunction with a control
+ * when extending existing operations in a way that requires them to return
+ * Intermediate response information.
+ * <p>
+ * An Intermediate response may convey an optional response name and value.
+ * These can be retrieved using the {@link #getOID} and {@link #getValue}
+ * methods respectively.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc3771">RFC 3771 - The Lightweight
+ *      Directory Access Protocol (LDAP) Intermediate Response Message</a>
+ */
+public interface IntermediateResponse extends Response {
+
+    @Override
+    IntermediateResponse addControl(Control control);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the numeric OID, if any, associated with this intermediate
+     * response.
+     *
+     * @return The numeric OID associated with this intermediate response, or
+     *         {@code null} if there is no OID.
+     */
+    String getOID();
+
+    /**
+     * Returns the value, if any, associated with this intermediate response.
+     * Its format is defined by the specification of this intermediate response.
+     *
+     * @return The value associated with this intermediate response, or
+     *         {@code null} if there is no value.
+     */
+    ByteString getValue();
+
+    /**
+     * Returns {@code true} if this intermediate response has a value. In some
+     * circumstances it may be useful to determine if an intermediate response
+     * has a value, without actually calculating the value and incurring any
+     * performance costs.
+     *
+     * @return {@code true} if this intermediate response has a value, or
+     *         {@code false} if there is no value.
+     */
+    boolean hasValue();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResult.java
new file mode 100644
index 0000000..f1116f3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResult.java
@@ -0,0 +1,131 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The password modify extended result as defined in RFC 3062. The result
+ * includes the generated password, if requested, but only if the modify request
+ * succeeded.
+ *
+ * @see org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest
+ * @see <a href="http://tools.ietf.org/html/rfc3062">RFC 3062 - LDAP Password
+ *      Modify Extended Operation </a>
+ */
+public interface PasswordModifyExtendedResult extends ExtendedResult {
+
+    @Override
+    PasswordModifyExtendedResult addControl(Control control);
+
+    @Override
+    PasswordModifyExtendedResult addReferralURI(String uri);
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    /**
+     * Returns the newly generated password, but only if the password modify
+     * request succeeded and a generated password was requested.
+     *
+     * @return The newly generated password, or {@code null} if the password
+     *         modify request failed or a generated password was not requested.
+     */
+    byte[] getGeneratedPassword();
+
+    @Override
+    String getMatchedDN();
+
+    @Override
+    String getOID();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    @Override
+    boolean isReferral();
+
+    @Override
+    boolean isSuccess();
+
+    @Override
+    PasswordModifyExtendedResult setCause(Throwable cause);
+
+    @Override
+    PasswordModifyExtendedResult setDiagnosticMessage(String message);
+
+    /**
+     * Sets the generated password.
+     *
+     * @param password
+     *            The generated password, or {@code null} if there is no
+     *            generated password associated with this result.
+     * @return This password modify result.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended result does not permit the
+     *             generated password to be set.
+     */
+    PasswordModifyExtendedResult setGeneratedPassword(byte[] password);
+
+    /**
+     * Sets the generated password. The password will be converted to a UTF-8
+     * octet string.
+     *
+     * @param password
+     *            The generated password, or {@code null} if there is no
+     *            generated password associated with this result.
+     * @return This password modify result.
+     * @throws UnsupportedOperationException
+     *             If this password modify extended result does not permit the
+     *             generated password to be set.
+     */
+    PasswordModifyExtendedResult setGeneratedPassword(char[] password);
+
+    @Override
+    PasswordModifyExtendedResult setMatchedDN(String dn);
+
+    @Override
+    PasswordModifyExtendedResult setResultCode(ResultCode resultCode);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResultImpl.java
new file mode 100644
index 0000000..930b77e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/PasswordModifyExtendedResultImpl.java
@@ -0,0 +1,122 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ResultCode;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * Password modify extended result implementation.
+ */
+final class PasswordModifyExtendedResultImpl extends
+        AbstractExtendedResult<PasswordModifyExtendedResult> implements
+        PasswordModifyExtendedResult {
+    /**
+     * The ASN.1 element type that will be used to encode the genPasswd
+     * component in a password modify extended response.
+     */
+    private static final byte TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD = (byte) 0x80;
+
+    private byte[] password;
+
+    PasswordModifyExtendedResultImpl(final PasswordModifyExtendedResult passwordModifyExtendedResult) {
+        super(passwordModifyExtendedResult);
+        this.password = passwordModifyExtendedResult.getGeneratedPassword();
+    }
+
+    /** Instantiation via factory. */
+    PasswordModifyExtendedResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public byte[] getGeneratedPassword() {
+        return password;
+    }
+
+    @Override
+    public String getOID() {
+        // No response name defined.
+        return null;
+    }
+
+    @Override
+    public ByteString getValue() {
+        if (password != null) {
+            final ByteStringBuilder buffer = new ByteStringBuilder();
+            final ASN1Writer writer = ASN1.getWriter(buffer);
+
+            try {
+                writer.writeStartSequence();
+                writer.writeOctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD, password);
+                writer.writeEndSequence();
+            } catch (final IOException ioe) {
+                // This should never happen unless there is a bug somewhere.
+                throw new RuntimeException(ioe);
+            }
+
+            return buffer.toByteString();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return password != null;
+    }
+
+    @Override
+    public PasswordModifyExtendedResult setGeneratedPassword(final byte[] password) {
+        this.password = password;
+        return this;
+    }
+
+    @Override
+    public PasswordModifyExtendedResult setGeneratedPassword(final char[] password) {
+        this.password = (password != null) ? StaticUtils.getBytes(password) : null;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("PasswordModifyExtendedResponse(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        if (password != null) {
+            builder.append(", genPassword=");
+            builder.append(ByteString.valueOfBytes(password));
+        }
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Response.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
new file mode 100644
index 0000000..1737046
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The base class of all Responses provides methods for querying and
+ * manipulating the set of Controls included with a Response.
+ */
+public interface Response {
+
+    /**
+     * Adds the provided control to this response.
+     *
+     * @param control
+     *            The control to be added.
+     * @return This response.
+     * @throws UnsupportedOperationException
+     *             If this response does not permit controls to be added.
+     * @throws NullPointerException
+     *             If {@code control} was {@code null}.
+     */
+    Response addControl(Control control);
+
+    /**
+     * Returns {@code true} if this response contains the specified response
+     * control.
+     *
+     * @param oid
+     *            The numeric OID of the response control.
+     * @return {@code true} if this response contains the specified response
+     *         control.
+     */
+    boolean containsControl(String oid);
+
+    /**
+     * Decodes and returns the first control in this response having an OID
+     * corresponding to the provided control decoder.
+     *
+     * @param <C>
+     *            The type of control to be decoded and returned.
+     * @param decoder
+     *            The control decoder.
+     * @param options
+     *            The set of decode options which should be used when decoding
+     *            the control.
+     * @return The decoded control, or {@code null} if the control is not
+     *         included with this response.
+     * @throws DecodeException
+     *             If the control could not be decoded because it was malformed
+     *             in some way (e.g. the control value was missing, or its
+     *             content could not be decoded).
+     * @throws NullPointerException
+     *             If {@code decoder} or {@code options} was {@code null}.
+     */
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    /**
+     * Returns a {@code List} containing the controls included with this
+     * response. The returned {@code List} may be modified if permitted by this
+     * response.
+     *
+     * @return A {@code List} containing the controls.
+     */
+    List<Control> getControls();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Responses.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Responses.java
new file mode 100644
index 0000000..9659828
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Responses.java
@@ -0,0 +1,537 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ResultCode;
+
+import org.forgerock.util.Reject;
+
+/**
+ * This class contains various methods for creating and manipulating responses.
+ * <p>
+ * All copy constructors of the form {@code copyOfXXXResult} perform deep copies
+ * of their response parameter. More specifically, any controls, modifications,
+ * and attributes contained within the response will be duplicated.
+ * <p>
+ * Similarly, all unmodifiable views of responses returned by methods of the
+ * form {@code unmodifiableXXXResult} return deep unmodifiable views of their
+ * response parameter. More specifically, any controls, modifications, and
+ * attributes contained within the returned response will be unmodifiable.
+ */
+public final class Responses {
+
+    // TODO: search reference from LDAP URL.
+
+    // TODO: referral from LDAP URL.
+
+    // TODO: synchronized requests?
+
+    /**
+     * Creates a new bind result that is an exact copy of the provided result.
+     *
+     * @param result
+     *            The bind result to be copied.
+     * @return The new bind result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static BindResult copyOfBindResult(final BindResult result) {
+        return new BindResultImpl(result);
+    }
+
+    /**
+     * Creates a new compare result that is an exact copy of the provided
+     * result.
+     *
+     * @param result
+     *            The compare result to be copied.
+     * @return The new compare result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static CompareResult copyOfCompareResult(final CompareResult result) {
+        return new CompareResultImpl(result);
+    }
+
+    /**
+     * Creates a new generic extended result that is an exact copy of the
+     * provided result.
+     *
+     * @param result
+     *            The generic extended result to be copied.
+     * @return The new generic extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static GenericExtendedResult copyOfGenericExtendedResult(
+            final GenericExtendedResult result) {
+        return new GenericExtendedResultImpl(result);
+    }
+
+    /**
+     * Creates a new generic intermediate response that is an exact copy of the
+     * provided response.
+     *
+     * @param result
+     *            The generic intermediate response to be copied.
+     * @return The new generic intermediate response.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static GenericIntermediateResponse copyOfGenericIntermediateResponse(
+            final GenericIntermediateResponse result) {
+        return new GenericIntermediateResponseImpl(result);
+    }
+
+    /**
+     * Creates a new password modify extended result that is an exact copy of
+     * the provided result.
+     *
+     * @param result
+     *            The password modify extended result to be copied.
+     * @return The new password modify extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static PasswordModifyExtendedResult copyOfPasswordModifyExtendedResult(
+            final PasswordModifyExtendedResult result) {
+        return new PasswordModifyExtendedResultImpl(result);
+    }
+
+    /**
+     * Creates a new result that is an exact copy of the provided result.
+     *
+     * @param result
+     *            The result to be copied.
+     * @return The new result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static Result copyOfResult(final Result result) {
+        return new ResultImpl(result);
+    }
+
+    /**
+     * Creates a new search result entry that is an exact copy of the provided
+     * result.
+     *
+     * @param entry
+     *            The search result entry to be copied.
+     * @return The new search result entry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public static SearchResultEntry copyOfSearchResultEntry(final SearchResultEntry entry) {
+        return new SearchResultEntryImpl(entry);
+    }
+
+    /**
+     * Creates a new search result reference that is an exact copy of the
+     * provided result.
+     *
+     * @param reference
+     *            The search result reference to be copied.
+     * @return The new search result reference.
+     * @throws NullPointerException
+     *             If {@code reference} was {@code null}.
+     */
+    public static SearchResultReference copyOfSearchResultReference(
+            final SearchResultReference reference) {
+        return new SearchResultReferenceImpl(reference);
+    }
+
+    /**
+     * Creates a new who am I extended result that is an exact copy of the
+     * provided result.
+     *
+     * @param result
+     *            The who am I result to be copied.
+     * @return The new who am I extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null} .
+     */
+    public static WhoAmIExtendedResult copyOfWhoAmIExtendedResult(final WhoAmIExtendedResult result) {
+        return new WhoAmIExtendedResultImpl(result);
+    }
+
+    /**
+     * Creates a new bind result using the provided result code.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new bind result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static BindResult newBindResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new BindResultImpl(resultCode);
+    }
+
+    /**
+     * Creates a new compare result using the provided result code.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new compare result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static CompareResult newCompareResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new CompareResultImpl(resultCode);
+    }
+
+    /**
+     * Creates a new generic extended result using the provided result code.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new generic extended result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static GenericExtendedResult newGenericExtendedResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new GenericExtendedResultImpl(resultCode);
+    }
+
+    /**
+     * Creates a new generic intermediate response with no name or value.
+     *
+     * @return The new generic intermediate response.
+     */
+    public static GenericIntermediateResponse newGenericIntermediateResponse() {
+        return new GenericIntermediateResponseImpl();
+    }
+
+    /**
+     * Creates a new generic intermediate response using the provided response
+     * name and value.
+     * <p>
+     * If the response value is not an instance of {@code ByteString} then it
+     * will be converted using the {@link ByteString#valueOfObject(Object)} method.
+     *
+     * @param responseName
+     *            The dotted-decimal representation of the unique OID
+     *            corresponding to this intermediate response, which may be
+     *            {@code null} indicating that none was provided.
+     * @param responseValue
+     *            The response value associated with this generic intermediate
+     *            response, which may be {@code null} indicating that none was
+     *            provided.
+     * @return The new generic intermediate response.
+     */
+    public static GenericIntermediateResponse newGenericIntermediateResponse(
+            final String responseName, final Object responseValue) {
+        return new GenericIntermediateResponseImpl().setOID(responseName).setValue(responseValue);
+    }
+
+    /**
+     * Creates a new password modify extended result using the provided result
+     * code, and no generated password.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new password modify extended result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static PasswordModifyExtendedResult newPasswordModifyExtendedResult(
+            final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new PasswordModifyExtendedResultImpl(resultCode);
+    }
+
+    /**
+     * Creates a new result using the provided result code.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    public static Result newResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new ResultImpl(resultCode);
+    }
+
+    /**
+     * Creates a new search result entry using the provided distinguished name.
+     *
+     * @param name
+     *            The distinguished name of the entry.
+     * @return The new search result entry.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static SearchResultEntry newSearchResultEntry(final DN name) {
+        final Entry entry = new LinkedHashMapEntry().setName(name);
+        return new SearchResultEntryImpl(entry);
+    }
+
+    /**
+     * Creates a new search result entry backed by the provided entry.
+     * Modifications made to {@code entry} will be reflected in the returned
+     * search result entry. The returned search result entry supports updates to
+     * its list of controls, as well as updates to the name and attributes if
+     * the underlying entry allows.
+     *
+     * @param entry
+     *            The entry.
+     * @return The new search result entry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null} .
+     */
+    public static SearchResultEntry newSearchResultEntry(final Entry entry) {
+        Reject.ifNull(entry);
+        return new SearchResultEntryImpl(entry);
+    }
+
+    /**
+     * Creates a new search result entry using the provided distinguished name
+     * decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the entry.
+     * @return The new search result entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    public static SearchResultEntry newSearchResultEntry(final String name) {
+        final Entry entry = new LinkedHashMapEntry().setName(name);
+        return new SearchResultEntryImpl(entry);
+    }
+
+    /**
+     * Creates a new search result entry using the provided lines of LDIF
+     * decoded using the default schema.
+     *
+     * @param ldifLines
+     *            Lines of LDIF containing an LDIF add change record or an LDIF
+     *            entry record.
+     * @return The new search result entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} was empty, or contained invalid LDIF, or
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null} .
+     */
+    public static SearchResultEntry newSearchResultEntry(final String... ldifLines) {
+        return newSearchResultEntry(new LinkedHashMapEntry(ldifLines));
+    }
+
+    /**
+     * Creates a new search result reference using the provided continuation
+     * reference URI.
+     *
+     * @param uri
+     *            The first continuation reference URI to be added to this
+     *            search result reference.
+     * @return The new search result reference.
+     * @throws NullPointerException
+     *             If {@code uri} was {@code null}.
+     */
+    public static SearchResultReference newSearchResultReference(final String uri) {
+        Reject.ifNull(uri);
+        return new SearchResultReferenceImpl(uri);
+    }
+
+    /**
+     * Creates a new who am I extended result with the provided result code and
+     * no authorization ID.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return The new who am I extended result.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null} .
+     */
+    public static WhoAmIExtendedResult newWhoAmIExtendedResult(final ResultCode resultCode) {
+        Reject.ifNull(resultCode);
+        return new WhoAmIExtendedResultImpl(ResultCode.SUCCESS);
+    }
+
+    /**
+     * Creates an unmodifiable bind result using the provided response.
+     *
+     * @param result
+     *            The bind result to be copied.
+     * @return The unmodifiable bind result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static BindResult unmodifiableBindResult(final BindResult result) {
+        if (result instanceof UnmodifiableBindResultImpl) {
+            return result;
+        }
+        return new UnmodifiableBindResultImpl(result);
+    }
+
+    /**
+     * Creates an unmodifiable compare result using the provided response.
+     *
+     * @param result
+     *            The compare result to be copied.
+     * @return The unmodifiable compare result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static CompareResult unmodifiableCompareResult(final CompareResult result) {
+        if (result instanceof UnmodifiableCompareResultImpl) {
+            return result;
+        }
+        return new UnmodifiableCompareResultImpl(result);
+    }
+
+    /**
+     * Creates an unmodifiable generic extended result using the provided
+     * response.
+     *
+     * @param result
+     *            The generic extended result to be copied.
+     * @return The unmodifiable generic extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static GenericExtendedResult unmodifiableGenericExtendedResult(
+            final GenericExtendedResult result) {
+        if (result instanceof UnmodifiableGenericExtendedResultImpl) {
+            return result;
+        }
+        return new UnmodifiableGenericExtendedResultImpl(result);
+    }
+
+    /**
+     * Creates an unmodifiable generic intermediate response using the provided
+     * response.
+     *
+     * @param response
+     *            The generic intermediate response to be copied.
+     * @return The unmodifiable generic intermediate response.
+     * @throws NullPointerException
+     *             If {@code response} was {@code null}.
+     */
+    public static GenericIntermediateResponse unmodifiableGenericIntermediateResponse(
+            final GenericIntermediateResponse response) {
+        if (response instanceof UnmodifiableGenericIntermediateResponseImpl) {
+            return response;
+        }
+        return new UnmodifiableGenericIntermediateResponseImpl(response);
+    }
+
+    /**
+     * Creates an unmodifiable password modify extended result using the
+     * provided response.
+     *
+     * @param result
+     *            The password modify extended result to be copied.
+     * @return The unmodifiable password modify extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static PasswordModifyExtendedResult unmodifiablePasswordModifyExtendedResult(
+            final PasswordModifyExtendedResult result) {
+        if (result instanceof UnmodifiablePasswordModifyExtendedResultImpl) {
+            return result;
+        }
+        return new UnmodifiablePasswordModifyExtendedResultImpl(result);
+    }
+
+    /**
+     * Creates an unmodifiable result using the provided response.
+     *
+     * @param result
+     *            The result to be copied.
+     * @return The unmodifiable result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null}.
+     */
+    public static Result unmodifiableResult(final Result result) {
+        if (result instanceof UnmodifiableResultImpl) {
+            return result;
+        }
+        return new UnmodifiableResultImpl(result);
+    }
+
+    /**
+     * Creates an unmodifiable search result entry using the provided response.
+     *
+     * @param entry
+     *            The search result entry to be copied.
+     * @return The unmodifiable search result entry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public static SearchResultEntry unmodifiableSearchResultEntry(final SearchResultEntry entry) {
+        if (entry instanceof UnmodifiableSearchResultEntryImpl) {
+            return entry;
+        }
+        return new UnmodifiableSearchResultEntryImpl(entry);
+    }
+
+    /**
+     * Creates an unmodifiable search result reference using the provided
+     * response.
+     *
+     * @param reference
+     *            The search result reference to be copied.
+     * @return The unmodifiable search result reference.
+     * @throws NullPointerException
+     *             If {@code searchResultReference} was {@code null}.
+     */
+    public static SearchResultReference unmodifiableSearchResultReference(
+            final SearchResultReference reference) {
+        if (reference instanceof UnmodifiableSearchResultReferenceImpl) {
+            return reference;
+        }
+        return new UnmodifiableSearchResultReferenceImpl(reference);
+    }
+
+    /**
+     * Creates an unmodifiable who am I extended result using the provided
+     * response.
+     *
+     * @param result
+     *            The who am I result to be copied.
+     * @return The unmodifiable who am I extended result.
+     * @throws NullPointerException
+     *             If {@code result} was {@code null} .
+     */
+    public static WhoAmIExtendedResult unmodifiableWhoAmIExtendedResult(
+            final WhoAmIExtendedResult result) {
+        if (result instanceof UnmodifiableSearchResultReferenceImpl) {
+            return result;
+        }
+        return new UnmodifiableWhoAmIExtendedResultImpl(result);
+    }
+
+    /** Private constructor. */
+    private Responses() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Result.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Result.java
new file mode 100644
index 0000000..2fe9875
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/Result.java
@@ -0,0 +1,188 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Result is used to indicate the status of an operation performed by the
+ * server. A Result is comprised of several fields:
+ * <ul>
+ * <li>The <b>result code</b> can be retrieved using the method
+ * {@link #getResultCode}. This indicates the overall outcome of the operation.
+ * In particular, whether or not it succeeded which is indicated using a value
+ * of {@link ResultCode#SUCCESS}.
+ * <li>The optional <b>diagnostic message</b> can be retrieved using the method
+ * {@link #getDiagnosticMessage}. At the server's discretion, a diagnostic
+ * message may be included in a Result in order to supplement the result code
+ * with additional human-readable information.
+ * <li>The optional <b>matched DN</b> can be retrieved using the method
+ * {@link #getMatchedDN}. For certain result codes, this is used to indicate to
+ * the client the last entry used in finding the Request's target (or base)
+ * entry.
+ * <li>The optional <b>referrals</b> can be retrieved using the method
+ * {@link #getReferralURIs}. Referrals are present in a Result if the result
+ * code is set to {@link ResultCode#REFERRAL}, and it are absent with all other
+ * result codes.
+ * </ul>
+ */
+public interface Result extends Response {
+
+    @Override
+    Result addControl(Control control);
+
+    /**
+     * Adds the provided referral URI to this result.
+     *
+     * @param uri
+     *            The referral URI to be added.
+     * @return This result.
+     * @throws UnsupportedOperationException
+     *             If this result does not permit referrals to be added.
+     * @throws NullPointerException
+     *             If {@code uri} was {@code null}.
+     */
+    Result addReferralURI(String uri);
+
+    /**
+     * Returns the throwable cause associated with this result if available. A
+     * cause may be provided in cases where a result indicates a failure due to
+     * a client-side error.
+     *
+     * @return The throwable cause, or {@code null} if none was provided.
+     */
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns the diagnostic message associated with this result.
+     *
+     * @return The diagnostic message, which may be empty if none was provided
+     *         (never {@code null}).
+     */
+    String getDiagnosticMessage();
+
+    /**
+     * Returns the matched DN associated with this result.
+     *
+     * @return The matched DN, which may be empty if none was provided (never
+     *         {@code null}).
+     */
+    String getMatchedDN();
+
+    /**
+     * Returns a {@code List} containing the referral URIs included with this
+     * result. The returned {@code List} may be modified if permitted by this
+     * result.
+     *
+     * @return A {@code List} containing the referral URIs.
+     */
+    List<String> getReferralURIs();
+
+    /**
+     * Returns the result code associated with this result.
+     *
+     * @return The result code.
+     */
+    ResultCode getResultCode();
+
+    /**
+     * Indicates whether or not a referral needs to be chased in order to
+     * complete the operation.
+     * <p>
+     * Specifically, this method returns {@code true} if the result code is
+     * equal to {@link ResultCode#REFERRAL}.
+     *
+     * @return {@code true} if a referral needs to be chased, otherwise
+     *         {@code false}.
+     */
+    boolean isReferral();
+
+    /**
+     * Indicates whether or not the request succeeded or not. This method will
+     * return {code true} for all non-error responses.
+     *
+     * @return {@code true} if the request succeeded, otherwise {@code false}.
+     */
+    boolean isSuccess();
+
+    /**
+     * Sets the throwable cause associated with this result if available. A
+     * cause may be provided in cases where a result indicates a failure due to
+     * a client-side error.
+     *
+     * @param cause
+     *            The throwable cause, which may be {@code null} indicating that
+     *            none was provided.
+     * @return This result.
+     * @throws UnsupportedOperationException
+     *             If this result does not permit the cause to be set.
+     */
+    Result setCause(Throwable cause);
+
+    /**
+     * Sets the diagnostic message associated with this result.
+     *
+     * @param message
+     *            The diagnostic message, which may be empty or {@code null}
+     *            indicating that none was provided.
+     * @return This result.
+     * @throws UnsupportedOperationException
+     *             If this result does not permit the diagnostic message to be
+     *             set.
+     */
+    Result setDiagnosticMessage(String message);
+
+    /**
+     * Sets the matched DN associated with this result.
+     *
+     * @param dn
+     *            The matched DN associated, which may be empty or {@code null}
+     *            indicating that none was provided.
+     * @return This result.
+     * @throws UnsupportedOperationException
+     *             If this result does not permit the matched DN to be set.
+     */
+    Result setMatchedDN(String dn);
+
+    /**
+     * Sets the result code associated with this result.
+     *
+     * @param resultCode
+     *            The result code.
+     * @return This result.
+     * @throws UnsupportedOperationException
+     *             If this result does not permit the result code to be set.
+     * @throws NullPointerException
+     *             If {@code resultCode} was {@code null}.
+     */
+    Result setResultCode(ResultCode resultCode);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ResultImpl.java
new file mode 100644
index 0000000..4f97d96
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/ResultImpl.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * A generic result indicates the final status of an operation.
+ */
+final class ResultImpl extends AbstractResultImpl<Result> implements Result {
+
+    ResultImpl(final Result result) {
+        super(result);
+    }
+
+    ResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Result(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    Result getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntry.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntry.java
new file mode 100644
index 0000000..919aa24
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntry.java
@@ -0,0 +1,119 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Search Result Entry represents an entry found during a Search operation.
+ * <p>
+ * Each entry returned in a Search Result Entry will contain all appropriate
+ * attributes as specified in the Search request, subject to access control and
+ * other administrative policy.
+ * <p>
+ * Note that a Search Result Entry may hold zero attributes. This may happen
+ * when none of the attributes of an entry were requested or could be returned.
+ * <p>
+ * Note also that each returned attribute may hold zero attribute values. This
+ * may happen when only attribute types are requested, access controls prevent
+ * the return of values, or other reasons.
+ */
+public interface SearchResultEntry extends Response, Entry {
+
+    @Override
+    boolean addAttribute(Attribute attribute);
+
+    @Override
+    boolean addAttribute(Attribute attribute, Collection<? super ByteString> duplicateValues);
+
+    @Override
+    SearchResultEntry addAttribute(String attributeDescription, Object... values);
+
+    @Override
+    SearchResultEntry addControl(Control control);
+
+    @Override
+    SearchResultEntry clearAttributes();
+
+    @Override
+    boolean containsAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    @Override
+    boolean containsAttribute(String attributeDescription, Object... values);
+
+    @Override
+    Iterable<Attribute> getAllAttributes();
+
+    @Override
+    Iterable<Attribute> getAllAttributes(AttributeDescription attributeDescription);
+
+    @Override
+    Iterable<Attribute> getAllAttributes(String attributeDescription);
+
+    @Override
+    Attribute getAttribute(AttributeDescription attributeDescription);
+
+    @Override
+    Attribute getAttribute(String attributeDescription);
+
+    @Override
+    int getAttributeCount();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    DN getName();
+
+    @Override
+    boolean removeAttribute(Attribute attribute, Collection<? super ByteString> missingValues);
+
+    @Override
+    boolean removeAttribute(AttributeDescription attributeDescription);
+
+    @Override
+    SearchResultEntry removeAttribute(String attributeDescription, Object... values);
+
+    @Override
+    boolean replaceAttribute(Attribute attribute);
+
+    @Override
+    SearchResultEntry replaceAttribute(String attributeDescription, Object... values);
+
+    @Override
+    SearchResultEntry setName(DN dn);
+
+    @Override
+    SearchResultEntry setName(String dn);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java
new file mode 100644
index 0000000..5495022
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java
@@ -0,0 +1,196 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+
+/**
+ * Search result entry implementation.
+ */
+final class SearchResultEntryImpl extends AbstractResponseImpl<SearchResultEntry> implements
+        SearchResultEntry {
+
+    private final Entry entry;
+
+    SearchResultEntryImpl(final Entry entry) {
+        this.entry = entry;
+    }
+
+    SearchResultEntryImpl(final SearchResultEntry searchResultEntry) {
+        super(searchResultEntry);
+        this.entry = LinkedHashMapEntry.deepCopyOfEntry(searchResultEntry);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute) {
+        return entry.addAttribute(attribute);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute,
+            final Collection<? super ByteString> duplicateValues) {
+        return entry.addAttribute(attribute, duplicateValues);
+    }
+
+    @Override
+    public SearchResultEntry addAttribute(final String attributeDescription, final Object... values) {
+        entry.addAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public SearchResultEntry clearAttributes() {
+        entry.clearAttributes();
+        return this;
+    }
+
+    @Override
+    public boolean containsAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return entry.containsAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean containsAttribute(final String attributeDescription, final Object... values) {
+        return entry.containsAttribute(attributeDescription, values);
+    }
+
+    @Override
+    public boolean equals(final Object object) {
+        return entry.equals(object);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes() {
+        return entry.getAllAttributes();
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+        return entry.getAllAttributes(attributeDescription);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+        return entry.getAllAttributes(attributeDescription);
+    }
+
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        return entry.getAttribute(attributeDescription);
+    }
+
+    @Override
+    public Attribute getAttribute(final String attributeDescription) {
+        return entry.getAttribute(attributeDescription);
+    }
+
+    @Override
+    public int getAttributeCount() {
+        return entry.getAttributeCount();
+    }
+
+    @Override
+    public DN getName() {
+        return entry.getName();
+    }
+
+    @Override
+    public int hashCode() {
+        return entry.hashCode();
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return entry.removeAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean removeAttribute(final AttributeDescription attributeDescription) {
+        return entry.removeAttribute(attributeDescription);
+    }
+
+    @Override
+    public SearchResultEntry removeAttribute(final String attributeDescription,
+            final Object... values) {
+        entry.removeAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public boolean replaceAttribute(final Attribute attribute) {
+        return entry.replaceAttribute(attribute);
+    }
+
+    @Override
+    public SearchResultEntry replaceAttribute(final String attributeDescription,
+            final Object... values) {
+        entry.replaceAttribute(attributeDescription, values);
+        return this;
+    }
+
+    @Override
+    public SearchResultEntry setName(final DN dn) {
+        entry.setName(dn);
+        return this;
+    }
+
+    @Override
+    public SearchResultEntry setName(final String dn) {
+        entry.setName(dn);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SearchResultEntry(name=");
+        builder.append(getName());
+        builder.append(", attributes=");
+        builder.append(getAllAttributes());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    SearchResultEntry getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReference.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReference.java
new file mode 100644
index 0000000..5dbc89f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReference.java
@@ -0,0 +1,67 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * A Search Result Reference represents an area not yet explored during a Search
+ * operation.
+ */
+public interface SearchResultReference extends Response {
+
+    @Override
+    SearchResultReference addControl(Control control);
+
+    /**
+     * Adds the provided continuation reference URI to this search result
+     * reference.
+     *
+     * @param uri
+     *            The continuation reference URI to be added.
+     * @return This search result reference.
+     * @throws UnsupportedOperationException
+     *             If this search result reference does not permit continuation
+     *             reference URI to be added.
+     * @throws NullPointerException
+     *             If {@code uri} was {@code null}.
+     */
+    SearchResultReference addURI(String uri);
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    /**
+     * Returns a {@code List} containing the continuation reference URIs
+     * included with this search result reference. The returned {@code List} may
+     * be modified if permitted by this search result reference.
+     *
+     * @return A {@code List} containing the continuation reference URIs.
+     */
+    List<String> getURIs();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReferenceImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReferenceImpl.java
new file mode 100644
index 0000000..387b63c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultReferenceImpl.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Search result reference implementation.
+ */
+final class SearchResultReferenceImpl extends AbstractResponseImpl<SearchResultReference> implements
+        SearchResultReference {
+
+    private final List<String> uris = new LinkedList<>();
+
+    SearchResultReferenceImpl(final SearchResultReference searchResultReference) {
+        super(searchResultReference);
+        this.uris.addAll(searchResultReference.getURIs());
+    }
+
+    SearchResultReferenceImpl(final String uri) {
+        addURI(uri);
+    }
+
+    @Override
+    public SearchResultReference addURI(final String uri) {
+        Reject.ifNull(uri);
+        uris.add(uri);
+        return this;
+    }
+
+    @Override
+    public List<String> getURIs() {
+        return uris;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("SearchResultReference(uris=");
+        builder.append(getURIs());
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    SearchResultReference getThis() {
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableBindResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableBindResultImpl.java
new file mode 100644
index 0000000..9065af3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableBindResultImpl.java
@@ -0,0 +1,45 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * Unmodifiable Bind result implementation.
+ */
+class UnmodifiableBindResultImpl extends AbstractUnmodifiableResultImpl<BindResult> implements
+        BindResult {
+    UnmodifiableBindResultImpl(final BindResult impl) {
+        super(impl);
+    }
+
+    @Override
+    public ByteString getServerSASLCredentials() {
+        return impl.getServerSASLCredentials();
+    }
+
+    @Override
+    public boolean isSASLBindInProgress() {
+        return impl.isSASLBindInProgress();
+    }
+
+    @Override
+    public BindResult setServerSASLCredentials(final ByteString credentials) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableCompareResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableCompareResultImpl.java
new file mode 100644
index 0000000..f90a862
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableCompareResultImpl.java
@@ -0,0 +1,32 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * Unmodifiable Compare result implementation.
+ */
+class UnmodifiableCompareResultImpl extends AbstractUnmodifiableResultImpl<CompareResult> implements
+        CompareResult {
+    UnmodifiableCompareResultImpl(final CompareResult impl) {
+        super(impl);
+    }
+
+    @Override
+    public boolean matched() {
+        return impl.matched();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericExtendedResultImpl.java
new file mode 100644
index 0000000..3fa20f8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericExtendedResultImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * Unmodifiable Generic extended result implementation.
+ */
+class UnmodifiableGenericExtendedResultImpl extends
+        AbstractUnmodifiableExtendedResultImpl<GenericExtendedResult> implements ExtendedResult,
+        GenericExtendedResult {
+    UnmodifiableGenericExtendedResultImpl(final GenericExtendedResult impl) {
+        super(impl);
+    }
+
+    @Override
+    public GenericExtendedResult setOID(final String oid) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GenericExtendedResult setValue(final Object value) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericIntermediateResponseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericIntermediateResponseImpl.java
new file mode 100644
index 0000000..08b82fd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableGenericIntermediateResponseImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * Unmodifiable Generic extended result implementation.
+ */
+class UnmodifiableGenericIntermediateResponseImpl extends
+        AbstractUnmodifiableIntermediateResponseImpl<GenericIntermediateResponse> implements
+        GenericIntermediateResponse {
+    UnmodifiableGenericIntermediateResponseImpl(final GenericIntermediateResponse impl) {
+        super(impl);
+    }
+
+    @Override
+    public GenericIntermediateResponse setOID(final String oid) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public GenericIntermediateResponse setValue(final Object value) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiablePasswordModifyExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiablePasswordModifyExtendedResultImpl.java
new file mode 100644
index 0000000..35cacaa
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiablePasswordModifyExtendedResultImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * Unmodifiable Password modify extended result implementation.
+ */
+class UnmodifiablePasswordModifyExtendedResultImpl extends
+        AbstractUnmodifiableExtendedResultImpl<PasswordModifyExtendedResult> implements
+        PasswordModifyExtendedResult {
+    UnmodifiablePasswordModifyExtendedResultImpl(final PasswordModifyExtendedResult impl) {
+        super(impl);
+    }
+
+    @Override
+    public byte[] getGeneratedPassword() {
+        return impl.getGeneratedPassword();
+    }
+
+    @Override
+    public PasswordModifyExtendedResult setGeneratedPassword(final byte[] password) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PasswordModifyExtendedResult setGeneratedPassword(final char[] password) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableResultImpl.java
new file mode 100644
index 0000000..0f6e50f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableResultImpl.java
@@ -0,0 +1,26 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * A unmodifiable generic result indicates the final status of an operation.
+ */
+class UnmodifiableResultImpl extends AbstractUnmodifiableResultImpl<Result> implements Result {
+    UnmodifiableResultImpl(final Result impl) {
+        super(impl);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java
new file mode 100644
index 0000000..ef7e8fb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java
@@ -0,0 +1,177 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+import com.forgerock.opendj.util.Iterables;
+
+/**
+ * Unmodifiable Search result entry implementation.
+ */
+class UnmodifiableSearchResultEntryImpl extends AbstractUnmodifiableResponseImpl<SearchResultEntry>
+        implements SearchResultEntry {
+    private static final Function<Attribute, Attribute, NeverThrowsException> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
+            new Function<Attribute, Attribute, NeverThrowsException>() {
+                @Override
+                public Attribute apply(final Attribute value) {
+                    return Attributes.unmodifiableAttribute(value);
+                }
+            };
+
+    UnmodifiableSearchResultEntryImpl(final SearchResultEntry impl) {
+        super(impl);
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean addAttribute(final Attribute attribute,
+            final Collection<? super ByteString> duplicateValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry addAttribute(final String attributeDescription, final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry clearAttributes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean containsAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        return impl.containsAttribute(attribute, missingValues);
+    }
+
+    @Override
+    public boolean containsAttribute(final String attributeDescription, final Object... values) {
+        return impl.containsAttribute(attributeDescription, values);
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes() {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(
+                impl.getAllAttributes(), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(impl
+                .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Iterable<Attribute> getAllAttributes(final String attributeDescription) {
+        return Iterables.unmodifiableIterable(Iterables.transformedIterable(impl
+                .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION));
+    }
+
+    @Override
+    public Attribute getAttribute(final AttributeDescription attributeDescription) {
+        final Attribute attribute = impl.getAttribute(attributeDescription);
+        if (attribute != null) {
+            return Attributes.unmodifiableAttribute(attribute);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Attribute getAttribute(final String attributeDescription) {
+        final Attribute attribute = impl.getAttribute(attributeDescription);
+        if (attribute != null) {
+            return Attributes.unmodifiableAttribute(attribute);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public int getAttributeCount() {
+        return impl.getAttributeCount();
+    }
+
+    @Override
+    public DN getName() {
+        return impl.getName();
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public AttributeParser parseAttribute(final String attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    @Override
+    public boolean removeAttribute(final Attribute attribute,
+            final Collection<? super ByteString> missingValues) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean removeAttribute(final AttributeDescription attributeDescription) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry removeAttribute(final String attributeDescription,
+            final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean replaceAttribute(final Attribute attribute) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry replaceAttribute(final String attributeDescription,
+            final Object... values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry setName(final DN dn) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SearchResultEntry setName(final String dn) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultReferenceImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultReferenceImpl.java
new file mode 100644
index 0000000..dd330db
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultReferenceImpl.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+/**
+ * Unmodifiable Search result reference implementation.
+ */
+class UnmodifiableSearchResultReferenceImpl extends
+        AbstractUnmodifiableResponseImpl<SearchResultReference> implements SearchResultReference {
+    UnmodifiableSearchResultReferenceImpl(final SearchResultReference impl) {
+        super(impl);
+    }
+
+    @Override
+    public SearchResultReference addURI(final String uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getURIs() {
+        return impl.getURIs();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableWhoAmIExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableWhoAmIExtendedResultImpl.java
new file mode 100644
index 0000000..a9c9a7f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableWhoAmIExtendedResultImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+/**
+ * Unmodifiable Who Am I extended result implementation.
+ */
+class UnmodifiableWhoAmIExtendedResultImpl extends
+        AbstractUnmodifiableExtendedResultImpl<WhoAmIExtendedResult> implements
+        WhoAmIExtendedResult {
+    UnmodifiableWhoAmIExtendedResultImpl(final WhoAmIExtendedResult impl) {
+        super(impl);
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return impl.getAuthorizationID();
+    }
+
+    @Override
+    public WhoAmIExtendedResult setAuthorizationID(final String authorizationID) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResult.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResult.java
new file mode 100644
index 0000000..1787f7d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResult.java
@@ -0,0 +1,151 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+
+/**
+ * The who am I extended result as defined in RFC 4532. The result includes the
+ * primary authorization identity, in its primary form, that the server has
+ * associated with the user or application entity, but only if the who am I
+ * request succeeded.
+ * <p>
+ * The authorization identity is specified using an authorization ID, or
+ * {@code authzId}, as defined in RFC 4513 section 5.2.1.8.
+ * <p>
+ * The following example demonstrates use of the Who Am I? request and response.
+ *
+ * <pre>
+ * Connection connection = ...;
+ * String name = ...;
+ * char[] password = ...;
+ *
+ * Result result = connection.bind(name, password);
+ * if (result.isSuccess()) {
+ *     WhoAmIExtendedRequest request = Requests.newWhoAmIExtendedRequest();
+ *     WhoAmIExtendedResult extResult = connection.extendedRequest(request);
+ *
+ *     if (extResult.isSuccess()) {
+ *         // Authz ID: "  + extResult.getAuthorizationID());
+ *     }
+ * }
+ * </pre>
+ *
+ * @see org.forgerock.opendj.ldap.requests.WhoAmIExtendedRequest
+ * @see org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl
+ * @see <a href="http://tools.ietf.org/html/rfc4532">RFC 4532 - Lightweight
+ *      Directory Access Protocol (LDAP) "Who am I?" Operation </a>
+ * @see <a href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513 -
+ *      SASL Authorization Identities (authzId) </a>
+ */
+public interface WhoAmIExtendedResult extends ExtendedResult {
+
+    @Override
+    WhoAmIExtendedResult addControl(Control control);
+
+    @Override
+    WhoAmIExtendedResult addReferralURI(String uri);
+
+    /**
+     * Returns the authorization ID of the user. The authorization ID usually
+     * has the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @return The authorization ID of the user, or {@code null} if this result
+     *         does not contain an authorization ID.
+     */
+    String getAuthorizationID();
+
+    @Override
+    Throwable getCause();
+
+    @Override
+    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
+            throws DecodeException;
+
+    @Override
+    List<Control> getControls();
+
+    @Override
+    String getDiagnosticMessage();
+
+    @Override
+    String getMatchedDN();
+
+    @Override
+    String getOID();
+
+    @Override
+    List<String> getReferralURIs();
+
+    @Override
+    ResultCode getResultCode();
+
+    @Override
+    ByteString getValue();
+
+    @Override
+    boolean hasValue();
+
+    @Override
+    boolean isReferral();
+
+    @Override
+    boolean isSuccess();
+
+    /**
+     * Sets the authorization ID of the user. The authorization ID usually has
+     * the form "dn:" immediately followed by the distinguished name of the
+     * user, or "u:" followed by a user ID string, but other forms are
+     * permitted.
+     *
+     * @param authorizationID
+     *            The authorization ID of the user, which may be {@code null} if
+     *            this result does not contain an authorization ID.
+     * @return This who am I result.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code authorizationID} was non-empty and did not contain
+     *             a valid authorization ID type.
+     * @throws UnsupportedOperationException
+     *             If this who am I extended result does not permit the
+     *             authorization ID to be set.
+     */
+    WhoAmIExtendedResult setAuthorizationID(String authorizationID);
+
+    @Override
+    WhoAmIExtendedResult setCause(Throwable cause);
+
+    @Override
+    WhoAmIExtendedResult setDiagnosticMessage(String message);
+
+    @Override
+    WhoAmIExtendedResult setMatchedDN(String dn);
+
+    @Override
+    WhoAmIExtendedResult setResultCode(ResultCode resultCode);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResultImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResultImpl.java
new file mode 100644
index 0000000..4f7b184
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/WhoAmIExtendedResultImpl.java
@@ -0,0 +1,100 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_WHOAMI_INVALID_AUTHZID_TYPE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+
+/**
+ * Who Am I extended result implementation.
+ */
+final class WhoAmIExtendedResultImpl extends AbstractExtendedResult<WhoAmIExtendedResult> implements
+        WhoAmIExtendedResult {
+
+    /** The authorization ID. */
+    private String authorizationID;
+
+    /** Instantiation via factory. */
+    WhoAmIExtendedResultImpl(final ResultCode resultCode) {
+        super(resultCode);
+    }
+
+    WhoAmIExtendedResultImpl(final WhoAmIExtendedResult whoAmIExtendedResult) {
+        super(whoAmIExtendedResult);
+        this.authorizationID = whoAmIExtendedResult.getAuthorizationID();
+    }
+
+    @Override
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    @Override
+    public String getOID() {
+        // No response name defined.
+        return null;
+    }
+
+    @Override
+    public ByteString getValue() {
+        return (authorizationID != null) ? ByteString.valueOfUtf8(authorizationID) : null;
+    }
+
+    @Override
+    public boolean hasValue() {
+        return authorizationID != null;
+    }
+
+    @Override
+    public WhoAmIExtendedResult setAuthorizationID(final String authorizationID) {
+        if (authorizationID != null && authorizationID.length() != 0) {
+            final int colonIndex = authorizationID.indexOf(':');
+            if (colonIndex < 0) {
+                final LocalizableMessage message =
+                        ERR_WHOAMI_INVALID_AUTHZID_TYPE.get(authorizationID);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+        }
+
+        this.authorizationID = authorizationID;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("WhoAmIExtendedResponse(resultCode=");
+        builder.append(getResultCode());
+        builder.append(", matchedDN=");
+        builder.append(getMatchedDN());
+        builder.append(", diagnosticMessage=");
+        builder.append(getDiagnosticMessage());
+        builder.append(", referrals=");
+        builder.append(getReferralURIs());
+        builder.append(", authzId=");
+        builder.append(authorizationID);
+        builder.append(", controls=");
+        builder.append(getControls());
+        builder.append(")");
+        return builder.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/package-info.java
new file mode 100644
index 0000000..171eaf0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/responses/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for core LDAP responses.
+ */
+package org.forgerock.opendj.ldap.responses;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractApproximateMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractApproximateMatchingRuleImpl.java
new file mode 100644
index 0000000..66d90a0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractApproximateMatchingRuleImpl.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class implements an approximate matching rule that matches normalized
+ * values in byte order.
+ */
+abstract class AbstractApproximateMatchingRuleImpl extends AbstractMatchingRuleImpl {
+    private final Indexer indexer;
+
+    AbstractApproximateMatchingRuleImpl(String indexID) {
+        indexer = new DefaultIndexer(indexID);
+    }
+
+    @Override
+    public final Assertion getAssertion(final Schema schema, final ByteSequence assertionValue)
+            throws DecodeException {
+        return named(indexer.getIndexID(), normalizeAttributeValue(schema, assertionValue));
+    }
+
+    @Override
+    public final Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return Collections.singleton(indexer);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..2fe8af9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractEqualityMatchingRuleImpl.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class implements an equality matching rule that matches normalized
+ * values in byte order.
+ */
+abstract class AbstractEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
+
+    private final Indexer indexer;
+
+    AbstractEqualityMatchingRuleImpl(String indexID) {
+        this.indexer = new DefaultIndexer(indexID);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue)
+            throws DecodeException {
+        return defaultAssertion(normalizeAttributeValue(schema, assertionValue));
+    }
+
+    @Override
+    public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return Collections.singleton(indexer);
+    }
+
+    Assertion defaultAssertion(final ByteSequence normalizedAssertionValue) {
+        return named(indexer.getIndexID(), normalizedAssertionValue);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractMatchingRuleImpl.java
new file mode 100644
index 0000000..7313783
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractMatchingRuleImpl.java
@@ -0,0 +1,115 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.Assertion.*;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+
+/**
+ * This class implements a default equality or approximate matching rule that
+ * matches normalized values in byte order.
+ */
+abstract class AbstractMatchingRuleImpl implements MatchingRuleImpl {
+
+    static DefaultAssertion named(final String indexID, final ByteSequence normalizedAssertionValue) {
+        return new DefaultAssertion(indexID, normalizedAssertionValue);
+    }
+
+    private static final class DefaultAssertion implements Assertion {
+        /** The ID of the DB index to use with this assertion. */
+        private final String indexID;
+        private final ByteSequence normalizedAssertionValue;
+
+        private DefaultAssertion(final String indexID, final ByteSequence normalizedAssertionValue) {
+            this.indexID = indexID;
+            this.normalizedAssertionValue = normalizedAssertionValue;
+        }
+
+        @Override
+        public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+            return ConditionResult.valueOf(normalizedAssertionValue.equals(normalizedAttributeValue));
+        }
+
+        @Override
+        public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+            return factory.createExactMatchQuery(indexID, normalizedAssertionValue);
+        }
+    }
+
+    final class DefaultIndexer implements Indexer {
+        /** The ID of the DB index to use with this indexer. */
+        private final String indexID;
+
+        DefaultIndexer(String indexID) {
+            this.indexID = indexID;
+        }
+
+        @Override
+        public void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) throws DecodeException {
+            keys.add(normalizeAttributeValue(schema, value));
+        }
+
+        @Override
+        public String keyToHumanReadableString(ByteSequence key) {
+            return AbstractMatchingRuleImpl.this.keyToHumanReadableString(key);
+        }
+
+        @Override
+        public String getIndexID() {
+            return indexID;
+        }
+    }
+
+    String keyToHumanReadableString(ByteSequence key) {
+        return key.toByteString().toHexString();
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue)
+            throws DecodeException {
+        return UNDEFINED_ASSERTION;
+    }
+
+    @Override
+    public Assertion getSubstringAssertion(final Schema schema, final ByteSequence subInitial,
+            final List<? extends ByteSequence> subAnyElements, final ByteSequence subFinal)
+            throws DecodeException {
+        return UNDEFINED_ASSERTION;
+    }
+
+    @Override
+    public Assertion getGreaterOrEqualAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return UNDEFINED_ASSERTION;
+    }
+
+    @Override
+    public Assertion getLessOrEqualAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return UNDEFINED_ASSERTION;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..96db48d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractOrderingMatchingRuleImpl.java
@@ -0,0 +1,105 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class implements a default ordering matching rule that matches
+ * normalized values in byte order.
+ * <p>
+ * The getXXXAssertion() methods are default implementations which assume that
+ * the assertion syntax is the same as the attribute syntax. Override them if
+ * this assumption does not hold true.
+ */
+abstract class AbstractOrderingMatchingRuleImpl extends AbstractMatchingRuleImpl {
+    private final Indexer indexer;
+
+    /** Constructor for non-default matching rules. */
+    AbstractOrderingMatchingRuleImpl(String indexId) {
+        this.indexer = new DefaultIndexer(indexId);
+    }
+
+    @Override
+    public final Assertion getAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final ByteString normAssertion = normalizeAttributeValue(schema, value);
+        return new Assertion() {
+            @Override
+            public ConditionResult matches(final ByteSequence attributeValue) {
+                return ConditionResult.valueOf(attributeValue.compareTo(normAssertion) < 0);
+            }
+
+            @Override
+            public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                return factory.createRangeMatchQuery(indexer.getIndexID(), ByteString.empty(), normAssertion, false,
+                        false);
+            }
+        };
+    }
+
+    @Override
+    public final Assertion getGreaterOrEqualAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final ByteString normAssertion = normalizeAttributeValue(schema, value);
+        return new Assertion() {
+            @Override
+            public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+                return ConditionResult.valueOf(normalizedAttributeValue.compareTo(normAssertion) >= 0);
+            }
+
+            @Override
+            public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                return factory.createRangeMatchQuery(indexer.getIndexID(), normAssertion, ByteString.empty(), true,
+                        false);
+            }
+        };
+    }
+
+    @Override
+    public final Assertion getLessOrEqualAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final ByteString normAssertion = normalizeAttributeValue(schema, value);
+        return new Assertion() {
+            @Override
+            public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+                return ConditionResult.valueOf(normalizedAttributeValue.compareTo(normAssertion) <= 0);
+            }
+
+            @Override
+            public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                return factory.createRangeMatchQuery(indexer.getIndexID(), ByteString.empty(), normAssertion, false,
+                        true);
+            }
+        };
+    }
+
+    @Override
+    public final Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return Collections.singleton(indexer);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..83b0c42
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImpl.java
@@ -0,0 +1,532 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TreeSet;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements a default substring matching rule that matches
+ * normalized substring assertion values in byte order.
+ */
+abstract class AbstractSubstringMatchingRuleImpl extends AbstractMatchingRuleImpl {
+
+    /** The backslash character. */
+    private static final int BACKSLASH = 0x5C;
+
+    /**
+     * Default assertion implementation for substring matching rules.
+     * For example, with the assertion value "initial*any1*any2*any3*final",
+     * the assertion will be decomposed like this:
+     * <ul>
+     * <li>normInitial will contain "initial"</li>
+     * <li>normAnys will contain [ "any1", "any2", "any3" ]</li>
+     * <li>normFinal will contain "final"</li>
+     * </ul>
+     */
+    final class DefaultSubstringAssertion implements Assertion {
+        /** Normalized substring for the text before the first '*' character. */
+        private final ByteString normInitial;
+        /** Normalized substrings for all text chunks in between '*' characters. */
+        private final ByteString[] normAnys;
+        /** Normalized substring for the text after the last '*' character. */
+        private final ByteString normFinal;
+
+
+        private DefaultSubstringAssertion(final ByteString normInitial, final ByteString[] normAnys,
+                final ByteString normFinal) {
+            this.normInitial = normInitial;
+            this.normAnys = normAnys;
+            this.normFinal = normFinal;
+        }
+
+        @Override
+        public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+            final int valueLength = normalizedAttributeValue.length();
+
+            int pos = 0;
+            if (normInitial != null) {
+                final int initialLength = normInitial.length();
+                if (initialLength > valueLength) {
+                    return ConditionResult.FALSE;
+                }
+
+                for (; pos < initialLength; pos++) {
+                    if (normInitial.byteAt(pos) != normalizedAttributeValue.byteAt(pos)) {
+                        return ConditionResult.FALSE;
+                    }
+                }
+            }
+
+            if (normAnys != null) {
+            matchEachSubstring:
+                for (final ByteSequence element : normAnys) {
+                    final int anyLength = element.length();
+                    final int end = valueLength - anyLength;
+                matchCurrentSubstring:
+                    for (; pos <= end; pos++) {
+                        // Try to match all characters from the substring
+                        for (int i = 0; i < anyLength; i++) {
+                            if (element.byteAt(i) != normalizedAttributeValue.byteAt(pos + i)) {
+                                // not a match,
+                                // try to find a match in the rest of this value
+                                continue matchCurrentSubstring;
+                            }
+                        }
+                        // we just matched current substring,
+                        // go try to match the next substring
+                        pos += anyLength;
+                        continue matchEachSubstring;
+                    }
+                    // Could not match current substring
+                    return ConditionResult.FALSE;
+                }
+            }
+
+            if (normFinal != null) {
+                final int finalLength = normFinal.length();
+
+                if (valueLength - finalLength < pos) {
+                    return ConditionResult.FALSE;
+                }
+
+                pos = valueLength - finalLength;
+                for (int i = 0; i < finalLength; i++, pos++) {
+                    if (normFinal.byteAt(i) != normalizedAttributeValue.byteAt(pos)) {
+                        return ConditionResult.FALSE;
+                    }
+                }
+            }
+
+            return ConditionResult.TRUE;
+        }
+
+        @Override
+        public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+            if (normInitial == null && (normAnys == null || normAnys.length == 0) && normFinal == null) {
+                // Can happen with a filter like "cn:en.6:=*", just return an empty record
+                return factory.createMatchAllQuery();
+            }
+
+            final Collection<T> subqueries = new LinkedList<>();
+            if (normInitial != null) {
+                // relies on the fact that equality indexes are also ordered
+                subqueries.add(rangeMatch(factory, equalityIndexId, normInitial));
+            }
+            if (normAnys != null) {
+                for (ByteString normAny : normAnys) {
+                    substringMatch(factory, normAny, subqueries);
+                }
+            }
+            if (normFinal != null) {
+                substringMatch(factory, normFinal, subqueries);
+            }
+            if (normInitial != null) {
+                // Add this one last to minimize the risk to run the same search twice
+                // (possible overlapping with the use of equality index at the start of this method)
+                substringMatch(factory, normInitial, subqueries);
+            }
+            return factory.createIntersectionQuery(subqueries);
+        }
+
+        private <T> T rangeMatch(IndexQueryFactory<T> factory, String indexID, ByteSequence lower) {
+            // Iterate through all the keys that have this value as the prefix.
+
+            // Set the upper bound for a range search.
+            // We need a key for the upper bound that is of equal length
+            // but slightly greater than the lower bound.
+            final ByteStringBuilder upper = new ByteStringBuilder(lower);
+
+            for (int i = upper.length() - 1; i >= 0; i--) {
+                if (upper.byteAt(i) == (byte) 0xFF) {
+                    // We have to carry the overflow to the more significant byte.
+                    upper.setByte(i, (byte) 0);
+                } else {
+                    // No overflow, we can stop.
+                    upper.setByte(i, (byte) (upper.byteAt(i) + 1));
+                    break;
+                }
+            }
+
+            // Read the range: lower <= keys < upper.
+            return factory.createRangeMatchQuery(indexID, lower, upper, true, false);
+        }
+
+        private <T> void substringMatch(final IndexQueryFactory<T> factory, final ByteString normSubstring,
+                final Collection<T> subqueries) {
+            final int substrLength = factory.getIndexingOptions().substringKeySize();
+            final String indexId = substringIndexId + ":" + substrLength;
+
+            // There are two cases, depending on whether the user-provided
+            // substring is smaller than the configured index substring length or not.
+            if (normSubstring.length() < substrLength) {
+                subqueries.add(rangeMatch(factory, indexId, normSubstring));
+            } else {
+                // Break the value up into fragments of length equal to the
+                // index substring length, and read those keys.
+
+                // Eliminate duplicates by putting the keys into a set.
+                final TreeSet<ByteSequence> substringKeys = new TreeSet<>();
+
+                // Example: The value is ABCDE and the substring length is 3.
+                // We produce the keys ABC BCD CDE.
+                for (int first = 0, last = substrLength;
+                     last <= normSubstring.length(); first++, last++) {
+                    substringKeys.add(normSubstring.subSequence(first, first + substrLength));
+                }
+
+                for (ByteSequence key : substringKeys) {
+                    subqueries.add(factory.createExactMatchQuery(indexId, key));
+                }
+            }
+        }
+
+    }
+
+    private final class SubstringIndexer implements Indexer {
+
+        private final String indexID;
+        private final int substringKeySize;
+
+        private SubstringIndexer(int substringKeySize) {
+            this.substringKeySize = substringKeySize;
+            this.indexID = substringIndexId + ":" + this.substringKeySize;
+        }
+
+        @Override
+        public void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) throws DecodeException {
+            final ByteString normValue = normalizeAttributeValue(schema, value);
+
+            // Example: The value is ABCDE and the substring length is 3.
+            // We produce the keys ABC BCD CDE DE E
+            // To find values containing a short substring such as DE,
+            // iterate through keys with prefix DE. To find values
+            // containing a longer substring such as BCDE, read keys BCD and CDE.
+            for (int i = 0, remain = normValue.length(); remain > 0; i++, remain--) {
+                int len = Math.min(substringKeySize, remain);
+                keys.add(normValue.subSequence(i, i  + len));
+            }
+        }
+
+        @Override
+        public String keyToHumanReadableString(ByteSequence key) {
+            return AbstractSubstringMatchingRuleImpl.this.keyToHumanReadableString(key);
+        }
+
+        @Override
+        public String getIndexID() {
+            return indexID;
+        }
+    }
+
+    /** Identifier of the substring index. */
+    private final String substringIndexId;
+
+    /** Identifier of the equality index. */
+    private final String equalityIndexId;
+
+    /** Constructor for non-default matching rules. */
+    AbstractSubstringMatchingRuleImpl(String substringIndexId, String equalityIndexId) {
+        this.substringIndexId = substringIndexId;
+        this.equalityIndexId = equalityIndexId;
+    }
+
+    @Override
+    String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+
+    @Override
+    public final Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
+        if (assertionValue.length() == 0) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_SUBSTRING_EMPTY.get());
+        }
+
+        ByteSequence initialString = null;
+        ByteSequence finalString = null;
+        List<ByteSequence> anyStrings = null;
+
+        final String valueString = assertionValue.toString();
+        if (valueString.length() == 1 && valueString.charAt(0) == '*') {
+            return getSubstringAssertion(schema, initialString, anyStrings, finalString);
+        }
+
+        final char[] escapeChars = new char[] { '*' };
+        final SubstringReader reader = new SubstringReader(valueString);
+
+        ByteString bytes = evaluateEscapes(reader, escapeChars);
+        if (bytes.length() > 0) {
+            initialString = bytes;
+        }
+        if (reader.remaining() == 0) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_SUBSTRING_NO_WILDCARDS.get(assertionValue));
+        }
+        while (true) {
+            reader.read();
+            bytes = evaluateEscapes(reader, escapeChars);
+            if (reader.remaining() > 0) {
+                if (bytes.length() == 0) {
+                    throw DecodeException.error(WARN_ATTR_SYNTAX_SUBSTRING_CONSECUTIVE_WILDCARDS
+                            .get(assertionValue, reader.pos()));
+                }
+                if (anyStrings == null) {
+                    anyStrings = new LinkedList<>();
+                }
+                anyStrings.add(bytes);
+            } else {
+                if (bytes.length() > 0) {
+                    finalString = bytes;
+                }
+                break;
+            }
+        }
+
+        return getSubstringAssertion(schema, initialString, anyStrings, finalString);
+    }
+
+    @Override
+    public final Assertion getSubstringAssertion(final Schema schema, final ByteSequence subInitial,
+            final List<? extends ByteSequence> subAnyElements, final ByteSequence subFinal)
+            throws DecodeException {
+        final ByteString normInitial =
+                subInitial == null ? null : normalizeSubString(schema, subInitial);
+
+        ByteString[] normAnys = null;
+        if (subAnyElements != null && !subAnyElements.isEmpty()) {
+            normAnys = new ByteString[subAnyElements.size()];
+            for (int i = 0; i < subAnyElements.size(); i++) {
+                normAnys[i] = normalizeSubString(schema, subAnyElements.get(i));
+            }
+        }
+        final ByteString normFinal = subFinal == null ? null : normalizeSubString(schema, subFinal);
+
+        return new DefaultSubstringAssertion(normInitial, normAnys, normFinal);
+    }
+
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value) throws DecodeException {
+        return normalizeAttributeValue(schema, value);
+    }
+
+    private byte evaluateEscapedChar(final SubstringReader reader, final char[] escapeChars) throws DecodeException {
+        final char c1 = reader.read();
+        byte b;
+        switch (c1) {
+        case '0':
+            b = 0x00;
+            break;
+        case '1':
+            b = 0x10;
+            break;
+        case '2':
+            b = 0x20;
+            break;
+        case '3':
+            b = 0x30;
+            break;
+        case '4':
+            b = 0x40;
+            break;
+        case '5':
+            b = 0x50;
+            break;
+        case '6':
+            b = 0x60;
+            break;
+        case '7':
+            b = 0x70;
+            break;
+        case '8':
+            b = (byte) 0x80;
+            break;
+        case '9':
+            b = (byte) 0x90;
+            break;
+        case 'A':
+        case 'a':
+            b = (byte) 0xA0;
+            break;
+        case 'B':
+        case 'b':
+            b = (byte) 0xB0;
+            break;
+        case 'C':
+        case 'c':
+            b = (byte) 0xC0;
+            break;
+        case 'D':
+        case 'd':
+            b = (byte) 0xD0;
+            break;
+        case 'E':
+        case 'e':
+            b = (byte) 0xE0;
+            break;
+        case 'F':
+        case 'f':
+            b = (byte) 0xF0;
+            break;
+        default:
+            if (c1 == BACKSLASH) {
+                return (byte) c1;
+            }
+            if (escapeChars != null) {
+                for (final char escapeChar : escapeChars) {
+                    if (c1 == escapeChar) {
+                        return (byte) c1;
+                    }
+                }
+            }
+            throw DecodeException.error(ERR_INVALID_ESCAPE_CHAR.get(reader.getString(), c1));
+        }
+
+        // The two positions must be the hex characters that
+        // comprise the escaped value.
+        if (reader.remaining() == 0) {
+            throw DecodeException.error(ERR_HEX_DECODE_INVALID_LENGTH.get(reader.getString()));
+        }
+
+        final char c2 = reader.read();
+        switch (c2) {
+        case '0':
+            // No action required.
+            break;
+        case '1':
+            b |= 0x01;
+            break;
+        case '2':
+            b |= 0x02;
+            break;
+        case '3':
+            b |= 0x03;
+            break;
+        case '4':
+            b |= 0x04;
+            break;
+        case '5':
+            b |= 0x05;
+            break;
+        case '6':
+            b |= 0x06;
+            break;
+        case '7':
+            b |= 0x07;
+            break;
+        case '8':
+            b |= 0x08;
+            break;
+        case '9':
+            b |= 0x09;
+            break;
+        case 'A':
+        case 'a':
+            b |= 0x0A;
+            break;
+        case 'B':
+        case 'b':
+            b |= 0x0B;
+            break;
+        case 'C':
+        case 'c':
+            b |= 0x0C;
+            break;
+        case 'D':
+        case 'd':
+            b |= 0x0D;
+            break;
+        case 'E':
+        case 'e':
+            b |= 0x0E;
+            break;
+        case 'F':
+        case 'f':
+            b |= 0x0F;
+            break;
+        default:
+            throw DecodeException.error(ERR_HEX_DECODE_INVALID_CHARACTER.get(new String(new char[] { c1, c2 }), c1));
+        }
+        return b;
+    }
+
+    private ByteString evaluateEscapes(final SubstringReader reader, final char[] escapeChars) throws DecodeException {
+        return evaluateEscapes(reader, escapeChars, escapeChars);
+    }
+
+    private ByteString evaluateEscapes(final SubstringReader reader, final char[] escapeChars,
+            final char[] delimiterChars) throws DecodeException {
+        int length = 0;
+        ByteStringBuilder valueBuffer = null;
+
+        reader.mark();
+        while (reader.remaining() > 0) {
+            char c = reader.read();
+            if (c == BACKSLASH) {
+                if (valueBuffer == null) {
+                    valueBuffer = new ByteStringBuilder();
+                }
+                valueBuffer.appendUtf8(reader.read(length));
+                valueBuffer.appendByte(evaluateEscapedChar(reader, escapeChars));
+                reader.mark();
+                length = 0;
+                continue;
+            }
+            if (delimiterChars != null) {
+                for (final char delimiterChar : delimiterChars) {
+                    if (c == delimiterChar) {
+                        return evaluateEscapes0(reader, length, valueBuffer);
+                    }
+                }
+            }
+            length++;
+        }
+
+        return evaluateEscapes0(reader, length, valueBuffer);
+    }
+
+    private ByteString evaluateEscapes0(final SubstringReader reader, int length, ByteStringBuilder valueBuffer) {
+        reader.reset();
+        if (valueBuffer != null) {
+            valueBuffer.appendUtf8(reader.read(length));
+            return valueBuffer.toByteString();
+        }
+        if (length > 0) {
+            return ByteString.valueOfUtf8(reader.read(length));
+        }
+        return ByteString.empty();
+    }
+
+    @Override
+    public final Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return Collections.singleton(new SubstringIndexer(options.substringKeySize()));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxImpl.java
new file mode 100644
index 0000000..aeb49ec
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxImpl.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+/**
+ * This class defines the set of methods and structures that must be implemented
+ * to define a new attribute syntax.
+ */
+abstract class AbstractSyntaxImpl implements SyntaxImpl {
+    AbstractSyntaxImpl() {
+        // Nothing to do.
+    }
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return null;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return null;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return null;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return null;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return false;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java
new file mode 100644
index 0000000..601cc52
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeType.java
@@ -0,0 +1,1094 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Arrays.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * This class defines a data structure for storing and interacting with an
+ * attribute type, which contains information about the format of an attribute
+ * and the syntax and matching rules that should be used when interacting with
+ * it.
+ * <p>
+ * Where ordered sets of names, or extra properties are provided, the ordering
+ * will be preserved when the associated fields are accessed via their getters
+ * or via the {@link #toString()} methods.
+ */
+public final class AttributeType extends SchemaElement implements Comparable<AttributeType> {
+
+    /** A fluent API for incrementally constructing attribute type. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private String oid;
+        private final List<String> names = new LinkedList<>();
+        private AttributeUsage attributeUsage;
+        private boolean isCollective;
+        private boolean isNoUserModification;
+        private boolean isObsolete;
+        private boolean isSingleValue;
+        private String approximateMatchingRuleOID;
+        private String equalityMatchingRuleOID;
+        private String orderingMatchingRuleOID;
+        private String substringMatchingRuleOID;
+        private String superiorTypeOID;
+        private String syntaxOID;
+
+        Builder(final AttributeType at, final SchemaBuilder builder) {
+            super(builder, at);
+            this.oid = at.oid;
+            this.attributeUsage = at.attributeUsage;
+            this.isCollective = at.isCollective;
+            this.isNoUserModification = at.isNoUserModification;
+            this.isObsolete = at.isObsolete;
+            this.isSingleValue = at.isSingleValue;
+            this.names.addAll(at.names);
+            this.approximateMatchingRuleOID = at.approximateMatchingRuleOID;
+            this.equalityMatchingRuleOID = at.equalityMatchingRuleOID;
+            this.orderingMatchingRuleOID = at.orderingMatchingRuleOID;
+            this.substringMatchingRuleOID = at.substringMatchingRuleOID;
+            this.superiorTypeOID = at.superiorTypeOID;
+            this.syntaxOID = at.syntaxOID;
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            this.oid = oid;
+        }
+
+        /**
+         * Adds this attribute type to the schema, throwing a
+         * {@code ConflictingSchemaElementException} if there is an existing
+         * attribute type with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing attribute type with the same
+         *             numeric OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return addToSchema(false);
+        }
+
+        /**
+         * Adds this attribute type to the schema overwriting any existing
+         * attribute type with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return addToSchema(true);
+        }
+
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return getSchemaBuilder().addAttributeType(new AttributeType(this), overwrite);
+        }
+
+        /**
+         * Sets the matching rule that should be used for approximate matching
+         * with this attribute type.
+         *
+         * @param approximateMatchingRuleOID
+         *            The matching rule OID.
+         * @return This builder.
+         */
+        public Builder approximateMatchingRule(String approximateMatchingRuleOID) {
+            this.approximateMatchingRuleOID = approximateMatchingRuleOID;
+            return this;
+        }
+
+        /**
+         * Specifies whether this attribute type is "collective".
+         *
+         * @param isCollective
+         *            {@code true} if this attribute type is "collective".
+         * @return This builder.
+         */
+        public Builder collective(boolean isCollective) {
+            this.isCollective = isCollective;
+            return this;
+        }
+
+        @Override
+        public Builder description(String description) {
+            return description0(description);
+        }
+
+        /**
+         * Sets the matching rule that should be used for equality matching with
+         * this attribute type.
+         *
+         * @param equalityMatchingRuleOID
+         *            The matching rule OID.
+         * @return This builder.
+         */
+        public Builder equalityMatchingRule(String equalityMatchingRuleOID) {
+            this.equalityMatchingRuleOID = equalityMatchingRuleOID;
+            return this;
+        }
+
+        @Override
+        public Builder extraProperties(Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(String extensionName, String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(asList(names));
+        }
+
+        /**
+         * Specifies whether this attribute type is "no-user-modification".
+         *
+         * @param isNoUserModification
+         *            {@code true} if this attribute type is
+         *            "no-user-modification"
+         * @return This builder.
+         */
+        public Builder noUserModification(boolean isNoUserModification) {
+            this.isNoUserModification = isNoUserModification;
+            return this;
+        }
+
+        /**
+         * Specifies whether this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete (default
+         *            is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this attribute type.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        /**
+         * Sets the matching rule that should be used for ordering with this
+         * attribute type.
+         *
+         * @param orderingMatchingRuleOID
+         *            The matching rule OID.
+         * @return This Builder.
+         */
+        public Builder orderingMatchingRule(String orderingMatchingRuleOID) {
+            this.orderingMatchingRuleOID = orderingMatchingRuleOID;
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user defined names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(String extensionName, String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user defined name.
+         *
+         * @param name
+         *            The user defined name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(String name) {
+            this.names.remove(name);
+            return this;
+        }
+
+        /**
+         * Specifies whether this attribute type is declared "single-value".
+         *
+         * @param isSingleValue
+         *            {@code true} if this attribute type is declared
+         *            "single-value".
+         * @return This builder.
+         */
+        public Builder singleValue(boolean isSingleValue) {
+            this.isSingleValue = isSingleValue;
+            return this;
+        }
+
+        /**
+         * Sets the matching rule that should be used for substring matching
+         * with this attribute type.
+         *
+         * @param substringMatchingRuleOID
+         *            The matching rule OID.
+         * @return This builder.
+         */
+        public Builder substringMatchingRule(String substringMatchingRuleOID) {
+            this.substringMatchingRuleOID = substringMatchingRuleOID;
+            return this;
+        }
+
+        /**
+         * Sets the superior type for this attribute type.
+         *
+         * @param superiorTypeOID
+         *            The superior type OID.
+         * @return This builder.
+         */
+        public Builder superiorType(String superiorTypeOID) {
+            this.superiorTypeOID = superiorTypeOID;
+            return this;
+        }
+
+        /**
+         * Sets the syntax for this attribute type.
+         *
+         * @param syntaxOID
+         *            The syntax OID.
+         * @return This builder.
+         */
+        public Builder syntax(String syntaxOID) {
+            this.syntaxOID = syntaxOID;
+            return this;
+        }
+
+        /**
+         * Sets the usage indicator for this attribute type.
+         *
+         * @param attributeUsage
+         *            The attribute usage.
+         * @return This builder.
+         */
+        public Builder usage(AttributeUsage attributeUsage) {
+            this.attributeUsage = attributeUsage;
+            return this;
+        }
+    }
+
+    /** The approximate matching rule for this attribute type. */
+    private final String approximateMatchingRuleOID;
+
+    /** The attribute usage for this attribute type. */
+    private final AttributeUsage attributeUsage;
+
+    /** The equality matching rule for this attribute type. */
+    private final String equalityMatchingRuleOID;
+
+    /** Indicates whether this attribute type is declared "collective". */
+    private final boolean isCollective;
+
+    /** Indicates whether this attribute type is declared "no-user-modification". */
+    private final boolean isNoUserModification;
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /** Indicates whether this definition is a temporary place-holder. */
+    private final boolean isPlaceHolder;
+
+    /** Indicates whether this attribute type is declared "single-value". */
+    private final boolean isSingleValue;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** The OID that may be used to reference this definition. */
+    private final String oid;
+
+    /** The ordering matching rule for this attribute type. */
+    private final String orderingMatchingRuleOID;
+
+    /** The substring matching rule for this attribute type. */
+    private final String substringMatchingRuleOID;
+
+    /** The superior attribute type from which this attribute type inherits. */
+    private final String superiorTypeOID;
+
+    /** The syntax for this attribute type. */
+    private final String syntaxOID;
+
+    /** True if this type has OID 2.5.4.0. */
+    private final boolean isObjectClassType;
+
+    /** The normalized name of this attribute type. */
+    private final String normalizedName;
+
+    /** The superior attribute type from which this attribute type inherits. */
+    private AttributeType superiorType;
+
+    /** The equality matching rule for this attribute type. */
+    private MatchingRule equalityMatchingRule;
+
+    /** The ordering matching rule for this attribute type. */
+    private MatchingRule orderingMatchingRule;
+
+    /** The substring matching rule for this attribute type. */
+    private MatchingRule substringMatchingRule;
+
+    /** The approximate matching rule for this attribute type. */
+    private MatchingRule approximateMatchingRule;
+
+    /** The syntax for this attribute type. */
+    private Syntax syntax;
+
+    /** Indicates whether or not validation has been performed. */
+    private boolean needsValidating = true;
+
+    /** The indicates whether or not validation failed. */
+    private boolean isValid;
+
+    private AttributeType(Builder builder) {
+        super(builder);
+        Reject.ifTrue(builder.oid == null || builder.oid.isEmpty(), "An OID must be specified.");
+
+        if (builder.superiorTypeOID == null && builder.syntaxOID == null
+                && !builder.getSchemaBuilder().getOptions().get(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX)) {
+            throw new IllegalArgumentException("Superior type and/or Syntax must not be null");
+        }
+
+        oid = builder.oid;
+        names = unmodifiableCopyOfList(builder.names);
+        attributeUsage = builder.attributeUsage;
+        isCollective = builder.isCollective;
+        isNoUserModification = builder.isNoUserModification;
+        isObjectClassType = "2.5.4.0".equals(oid);
+        isObsolete = builder.isObsolete;
+        isSingleValue = builder.isSingleValue;
+        approximateMatchingRuleOID = builder.approximateMatchingRuleOID;
+        equalityMatchingRuleOID = builder.equalityMatchingRuleOID;
+        orderingMatchingRuleOID = builder.orderingMatchingRuleOID;
+        substringMatchingRuleOID = builder.substringMatchingRuleOID;
+        superiorTypeOID = builder.superiorTypeOID;
+        syntaxOID = builder.syntaxOID;
+        isPlaceHolder = false;
+        normalizedName = toLowerCase(getNameOrOID());
+    }
+
+    /**
+     * Creates a new place-holder attribute type having the specified name,
+     * default syntax, and default matching rule. The OID of the place-holder
+     * attribute will be the normalized attribute type name followed by the
+     * suffix "-oid".
+     *
+     * @param name
+     *            The name of the place-holder attribute type.
+     * @param syntax
+     *            The syntax of the place-holder attribute type.
+     * @param equalityMatchingRule
+     *            The equality matching rule of the place-holder attribute type.
+     */
+    AttributeType(final String name, final Syntax syntax, final MatchingRule equalityMatchingRule) {
+        final StringBuilder builder = new StringBuilder(name.length() + 4);
+        StaticUtils.toLowerCase(name, builder);
+        builder.append("-oid");
+
+        this.oid = builder.toString();
+        this.names = Collections.singletonList(name);
+        this.isObsolete = false;
+        this.superiorTypeOID = null;
+        this.superiorType = null;
+        this.equalityMatchingRule = equalityMatchingRule;
+        this.equalityMatchingRuleOID = equalityMatchingRule.getOID();
+        this.orderingMatchingRuleOID = null;
+        this.substringMatchingRuleOID = null;
+        this.approximateMatchingRuleOID = null;
+        this.syntax = syntax;
+        this.syntaxOID = syntax.getOID();
+        this.isSingleValue = false;
+        this.isCollective = false;
+        this.isNoUserModification = false;
+        this.attributeUsage = null;
+        this.isObjectClassType = false;
+        this.isPlaceHolder = true;
+        this.normalizedName = StaticUtils.toLowerCase(getNameOrOID());
+    }
+
+    /**
+     * Compares this attribute type to the provided attribute type. The
+     * sort-order is defined as follows:
+     * <ul>
+     * <li>The {@code objectClass} attribute is less than all other attribute
+     * types.
+     * <li>User attributes are less than operational attributes.
+     * <li>Lexicographic comparison of the primary name and then, if equal, the
+     * OID.
+     * </ul>
+     *
+     * @param type
+     *            The attribute type to be compared.
+     * @return A negative integer, zero, or a positive integer as this attribute
+     *         type is less than, equal to, or greater than the specified
+     *         attribute type.
+     * @throws NullPointerException
+     *             If {@code name} was {@code null}.
+     */
+    @Override
+    public int compareTo(final AttributeType type) {
+        if (isObjectClassType) {
+            return type.isObjectClassType ? 0 : -1;
+        } else if (type.isObjectClassType) {
+            return 1;
+        } else {
+            final boolean isOperational = getUsage().isOperational();
+            final boolean typeIsOperational = type.getUsage().isOperational();
+            if (isOperational == typeIsOperational) {
+                final int tmp = normalizedName.compareTo(type.normalizedName);
+                if (tmp == 0) {
+                    return oid.compareTo(type.oid);
+                } else {
+                    return tmp;
+                }
+            } else {
+                return isOperational ? 1 : -1;
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if the provided object is an attribute type having
+     * the same numeric OID as this attribute type.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is an attribute type having
+     *         the same numeric OID as this attribute type.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof AttributeType) {
+            final AttributeType other = (AttributeType) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the matching rule that should be used for approximate matching
+     * with this attribute type.
+     *
+     * @return The matching rule that should be used for approximate matching
+     *         with this attribute type.
+     */
+    public MatchingRule getApproximateMatchingRule() {
+        return approximateMatchingRule;
+    }
+
+    /**
+     * Returns the matching rule that should be used for equality matching with
+     * this attribute type.
+     *
+     * @return The matching rule that should be used for equality matching with
+     *         this attribute type.
+     */
+    public MatchingRule getEqualityMatchingRule() {
+        return equalityMatchingRule;
+    }
+
+    /**
+     * Returns the name or OID for this schema definition. If it has one or more
+     * names, then the primary name will be returned. If it does not have any
+     * names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return oid;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Returns the OID for this schema definition.
+     *
+     * @return The OID for this schema definition.
+     */
+    public String getOID() {
+        return oid;
+    }
+
+    /**
+     * Returns the matching rule that should be used for ordering with this
+     * attribute type.
+     *
+     * @return The matching rule that should be used for ordering with this
+     *         attribute type.
+     */
+    public MatchingRule getOrderingMatchingRule() {
+        return orderingMatchingRule;
+    }
+
+    /**
+     * Returns the matching rule that should be used for substring matching with
+     * this attribute type.
+     *
+     * @return The matching rule that should be used for substring matching with
+     *         this attribute type.
+     */
+    public MatchingRule getSubstringMatchingRule() {
+        return substringMatchingRule;
+    }
+
+    /**
+     * Returns the superior type for this attribute type.
+     *
+     * @return The superior type for this attribute type, or <CODE>null</CODE>
+     *         if it does not have one.
+     */
+    public AttributeType getSuperiorType() {
+        return superiorType;
+    }
+
+    /**
+     * Returns the syntax for this attribute type.
+     *
+     * @return The syntax for this attribute type.
+     */
+    public Syntax getSyntax() {
+        return syntax;
+    }
+
+    /**
+     * Returns the usage indicator for this attribute type.
+     *
+     * @return The usage indicator for this attribute type.
+     */
+    public AttributeUsage getUsage() {
+        return attributeUsage != null ? attributeUsage : AttributeUsage.USER_APPLICATIONS;
+    }
+
+    /**
+     * Returns the hash code for this attribute type. It will be calculated as
+     * the hash code of the numeric OID.
+     *
+     * @return The hash code for this attribute type.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return {@code true} if the specified name is assigned to this schema
+     *         definition, or {@code false} if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name or OID.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return {@code true} if the provided value matches the OID or one of the
+     *         names assigned to this schema definition, or {@code false} if
+     *         not.
+     */
+    public boolean hasNameOrOID(final String value) {
+        return hasName(value) || getOID().equals(value);
+    }
+
+    /**
+     * Indicates whether this attribute type is declared "collective".
+     *
+     * @return {@code true} if this attribute type is declared "collective", or
+     *         {@code false} if not.
+     */
+    public boolean isCollective() {
+        return isCollective;
+    }
+
+    /**
+     * Indicates whether this attribute type is declared "no-user-modification".
+     *
+     * @return {@code true} if this attribute type is declared
+     *         "no-user-modification", or {@code false} if not.
+     */
+    public boolean isNoUserModification() {
+        return isNoUserModification;
+    }
+
+    /**
+     * Indicates whether or not this attribute type is the {@code objectClass}
+     * attribute type having the OID 2.5.4.0.
+     *
+     * @return {@code true} if this attribute type is the {@code objectClass}
+     *         attribute type, or {@code false} if not.
+     */
+    public boolean isObjectClass() {
+        return isObjectClassType;
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return {@code true} if this schema definition is declared "obsolete", or
+     *         {@code false} if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    /**
+     * Indicates whether this is an operational attribute. An operational
+     * attribute is one with a usage of "directoryOperation",
+     * "distributedOperation", or "dSAOperation" (i.e., only userApplications is
+     * not operational).
+     *
+     * @return {@code true} if this is an operational attribute, or
+     *         {@code false} if not.
+     */
+    public boolean isOperational() {
+        return getUsage().isOperational();
+    }
+
+    /**
+     * Indicates whether this attribute type is a temporary place-holder
+     * allocated dynamically by a non-strict schema when no registered attribute
+     * type was found.
+     * <p>
+     * Place holder attribute types have an OID which is the normalized
+     * attribute name with the string {@code -oid} appended. In addition, they
+     * will use the directory string syntax and case ignore matching rule.
+     *
+     * @return {@code true} if this is a temporary place-holder attribute type
+     *         allocated dynamically by a non-strict schema when no registered
+     *         attribute type was found.
+     * @see Schema#getAttributeType(String)
+     */
+    public boolean isPlaceHolder() {
+        return isPlaceHolder;
+    }
+
+    /**
+     * Indicates whether this attribute type is declared "single-value".
+     *
+     * @return {@code true} if this attribute type is declared "single-value",
+     *         or {@code false} if not.
+     */
+    public boolean isSingleValue() {
+        return isSingleValue;
+    }
+
+    /**
+     * Indicates whether or not this attribute type is a sub-type of the
+     * provided attribute type.
+     *
+     * @param type
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if this attribute type is a sub-type of the provided
+     *         attribute type, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code type} was {@code null}.
+     */
+    public boolean isSubTypeOf(final AttributeType type) {
+        AttributeType tmp = this;
+        do {
+            if (tmp.matches(type)) {
+                return true;
+            }
+            tmp = tmp.getSuperiorType();
+        } while (tmp != null);
+        return false;
+    }
+
+    /**
+     * Indicates whether or not this attribute type is a super-type of the
+     * provided attribute type.
+     *
+     * @param type
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if this attribute type is a super-type of the
+     *         provided attribute type, or {@code false} if not.
+     * @throws NullPointerException
+     *             If {@code type} was {@code null}.
+     */
+    public boolean isSuperTypeOf(final AttributeType type) {
+        return type.isSubTypeOf(this);
+    }
+
+    /**
+     * Implements a place-holder tolerant version of {@link #equals}. This
+     * method returns {@code true} in the following cases:
+     * <ul>
+     * <li>this attribute type is equal to the provided attribute type as
+     * specified by {@link #equals}
+     * <li>this attribute type is a place-holder and the provided attribute type
+     * has a name which matches the name of this attribute type
+     * <li>the provided attribute type is a place-holder and this attribute type
+     * has a name which matches the name of the provided attribute type.
+     * </ul>
+     *
+     * @param type
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if the provided attribute type matches this
+     *         attribute type.
+     */
+    public boolean matches(final AttributeType type) {
+        if (this == type) {
+            return true;
+        } else if (oid.equals(type.oid)) {
+            return true;
+        } else if (isPlaceHolder != type.isPlaceHolder) {
+            return isPlaceHolder ? type.hasName(normalizedName) : hasName(type.normalizedName);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        if (superiorTypeOID != null) {
+            buffer.append(" SUP ");
+            buffer.append(superiorTypeOID);
+        }
+
+        if (equalityMatchingRuleOID != null) {
+            buffer.append(" EQUALITY ");
+            buffer.append(equalityMatchingRuleOID);
+        }
+
+        if (orderingMatchingRuleOID != null) {
+            buffer.append(" ORDERING ");
+            buffer.append(orderingMatchingRuleOID);
+        }
+
+        if (substringMatchingRuleOID != null) {
+            buffer.append(" SUBSTR ");
+            buffer.append(substringMatchingRuleOID);
+        }
+
+        if (syntaxOID != null) {
+            buffer.append(" SYNTAX ");
+            buffer.append(syntaxOID);
+        }
+
+        if (isSingleValue()) {
+            buffer.append(" SINGLE-VALUE");
+        }
+
+        if (isCollective()) {
+            buffer.append(" COLLECTIVE");
+        }
+
+        if (isNoUserModification()) {
+            buffer.append(" NO-USER-MODIFICATION");
+        }
+
+        if (attributeUsage != null) {
+            buffer.append(" USAGE ");
+            buffer.append(attributeUsage);
+        }
+
+        if (approximateMatchingRuleOID != null) {
+            buffer.append(" ");
+            buffer.append(SCHEMA_PROPERTY_APPROX_RULE);
+            buffer.append(" '");
+            buffer.append(approximateMatchingRuleOID);
+            buffer.append("'");
+        }
+    }
+
+    boolean validate(final Schema schema, final List<AttributeType> invalidSchemaElements,
+            final List<LocalizableMessage> warnings) {
+        // Avoid validating this schema element more than once. This may occur
+        // if multiple attributes specify the same superior.
+        if (!needsValidating) {
+            return isValid;
+        }
+
+        // Prevent re-validation.
+        needsValidating = false;
+
+        if (superiorTypeOID != null) {
+            try {
+                superiorType = schema.getAttributeType(superiorTypeOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_SUPERIOR_TYPE1.get(getNameOrOID(),
+                                superiorTypeOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+
+            // First ensure that the superior has been validated and fail if it
+            // is invalid.
+            if (!superiorType.validate(schema, invalidSchemaElements, warnings)) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_SUPERIOR_TYPE.get(getNameOrOID(),
+                                superiorTypeOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+
+            // If there is a superior type, then it must have the same usage
+            // as the subordinate type. Also, if the superior type is
+            // collective, then so must the subordinate type be collective.
+            if (superiorType.getUsage() != getUsage()) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_SUPERIOR_USAGE.get(getNameOrOID(),
+                                getUsage().toString(), superiorType.getNameOrOID());
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+
+            if (superiorType.isCollective() != isCollective() && !isCollective()) {
+                LocalizableMessage message = WARN_ATTR_SYNTAX_ATTRTYPE_NONCOLLECTIVE_FROM_COLLECTIVE.get(
+                    getNameOrOID(), superiorType.getNameOrOID());
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+        }
+
+        if (syntaxOID != null) {
+            if (!schema.hasSyntax(syntaxOID)) {
+                // Try substituting a syntax from the core schema. This will
+                // never fail since the core schema is non-strict and will
+                // substitute the syntax if required.
+                syntax = Schema.getCoreSchema().getSyntax(syntaxOID);
+                final LocalizableMessage message =
+                        WARN_ATTR_TYPE_NOT_DEFINED1.get(getNameOrOID(), syntaxOID, syntax.getOID());
+                warnings.add(message);
+            } else {
+                syntax = schema.getSyntax(syntaxOID);
+            }
+        } else if (getSuperiorType() != null && getSuperiorType().getSyntax() != null) {
+            // Try to inherit the syntax from the superior type if possible
+            syntax = getSuperiorType().getSyntax();
+        } else {
+            syntax = schema.getDefaultSyntax();
+        }
+
+        if (equalityMatchingRuleOID != null) {
+            // Use explicitly defined matching rule first.
+            try {
+                equalityMatchingRule = schema.getMatchingRule(equalityMatchingRuleOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_EQUALITY_MR1.get(getNameOrOID(),
+                                equalityMatchingRuleOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+        } else if (getSuperiorType() != null && getSuperiorType().getEqualityMatchingRule() != null) {
+            // Inherit matching rule from superior type if possible
+            equalityMatchingRule = getSuperiorType().getEqualityMatchingRule();
+        } else if (getSyntax() != null && getSyntax().getEqualityMatchingRule() != null) {
+            // Use default for syntax
+            equalityMatchingRule = getSyntax().getEqualityMatchingRule();
+        }
+
+        if (orderingMatchingRuleOID != null) {
+            // Use explicitly defined matching rule first.
+            try {
+                orderingMatchingRule = schema.getMatchingRule(orderingMatchingRuleOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_ORDERING_MR1.get(getNameOrOID(),
+                                orderingMatchingRuleOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+        } else if (getSuperiorType() != null && getSuperiorType().getOrderingMatchingRule() != null) {
+            // Inherit matching rule from superior type if possible
+            orderingMatchingRule = getSuperiorType().getOrderingMatchingRule();
+        } else if (getSyntax() != null && getSyntax().getOrderingMatchingRule() != null) {
+            // Use default for syntax
+            orderingMatchingRule = getSyntax().getOrderingMatchingRule();
+        }
+
+        if (substringMatchingRuleOID != null) {
+            // Use explicitly defined matching rule first.
+            try {
+                substringMatchingRule = schema.getMatchingRule(substringMatchingRuleOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_SUBSTRING_MR1.get(getNameOrOID(),
+                                substringMatchingRuleOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+        } else if (getSuperiorType() != null
+                && getSuperiorType().getSubstringMatchingRule() != null) {
+            // Inherit matching rule from superior type if possible
+            substringMatchingRule = getSuperiorType().getSubstringMatchingRule();
+        } else if (getSyntax() != null && getSyntax().getSubstringMatchingRule() != null) {
+            // Use default for syntax
+            substringMatchingRule = getSyntax().getSubstringMatchingRule();
+        }
+
+        if (approximateMatchingRuleOID != null) {
+            // Use explicitly defined matching rule first.
+            try {
+                approximateMatchingRule = schema.getMatchingRule(approximateMatchingRuleOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_APPROXIMATE_MR1.get(getNameOrOID(),
+                                approximateMatchingRuleOID);
+                failValidation(invalidSchemaElements, warnings, message);
+                return false;
+            }
+        } else if (getSuperiorType() != null
+                && getSuperiorType().getApproximateMatchingRule() != null) {
+            // Inherit matching rule from superior type if possible
+            approximateMatchingRule = getSuperiorType().getApproximateMatchingRule();
+        } else if (getSyntax() != null && getSyntax().getApproximateMatchingRule() != null) {
+            // Use default for syntax
+            approximateMatchingRule = getSyntax().getApproximateMatchingRule();
+        }
+
+        // If the attribute type is COLLECTIVE, then it must have a usage of
+        // userApplications.
+        if (isCollective() && getUsage() != AttributeUsage.USER_APPLICATIONS) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_ATTRTYPE_COLLECTIVE_IS_OPERATIONAL.get(getNameOrOID());
+            warnings.add(message);
+        }
+
+        // If the attribute type is NO-USER-MODIFICATION, then it must not
+        // have a usage of userApplications.
+        if (isNoUserModification() && getUsage() == AttributeUsage.USER_APPLICATIONS) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_ATTRTYPE_NO_USER_MOD_NOT_OPERATIONAL.get(getNameOrOID());
+            warnings.add(message);
+        }
+
+        return isValid = true;
+    }
+
+    private void failValidation(final List<AttributeType> invalidSchemaElements,
+            final List<LocalizableMessage> warnings, final LocalizableMessage message) {
+        invalidSchemaElements.add(this);
+        warnings.add(ERR_ATTR_TYPE_VALIDATION_FAIL.get(toString(), message));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxImpl.java
new file mode 100644
index 0000000..75bd65c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxImpl.java
@@ -0,0 +1,200 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class defines the attribute type description syntax, which is used to
+ * hold attribute type definitions in the server schema. The format of this
+ * syntax is defined in RFC 2252.
+ */
+final class AttributeTypeSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_ATTRIBUTE_TYPE_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_ATTRTYPE_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c)));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            readOID(reader, allowMalformedNamesAndOptions);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the definition. But before we start, set default
+            // values for everything else we might need to know.
+            while (true) {
+                final String tokenName = SchemaUtils.readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the superior attribute
+                    // type from which this attribute type should inherit its
+                    // properties.
+                    readOID(reader, allowMalformedNamesAndOptions);
+                } else if ("equality".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the equality matching
+                    // rule to use for this attribute type.
+                    readOID(reader, allowMalformedNamesAndOptions);
+                } else if ("ordering".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the ordering matching
+                    // rule to use for this attribute type.
+                    readOID(reader, allowMalformedNamesAndOptions);
+                } else if ("substr".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the substring matching
+                    // rule to use for this attribute type.
+                    readOID(reader, allowMalformedNamesAndOptions);
+                } else if ("syntax".equalsIgnoreCase(tokenName)) {
+                    // This specifies the numeric OID of the syntax for this
+                    // matching rule. It may optionally be immediately followed
+                    // by
+                    // an open curly brace, an integer definition, and a close
+                    // curly brace to suggest the minimum number of characters
+                    // that should be allowed in values of that type. This
+                    // implementation will ignore any such length because it
+                    // does
+                    // not impose any practical limit on the length of attribute
+                    // values.
+                    readOIDLen(reader, allowMalformedNamesAndOptions);
+                } else if ("single-definition".equalsIgnoreCase(tokenName)) {
+                    // This indicates that attributes of this type are allowed to
+                    // have at most one definition. We do not need any more
+                    // parsing for this token.
+                } else if ("single-value".equalsIgnoreCase(tokenName)) {
+                    // This indicates that attributes of this type are allowed
+                    // to have at most one value.
+                    // We do not need any more parsing for this token.
+                } else if ("collective".equalsIgnoreCase(tokenName)) {
+                    // This indicates that attributes of this type are collective
+                    // (i.e., have their values generated dynamically in some way).
+                    // We do not need any more parsing for this token.
+                } else if ("no-user-modification".equalsIgnoreCase(tokenName)) {
+                    // This indicates that the values of attributes of this type
+                    // are not to be modified by end users. We do not need any
+                    // more parsing for this token.
+                } else if ("usage".equalsIgnoreCase(tokenName)) {
+                    // This specifies the usage string for this attribute type.
+                    // It should be followed by one of the strings
+                    // "userApplications", "directoryOperation",
+                    // "distributedOperation", or "dSAOperation".
+                    int length = 0;
+
+                    reader.skipWhitespaces();
+                    reader.mark();
+
+                    while (" )".indexOf(reader.read()) == -1) {
+                        length++;
+                    }
+
+                    reader.reset();
+                    final String usageStr = reader.read(length);
+                    if (!"userapplications".equalsIgnoreCase(usageStr)
+                            && !"directoryoperation".equalsIgnoreCase(usageStr)
+                            && !"distributedoperation".equalsIgnoreCase(usageStr)
+                            && !"dsaoperation".equalsIgnoreCase(usageStr)) {
+                        throwDecodeException(logger,
+                            WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_ATTRIBUTE_USAGE1.get(definition, usageStr));
+                    }
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    SchemaUtils.readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_ATTRTYPE_ILLEGAL_TOKEN1.get(definition, tokenName));
+
+                }
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ATTRTYPE_INVALID1.get(definition, de
+                    .getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeUsage.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeUsage.java
new file mode 100644
index 0000000..f373198
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AttributeUsage.java
@@ -0,0 +1,92 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+/**
+ * This enumeration defines the set of possible attribute usage values that may
+ * apply to an attribute type, as defined in RFC 2252.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2252">RFC 2252 - Lightweight
+ *      Directory Access Protocol (v3): Attribute Syntax Definitions</a>
+ */
+public enum AttributeUsage {
+    /**
+     * The attribute usage intended for user-defined attribute types.
+     */
+    USER_APPLICATIONS("userApplications", false),
+
+    /**
+     * The attribute usage intended for standard operational attributes.
+     */
+    DIRECTORY_OPERATION("directoryOperation", true),
+
+    /**
+     * The attribute usage intended for non-standard operational attributes
+     * shared among multiple DSAs.
+     */
+    DISTRIBUTED_OPERATION("distributedOperation", true),
+
+    /**
+     * The attribute usage intended for non-standard operational attributes used
+     * by a single DSA.
+     */
+    DSA_OPERATION("dSAOperation", true);
+
+    /** The string representation of this attribute usage. */
+    private final String usageString;
+
+    /**
+     * Flag indicating whether or not the usage should be categorized as
+     * operational.
+     */
+    private final boolean isOperational;
+
+    /**
+     * Creates a new attribute usage with the provided string representation.
+     *
+     * @param usageString
+     *            The string representation of this attribute usage.
+     * @param isOperational
+     *            <code>true</code> if attributes having this attribute usage
+     *            are operational, or <code>false</code> otherwise.
+     */
+    private AttributeUsage(final String usageString, final boolean isOperational) {
+        this.usageString = usageString;
+        this.isOperational = isOperational;
+    }
+
+    /**
+     * Determine whether or not attributes having this attribute usage are
+     * operational.
+     *
+     * @return Returns <code>true</code> if attributes having this attribute
+     *         usage are operational, or <code>false</code> otherwise.
+     */
+    public boolean isOperational() {
+        return isOperational;
+    }
+
+    /**
+     * Retrieves a string representation of this attribute usage.
+     *
+     * @return A string representation of this attribute usage.
+     */
+    @Override
+    public String toString() {
+        return usageString;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordExactEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordExactEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..1e61b4d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordExactEqualityMatchingRuleImpl.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/** This class implements the authPasswordMatch matching rule defined in RFC 3112. */
+final class AuthPasswordExactEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+    AuthPasswordExactEqualityMatchingRuleImpl() {
+        super(EMR_AUTH_PASSWORD_EXACT_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final String[] authPWComponents = AuthPasswordSyntaxImpl.decodeAuthPassword(value.toString());
+
+        final StringBuilder normalizedValue = new StringBuilder(
+            2 + authPWComponents[0].length() + authPWComponents[1].length() + authPWComponents[2].length());
+        normalizedValue.append(authPWComponents[0]);
+        normalizedValue.append('$');
+        normalizedValue.append(authPWComponents[1]);
+        normalizedValue.append('$');
+        normalizedValue.append(authPWComponents[2]);
+
+        return ByteString.valueOfUtf8(normalizedValue);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImpl.java
new file mode 100644
index 0000000..b24e5a8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImpl.java
@@ -0,0 +1,253 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_AUTH_PASSWORD_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_AUTH_PASSWORD_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg0;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the auth password attribute syntax, which is defined in
+ * RFC 3112 and is used to hold authentication information. Only equality
+ * matching will be allowed by default.
+ */
+final class AuthPasswordSyntaxImpl extends AbstractSyntaxImpl {
+
+    /**
+     * Decodes the provided authentication password value into its component parts.
+     *
+     * @param authPasswordValue
+     *            The authentication password value to be decoded.
+     * @return A three-element array, containing the scheme, authInfo, and
+     *         authValue components of the given string, in that order.
+     * @throws DecodeException
+     *             If a problem is encountered while attempting to decode the value.
+     */
+    static String[] decodeAuthPassword(final String authPasswordValue) throws DecodeException {
+        // First, ignore any leading whitespace.
+        final int length = authPasswordValue.length();
+        int pos = 0;
+        pos = readSpaces(authPasswordValue, pos);
+
+        // The next set of characters will be the scheme, which must consist
+        // only of digits, uppercase alphabetic characters, dash, period,
+        // slash, and underscore characters. It must be immediately followed
+        // by one or more spaces or a dollar sign.
+        final StringBuilder scheme = new StringBuilder();
+        pos = readScheme(authPasswordValue, scheme, pos);
+        if (scheme.length() == 0) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_NO_SCHEME.get());
+        }
+
+        pos = readSpaces(authPasswordValue, pos);
+        throwIfEndReached(authPasswordValue, length, pos, ERR_ATTR_SYNTAX_AUTHPW_NO_SCHEME_SEPARATOR);
+        pos++;
+        pos = readSpaces(authPasswordValue, pos);
+
+        // The next component must be the authInfo element, containing only
+        // printable characters other than the dollar sign and space character.
+        final StringBuilder authInfo = new StringBuilder();
+        pos = readAuthInfo(authPasswordValue, authInfo, pos);
+        if (authInfo.length() == 0) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_INFO.get());
+        }
+
+        pos = readSpaces(authPasswordValue, pos);
+        throwIfEndReached(authPasswordValue, length, pos, ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_INFO_SEPARATOR);
+        pos++;
+        pos = readSpaces(authPasswordValue, pos);
+
+        // The final component must be the authValue element, containing
+        // only printable characters other than the dollar sign and space character.
+        final StringBuilder authValue = new StringBuilder();
+        pos = readAuthValue(authPasswordValue, length, pos, authValue);
+        if (authValue.length() == 0) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_VALUE.get());
+        }
+
+        // The only characters remaining must be whitespace.
+        while (pos < length) {
+            final char c = authPasswordValue.charAt(pos);
+            if (c == ' ') {
+                pos++;
+            } else {
+                throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_INVALID_TRAILING_CHAR.get(pos));
+            }
+        }
+
+        return new String[] { scheme.toString(), authInfo.toString(), authValue.toString() };
+    }
+
+    private static int readAuthValue(final String authPasswordValue, final int length, int pos,
+            final StringBuilder authValue) throws DecodeException {
+        while (pos < length) {
+            final char c = authPasswordValue.charAt(pos);
+            if (c == ' ' || c == '$') {
+                break;
+            } else if (PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                authValue.append(c);
+                pos++;
+            } else {
+                throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_INVALID_AUTH_VALUE_CHAR.get(pos));
+            }
+        }
+        return pos;
+    }
+
+    private static void throwIfEndReached(final String authPasswordValue, final int length, int pos, final Arg0 message)
+            throws DecodeException {
+        if (pos >= length || authPasswordValue.charAt(pos) != '$') {
+            throw DecodeException.error(message.get());
+        }
+    }
+
+    private static int readAuthInfo(final String authPasswordValue, final StringBuilder authInfo, int pos)
+            throws DecodeException {
+        final int length = authPasswordValue.length();
+        while (pos < length) {
+            final char c = authPasswordValue.charAt(pos);
+            if (c == ' ' || c == '$') {
+                break;
+            } else if (PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                authInfo.append(c);
+                pos++;
+            } else {
+                throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_INVALID_AUTH_INFO_CHAR.get(pos));
+            }
+        }
+        return pos;
+    }
+
+    private static int readSpaces(final String authPasswordValue, int pos) {
+        final int length = authPasswordValue.length();
+        while (pos < length && authPasswordValue.charAt(pos) == ' ') {
+            pos++;
+        }
+        return pos;
+    }
+
+    private static int readScheme(final String authPasswordValue, final StringBuilder scheme, int pos)
+            throws DecodeException {
+        final int length = authPasswordValue.length();
+        while (pos < length) {
+            final char c = authPasswordValue.charAt(pos);
+
+            switch (c) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+            case 'A':
+            case 'B':
+            case 'C':
+            case 'D':
+            case 'E':
+            case 'F':
+            case 'G':
+            case 'H':
+            case 'I':
+            case 'J':
+            case 'K':
+            case 'L':
+            case 'M':
+            case 'N':
+            case 'O':
+            case 'P':
+            case 'Q':
+            case 'R':
+            case 'S':
+            case 'T':
+            case 'U':
+            case 'V':
+            case 'W':
+            case 'X':
+            case 'Y':
+            case 'Z':
+            case '-':
+            case '.':
+            case '/':
+            case '_':
+                scheme.append(c);
+                pos++;
+                break;
+            case ' ':
+            case '$':
+                return pos;
+            default:
+                throw DecodeException.error(ERR_ATTR_SYNTAX_AUTHPW_INVALID_SCHEME_CHAR.get(pos));
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * Indicates whether the provided value is encoded using the auth password syntax.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <CODE>true</CODE> if the value appears to be encoded using the
+     *         auth password syntax, or <CODE>false</CODE> if not.
+     */
+    static boolean isEncoded(final ByteSequence value) {
+        // FIXME -- Make this more efficient, and don't use exceptions for flow control.
+        try {
+            decodeAuthPassword(value.toString());
+            return true;
+        } catch (final Exception e) {
+            return false;
+        }
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_AUTH_PASSWORD_EXACT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_AUTH_PASSWORD_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        try {
+            decodeAuthPassword(value.toString());
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(de.getMessageObject());
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BinarySyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BinarySyntaxImpl.java
new file mode 100644
index 0000000..aff428a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BinarySyntaxImpl.java
@@ -0,0 +1,58 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_BINARY_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the binary attribute syntax, which is essentially a byte
+ * array using very strict matching. Equality, ordering, and substring matching
+ * will be allowed by default.
+ */
+final class BinarySyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_BINARY_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the binary syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..48861b0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleImpl.java
@@ -0,0 +1,66 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_INVALID_BIT;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_NOT_QUOTED;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_TOO_SHORT;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the bitStringMatch matching rule defined in X.520 and
+ * referenced in RFC 2252.
+ */
+final class BitStringEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    BitStringEqualityMatchingRuleImpl() {
+        super(EMR_BIT_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final String valueString = value.toString().toUpperCase();
+        final int length = valueString.length();
+        if (length < 3) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_BIT_STRING_TOO_SHORT.get(value));
+        }
+
+        if (valueString.charAt(0) != '\''
+                || valueString.charAt(length - 1) != 'B'
+                || valueString.charAt(length - 2) != '\'') {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_BIT_STRING_NOT_QUOTED.get(value));
+        }
+
+        for (int i = 1; i < length - 2; i++) {
+            switch (valueString.charAt(i)) {
+            case '0':
+            case '1':
+                // These characters are fine.
+                break;
+            default:
+                throw DecodeException.error(WARN_ATTR_SYNTAX_BIT_STRING_INVALID_BIT.get(value, valueString.charAt(i)));
+            }
+        }
+
+        return ByteString.valueOfUtf8(valueString);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxImpl.java
new file mode 100644
index 0000000..887ecf6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxImpl.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_INVALID_BIT;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_NOT_QUOTED;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_BIT_STRING_TOO_SHORT;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_BIT_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_BIT_STRING_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the bit string attribute syntax, which is comprised of a
+ * string of binary digits surrounded by single quotes and followed by a capital
+ * letter "B" (e.g., '101001'B).
+ */
+final class BitStringSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_BIT_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_BIT_STRING_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String valueString = value.toString().toUpperCase();
+
+        final int length = valueString.length();
+        if (length < 3) {
+            invalidReason.append(WARN_ATTR_SYNTAX_BIT_STRING_TOO_SHORT.get(value.toString()));
+            return false;
+        }
+
+        if (valueString.charAt(0) != '\'' || valueString.charAt(length - 2) != '\''
+                || valueString.charAt(length - 1) != 'B') {
+            invalidReason.append(WARN_ATTR_SYNTAX_BIT_STRING_NOT_QUOTED.get(value.toString()));
+            return false;
+        }
+
+        for (int i = 1; i < length - 2; i++) {
+            switch (valueString.charAt(i)) {
+            case '0':
+            case '1':
+                // These characters are fine.
+                break;
+            default:
+                invalidReason.append(WARN_ATTR_SYNTAX_BIT_STRING_INVALID_BIT.get(value.toString(),
+                        String.valueOf(valueString.charAt(i))));
+                return false;
+            }
+        }
+
+        // If we've gotten here, then everything is fine.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..5297276
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleImpl.java
@@ -0,0 +1,49 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the booleanMatch matching rule defined in X.520 and
+ * referenced in RFC 4519.
+ */
+final class BooleanEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    BooleanEqualityMatchingRuleImpl() {
+        super(EMR_BOOLEAN_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final String valueString = value.toString().toUpperCase();
+        if ("TRUE".equals(valueString) || "YES".equals(valueString)
+                || "ON".equals(valueString) || "1".equals(valueString)) {
+            return SchemaConstants.TRUE_VALUE;
+        } else if ("FALSE".equals(valueString) || "NO".equals(valueString)
+                || "OFF".equals(valueString) || "0".equals(valueString)) {
+            return SchemaConstants.FALSE_VALUE;
+        }
+        throw DecodeException.error(WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN.get(value.toString()));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxImpl.java
new file mode 100644
index 0000000..c8feaaa
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxImpl.java
@@ -0,0 +1,65 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_BOOLEAN_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_BOOLEAN_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the Boolean attribute syntax, which only allows values of
+ * "TRUE" or "FALSE" (although this implementation is more flexible and will
+ * also allow "YES", "ON", or "1" instead of "TRUE", or "NO", "OFF", or "0"
+ * instead of "FALSE"). Only equality matching is allowed by default for this
+ * syntax.
+ */
+final class BooleanSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_BOOLEAN_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_BOOLEAN_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String valueString = value.toString();
+        final String valueUpperCase = valueString.toUpperCase();
+
+        if (!"TRUE".equals(valueUpperCase) && !"YES".equals(valueUpperCase)
+                && !"ON".equals(valueUpperCase) && !"1".equals(valueUpperCase)
+                && !"FALSE".equals(valueUpperCase) && !"NO".equals(valueUpperCase)
+                && !"OFF".equals(valueUpperCase) && !"0".equals(valueUpperCase)) {
+            invalidReason.append(WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN.get(valueString));
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..bc82370
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class defines the caseExactMatch matching rule defined in X.520 and
+ * referenced in RFC 4519.
+ */
+final class CaseExactEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    CaseExactEqualityMatchingRuleImpl() {
+        super(EMR_CASE_EXACT_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, NO_CASE_FOLD);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..7f31eae
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleImpl.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseExactIA5Match matching rule defined in RFC
+ * 2252.
+ */
+final class CaseExactIA5EqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    CaseExactIA5EqualityMatchingRuleImpl() {
+        super(EMR_CASE_EXACT_IA5_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return SchemaUtils.normalizeIA5StringAttributeValue(value, TRIM, NO_CASE_FOLD);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..71b7de5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleImpl.java
@@ -0,0 +1,54 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseExactIA5SubstringsMatch matching rule. This
+ * matching rule actually isn't defined in any official specification, but some
+ * directory vendors do provide an implementation using an OID from their own
+ * private namespace.
+ */
+final class CaseExactIA5SubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    CaseExactIA5SubstringMatchingRuleImpl() {
+        super(SMR_CASE_EXACT_IA5_NAME, EMR_CASE_EXACT_IA5_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(TRIM, value);
+    }
+
+    @Override
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(false, value);
+    }
+
+    private ByteString normalize(final boolean trim, final ByteSequence value)
+            throws DecodeException {
+        return SchemaUtils.normalizeIA5StringAttributeValue(value, trim, NO_CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..abd6ece
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleImpl.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class defines the caseExactOrderingMatch matching rule defined in X.520
+ * and referenced in RFC 4519.
+ */
+final class CaseExactOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+
+    public CaseExactOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_CASE_EXACT_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, NO_CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..af43f46
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleImpl.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class defines the caseExactSubstringsMatch matching rule defined in
+ * X.520 and referenced in RFC 2252.
+ */
+final class CaseExactSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    CaseExactSubstringMatchingRuleImpl() {
+        super(SMR_CASE_EXACT_NAME, EMR_CASE_EXACT_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return normalize(TRIM, value);
+    }
+
+    @Override
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(false, value);
+    }
+
+    private ByteString normalize(final boolean trim, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, trim, NO_CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..d085ce9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines the caseIgnoreMatch matching rule defined in X.520 and
+ * referenced in RFC 2252.
+ */
+final class CaseIgnoreEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    CaseIgnoreEqualityMatchingRuleImpl() {
+        super(EMR_CASE_IGNORE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..ba31a8a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleImpl.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseIgnoreIA5Match matching rule defined in RFC
+ * 2252.
+ */
+final class CaseIgnoreIA5EqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    CaseIgnoreIA5EqualityMatchingRuleImpl() {
+        super(EMR_CASE_IGNORE_IA5_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return SchemaUtils.normalizeIA5StringAttributeValue(value, TRIM, CASE_FOLD);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..39d4a7c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleImpl.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseIgnoreIA5SubstringsMatch matching rule defined
+ * in RFC 2252.
+ */
+final class CaseIgnoreIA5SubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    CaseIgnoreIA5SubstringMatchingRuleImpl() {
+        super(SMR_CASE_IGNORE_IA5_NAME, EMR_CASE_IGNORE_IA5_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(TRIM, value);
+    }
+
+    @Override
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(false, value);
+    }
+
+    private ByteString normalize(final boolean trim, final ByteSequence value)
+            throws DecodeException {
+        return SchemaUtils.normalizeIA5StringAttributeValue(value, trim, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..ec65dd2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListEqualityMatchingRuleImpl.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StringPrepProfile.CASE_FOLD;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseIgnoreListMatch matching rule defined in X.520
+ * and referenced in RFC 2252.
+ */
+final class CaseIgnoreListEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    CaseIgnoreListEqualityMatchingRuleImpl() {
+        super(EMR_CASE_IGNORE_LIST_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringListAttributeValue(value, TRIM, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..56d2a7d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreListSubstringMatchingRuleImpl.java
@@ -0,0 +1,49 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the caseIgnoreListSubstringsMatch matching rule defined
+ * in X.520 and referenced in RFC 2252.
+ */
+final class CaseIgnoreListSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    CaseIgnoreListSubstringMatchingRuleImpl() {
+        super(SMR_CASE_IGNORE_LIST_NAME, EMR_CASE_IGNORE_LIST_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringListAttributeValue(value, TRIM, CASE_FOLD);
+    }
+
+    @Override
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        // In this case, the process for normalizing a substring is the same
+        // as normalizing a full value with the exception that it may
+        // include an opening or trailing space.
+        return SchemaUtils.normalizeStringAttributeValue(value, false, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..1a30f03
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleImpl.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class defines the caseIgnoreOrderingMatch matching rule defined in X.520
+ * and referenced in RFC 2252.
+ */
+final class CaseIgnoreOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+
+    public CaseIgnoreOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_CASE_IGNORE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..3bc1a5f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleImpl.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class defines the caseIgnoreSubstringsMatch matching rule defined in
+ * X.520 and referenced in RFC 2252.
+ */
+final class CaseIgnoreSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    CaseIgnoreSubstringMatchingRuleImpl() {
+        super(SMR_CASE_IGNORE_NAME, EMR_CASE_IGNORE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return normalize(TRIM, value);
+    }
+
+    @Override
+    ByteString normalizeSubString(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalize(false, value);
+    }
+
+    private ByteString normalize(final boolean trim, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, trim, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactAssertionSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactAssertionSyntaxImpl.java
new file mode 100644
index 0000000..dff3910
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactAssertionSyntaxImpl.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 Manuel Gaupp
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_CERTIFICATE_EXACT_ASSERTION_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the Certificate Exact Assertion attribute syntax, which
+ * contains components for matching X.509 certificates.
+ */
+final class CertificateExactAssertionSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getName() {
+        return SYNTAX_CERTIFICATE_EXACT_ASSERTION_NAME;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return false;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // This method will never be called because this syntax is only used
+        // within assertions.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java
new file mode 100644
index 0000000..c81a94b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java
@@ -0,0 +1,202 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2014 Manuel Gaupp
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.GSERParser;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the certificateExactMatch matching rule defined in
+ * X.509 and referenced in RFC 4523.
+ */
+final class CertificateExactMatchingRuleImpl
+        extends AbstractEqualityMatchingRuleImpl {
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The GSER identifier for the serialNumber named value. */
+    private static final String GSER_ID_SERIALNUMBER = "serialNumber";
+    /** The GSER identifier for the issuer named value. */
+    private static final String GSER_ID_ISSUER = "issuer";
+    /** The GSER identifier for the rdnSequence IdentifiedChoiceValue. */
+    private static final String GSER_ID_RDNSEQUENCE = "rdnSequence";
+
+    CertificateExactMatchingRuleImpl() {
+        super(EMR_CERTIFICATE_EXACT_NAME);
+    }
+
+    /**
+     * Retrieves the normalized form of the provided value, which is best suited
+     * for efficiently performing matching operations on that value.
+     *
+     * @param value The value to be normalized.
+     *
+     * @return The normalized version of the provided value.
+     *
+     * @throws DecodeException If the provided value is invalid according to
+     * the associated attribute syntax.
+     */
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        // Read the X.509 Certificate and extract serialNumber and issuerDN
+        final BigInteger serialNumber;
+        final String dnstring;
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            InputStream inputStream = new ByteArrayInputStream(value.toByteArray());
+            X509Certificate certValue = (X509Certificate) certFactory
+                    .generateCertificate(inputStream);
+
+            serialNumber = certValue.getSerialNumber();
+            X500Principal issuer = certValue.getIssuerX500Principal();
+            dnstring = issuer.getName(X500Principal.RFC2253);
+        } catch (CertificateException ce) {
+            // There seems to be a problem while parsing the certificate.
+            final LocalizableMessage message =
+                    ERR_MR_CERTIFICATE_MATCH_PARSE_ERROR.get(ce.getMessage());
+            logger.trace(message);
+
+            // return the raw bytes as a fall back
+            return value.toByteString();
+        }
+
+        final ByteString certificateIssuer = normalizeDN(schema, dnstring);
+        return createEncodedValue(serialNumber, certificateIssuer);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        // validate and normalize the GSER structure
+        // according to the definitions from RFC 4523, Appendix A.1
+        final GSERParser parser = new GSERParser(value.toString());
+        try {
+            // the String starts with a sequence
+            parser.readStartSequence();
+        } catch (DecodeException e) {
+            logger.traceException(e);
+            // Assume the assertion value is a certificate and parse issuer and
+            // serial number. If the value is not even a certificate then the
+            // raw bytes will be returned.
+            return defaultAssertion(normalizeAttributeValue(schema, value));
+        }
+
+        final BigInteger serialNumber;
+        final String dnstring;
+        String identifier;
+        try {
+            // the first namedValue is serialNumber
+            identifier = parser.nextNamedValueIdentifier();
+            if (!GSER_ID_SERIALNUMBER.equals(identifier)) {
+                throw DecodeException.error(
+                        ERR_MR_CERTIFICATE_MATCH_IDENTIFIER_NOT_FOUND.get(GSER_ID_SERIALNUMBER));
+            }
+
+            // The value for the serialNumber
+            serialNumber = parser.nextBigInteger();
+
+            // separator
+            parser.skipSeparator();
+
+            // the next namedValue is issuer
+            identifier = parser.nextNamedValueIdentifier();
+            if (!GSER_ID_ISSUER.equals(identifier)) {
+                throw DecodeException.error(
+                        ERR_MR_CERTIFICATE_MATCH_IDENTIFIER_NOT_FOUND.get(GSER_ID_ISSUER));
+            }
+
+            // expecting "rdnSequence:"
+            identifier = parser.nextChoiceValueIdentifier();
+            if (!GSER_ID_RDNSEQUENCE.equals(identifier)) {
+                throw DecodeException.error(
+                        ERR_MR_CERTIFICATE_MATCH_IDENTIFIER_NOT_FOUND.get(GSER_ID_RDNSEQUENCE));
+            }
+
+            // now the issuer dn
+            dnstring = parser.nextString();
+
+            // Closing the Sequence
+            parser.readEndSequence();
+
+            // There should not be additional characters
+            if (parser.hasNext()) {
+                LocalizableMessage message = ERR_MR_CERTIFICATE_MATCH_EXPECTED_END.get();
+                throw DecodeException.error(message);
+            }
+        } catch (DecodeException e) {
+            LocalizableMessage message =
+                    ERR_MR_CERTIFICATE_MATCH_GSER_INVALID.get(StaticUtils.getExceptionMessage(e));
+            throw DecodeException.error(message);
+        }
+
+        final ByteString certificateIssuer = normalizeDN(schema, dnstring);
+        return defaultAssertion(createEncodedValue(serialNumber, certificateIssuer));
+    }
+
+    private ByteString normalizeDN(final Schema schema, final String dnstring) throws DecodeException {
+        try {
+            DN dn = DN.valueOf(dnstring, schema.asNonStrictSchema());
+            return dn.toNormalizedByteString();
+        } catch (Exception e) {
+            logger.traceException(e);
+
+            // We couldn't normalize the DN for some reason.
+            LocalizableMessage message =
+                    ERR_MR_CERTIFICATE_MATCH_INVALID_DN.get(dnstring,
+                            StaticUtils.getExceptionMessage(e));
+            throw DecodeException.error(message);
+        }
+    }
+
+    /**
+     * Creates the value containing serialNumber and issuer DN.
+     *
+     * @param serial the serialNumber
+     * @param issuerDN the issuer DN String
+     *
+     * @return the encoded ByteString
+     */
+    private static ByteString createEncodedValue(BigInteger serial, ByteString issuerDN) {
+        return new ByteStringBuilder()
+            .appendBytes(issuerDN)
+            .appendByte(0) // Separator
+            .appendBytes(serial.toByteArray())
+            .toByteString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateListSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateListSyntaxImpl.java
new file mode 100644
index 0000000..bae480e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateListSyntaxImpl.java
@@ -0,0 +1,65 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_CERTLIST_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the certificate list attribute syntax. This should be
+ * restricted to holding only X.509 certificate lists, but we will accept any
+ * set of bytes. It will be treated much like the octet string attribute syntax.
+ */
+final class CertificateListSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_CERTLIST_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return true;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the certificate list syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificatePairSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificatePairSyntaxImpl.java
new file mode 100644
index 0000000..4f2cfc8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificatePairSyntaxImpl.java
@@ -0,0 +1,64 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_CERTPAIR_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the certificate pair attribute syntax. This should be
+ * restricted to holding only X.509 certificate pairs, but we will accept any
+ * set of bytes. It will be treated much like the octet string attribute syntax.
+ */
+final class CertificatePairSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_CERTPAIR_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return true;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the certificate pair syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxImpl.java
new file mode 100644
index 0000000..abb5aaf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxImpl.java
@@ -0,0 +1,200 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 Manuel Gaupp
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.io.IOException;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_INVALID_DER;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_INVALID_VERSION;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_NO_ELEMENT_EXPECTED;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_NOTVALID;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V23;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V3;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CERTIFICATE_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_CERTIFICATE_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.io.ASN1.*;
+
+import com.forgerock.opendj.util.StaticUtils;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class implements the certificate attribute syntax. It is restricted to
+ * accept only X.509 certificates.
+ */
+final class CertificateSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CERTIFICATE_EXACT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_CERTIFICATE_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return true;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Skip validation if strict validation is disabled.
+        if (schema.getOption(ALLOW_MALFORMED_CERTIFICATES)) {
+            return true;
+        }
+
+        // Validate the ByteSequence against the definitions of X.509, clause 7
+        ASN1Reader reader = ASN1.getReader(value);
+        try {
+            // Certificate SIGNED SEQUENCE
+            reader.readStartSequence(UNIVERSAL_SEQUENCE_TYPE);
+
+            // CertificateContent SEQUENCE
+            reader.readStartSequence(UNIVERSAL_SEQUENCE_TYPE);
+
+            // Optional Version
+            long x509Version = 0;
+            if (reader.hasNextElement() && reader.peekType() == (TYPE_MASK_CONTEXT | TYPE_MASK_CONSTRUCTED)) {
+                reader.readStartExplicitTag((byte) (TYPE_MASK_CONTEXT | TYPE_MASK_CONSTRUCTED));
+
+                x509Version = reader.readInteger(UNIVERSAL_INTEGER_TYPE);
+                if (x509Version < 0 || x509Version > 2) {
+                    // invalid Version specified
+                    invalidReason.append(ERR_SYNTAX_CERTIFICATE_INVALID_VERSION.get(x509Version));
+                    return false;
+                }
+
+                if (x509Version == 0) {
+                    // DEFAULT values shall not be included in DER encoded
+                    // SEQUENCE (X.690, 11.5)
+                    invalidReason.append(ERR_SYNTAX_CERTIFICATE_INVALID_DER.get());
+                    return false;
+                }
+
+                reader.readEndExplicitTag();
+            }
+
+            // serialNumber
+            reader.skipElement(UNIVERSAL_INTEGER_TYPE);
+
+            // signature AlgorithmIdentifier
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // issuer name (SEQUENCE as of X.501, 9.2)
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // validity (SEQUENCE)
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // subject name (SEQUENCE as of X.501, 9.2)
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // SubjectPublicKeyInfo (SEQUENCE)
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // OPTIONAL issuerUniqueIdentifier
+            if (reader.hasNextElement() && reader.peekType() == (TYPE_MASK_CONTEXT + 1)) {
+                if (x509Version < 1) {
+                    // only valid in v2 and v3
+                    invalidReason.append(ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V23.get("issuerUniqueIdentifier"));
+                    return false;
+                }
+                reader.skipElement();
+            }
+
+            // OPTIONAL subjectUniqueIdentifier
+            if (reader.hasNextElement() && reader.peekType() == (TYPE_MASK_CONTEXT + 2)) {
+                if (x509Version < 1) {
+                    // only valid in v2 and v3
+                    invalidReason.append(ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V23.get("subjectUniqueIdentifier"));
+                    return false;
+                }
+                reader.skipElement();
+            }
+
+            // OPTIONAL extensions
+            if (reader.hasNextElement() && reader.peekType() == ((TYPE_MASK_CONTEXT | TYPE_MASK_CONSTRUCTED) + 3)) {
+                if (x509Version < 2) {
+                    // only valid in v3
+                    invalidReason.append(ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V3.get("extensions"));
+                    return false;
+                }
+
+                reader.readStartExplicitTag((byte) ((TYPE_MASK_CONTEXT | TYPE_MASK_CONSTRUCTED) + 3));
+
+                reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+                reader.readEndExplicitTag();
+            }
+
+            // There should not be any further ASN.1 elements within this SEQUENCE
+            if (reader.hasNextElement()) {
+                invalidReason.append(ERR_SYNTAX_CERTIFICATE_NO_ELEMENT_EXPECTED.get());
+                return false;
+            }
+            reader.readEndSequence(); // End CertificateContent SEQUENCE
+
+            // AlgorithmIdentifier SEQUENCE
+            reader.skipElement(UNIVERSAL_SEQUENCE_TYPE);
+
+            // ENCRYPTED HASH BIT STRING
+            reader.skipElement(UNIVERSAL_BIT_STRING_TYPE);
+
+            // There should not be any further ASN.1 elements within this SEQUENCE
+            if (reader.hasNextElement()) {
+                invalidReason.append(ERR_SYNTAX_CERTIFICATE_NO_ELEMENT_EXPECTED.get());
+                return false;
+            }
+            reader.readEndSequence(); // End Certificate SEQUENCE
+
+            // There should not be any further ASN.1 elements
+            if (reader.hasNextElement()) {
+                invalidReason.append(ERR_SYNTAX_CERTIFICATE_NO_ELEMENT_EXPECTED.get());
+                return false;
+            }
+            // End of the certificate
+        } catch (DecodeException de) {
+            invalidReason.append(ERR_SYNTAX_CERTIFICATE_NOTVALID.get(de));
+            return false;
+        } catch (IOException e) {
+            invalidReason.append(StaticUtils.getExceptionMessage(e));
+            return false;
+        }
+
+        // The basic structure of the value is an X.509 certificate
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CollationMatchingRulesImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CollationMatchingRulesImpl.java
new file mode 100644
index 0000000..83daaf5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CollationMatchingRulesImpl.java
@@ -0,0 +1,333 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * Implementations of collation matching rules. Each matching rule is created
+ * from a locale (eg, "en-US" or "en-GB").
+ * <p>
+ * The PRIMARY strength is used for collation, which means that only primary
+ * differences are considered significant for comparison. It usually means that
+ * spaces, case, and accent are not significant, although it is language dependant.
+ * <p>
+ * For a given locale, two indexes are used: a shared one (for equality and
+ * ordering rules) and a substring one (for substring rule).
+ */
+final class CollationMatchingRulesImpl {
+    private static final String INDEX_ID_SHARED = "shared";
+    private static final String INDEX_ID_SUBSTRING = "substring";
+
+    private CollationMatchingRulesImpl() {
+        // This class is not instanciable
+    }
+
+    /**
+     * Creates a collation equality matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationEqualityMatchingRule(Locale locale) {
+        return new CollationEqualityMatchingRuleImpl(locale);
+    }
+
+    /**
+     * Creates a collation substring matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationSubstringMatchingRule(Locale locale) {
+        return new CollationSubstringMatchingRuleImpl(locale);
+    }
+
+    /**
+     * Creates a collation less than matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationLessThanMatchingRule(Locale locale) {
+        return new CollationLessThanMatchingRuleImpl(locale);
+    }
+
+    /**
+     * Creates a collation less than or equal matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationLessThanOrEqualMatchingRule(Locale locale) {
+        return new CollationLessThanOrEqualToMatchingRuleImpl(locale);
+    }
+
+    /**
+     * Creates a collation greater than matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationGreaterThanMatchingRule(Locale locale) {
+        return new CollationGreaterThanMatchingRuleImpl(locale);
+    }
+
+    /**
+     * Creates a collation greater than or equal matching Rule.
+     *
+     * @param locale
+     *            the locale to use for this rule
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl collationGreaterThanOrEqualToMatchingRule(Locale locale) {
+        return new CollationGreaterThanOrEqualToMatchingRuleImpl(locale);
+    }
+
+    /** Defines the base for collation matching rules. */
+    private static abstract class AbstractCollationMatchingRuleImpl extends AbstractMatchingRuleImpl {
+        private final Locale locale;
+        final Collator collator;
+        final String indexName;
+        final Indexer indexer;
+
+        /**
+         * Creates the collation matching rule with the provided locale.
+         *
+         * @param locale
+         *            Locale associated with this rule.
+         */
+        AbstractCollationMatchingRuleImpl(Locale locale) {
+            this.locale = locale;
+            this.collator = createCollator(locale);
+            this.indexName = getPrefixIndexName() + "." + INDEX_ID_SHARED;
+            this.indexer = new DefaultIndexer(indexName);
+        }
+
+        private Collator createCollator(Locale locale) {
+            Collator collator = Collator.getInstance(locale);
+            collator.setStrength(Collator.PRIMARY);
+            collator.setDecomposition(Collator.FULL_DECOMPOSITION);
+            return collator;
+        }
+
+        /**
+         * Returns the prefix name of the index database for this matching rule. An
+         * index name for this rule will be based upon the Locale. This will
+         * ensure that multiple collation matching rules corresponding to the
+         * same Locale can share the same index database.
+         *
+         * @return The prefix name of the index for this matching rule.
+         */
+        String getPrefixIndexName() {
+            String language = locale.getLanguage();
+            String country = locale.getCountry();
+            String variant = locale.getVariant();
+
+            StringBuilder builder = new StringBuilder(language);
+            if (country != null && country.length() > 0) {
+                builder.append("_");
+                builder.append(country);
+            }
+            if (variant != null && variant.length() > 0) {
+                builder.append("_");
+                builder.append(variant);
+            }
+            return builder.toString();
+        }
+
+        @Override
+        public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+            return Collections.singletonList(indexer);
+        }
+
+        @Override
+        public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+                throws DecodeException {
+            try {
+                final byte[] byteArray = collator.getCollationKey(value.toString()).toByteArray();
+                // Last 4 bytes are 0s when collator strength is set to PRIMARY, so skip them
+                return ByteString.wrap(byteArray).subSequence(0, byteArray.length - 4);
+            } catch (final LocalizedIllegalArgumentException e) {
+                throw DecodeException.error(e.getMessageObject());
+            }
+        }
+    }
+
+    /** Defines the collation equality matching rule. */
+    private static final class CollationEqualityMatchingRuleImpl extends AbstractCollationMatchingRuleImpl {
+        /**
+         * Creates the matching rule with the provided locale.
+         *
+         * @param locale
+         *          Locale associated with this rule.
+         */
+        CollationEqualityMatchingRuleImpl(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue)
+                throws DecodeException {
+            return named(indexName, normalizeAttributeValue(schema, assertionValue));
+        }
+    }
+
+    /** Defines the collation substring matching rule. */
+    private static final class CollationSubstringMatchingRuleImpl extends AbstractCollationMatchingRuleImpl {
+        private final AbstractSubstringMatchingRuleImpl substringMatchingRule;
+
+        /**
+         * Creates the matching rule with the provided locale.
+         *
+         * @param locale
+         *          Locale associated with this rule.
+         */
+        CollationSubstringMatchingRuleImpl(Locale locale) {
+            super(locale);
+            substringMatchingRule = new AbstractSubstringMatchingRuleImpl(
+                    getPrefixIndexName() + "." + INDEX_ID_SUBSTRING, indexName) {
+                @Override
+                public ByteString normalizeAttributeValue(Schema schema, ByteSequence value)
+                        throws DecodeException {
+                    return CollationSubstringMatchingRuleImpl.this.normalizeAttributeValue(schema, value);
+                }
+            };
+        }
+
+        @Override
+        public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
+            return substringMatchingRule.getAssertion(schema, assertionValue);
+        }
+
+        @Override
+        public Assertion getSubstringAssertion(Schema schema, ByteSequence subInitial,
+                List<? extends ByteSequence> subAnyElements, ByteSequence subFinal) throws DecodeException {
+            return substringMatchingRule.getSubstringAssertion(schema, subInitial, subAnyElements, subFinal);
+        }
+
+        @Override
+        public final Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+            final Collection<Indexer> indexers = new ArrayList<>(substringMatchingRule.createIndexers(options));
+            indexers.add(indexer);
+            return indexers;
+        }
+    }
+
+    /** Defines the collation ordering matching rule. */
+    private static abstract class CollationOrderingMatchingRuleImpl extends AbstractCollationMatchingRuleImpl {
+        final AbstractOrderingMatchingRuleImpl orderingMatchingRule;
+
+        /**
+         * Creates the matching rule with the provided locale.
+         *
+         * @param locale
+         *          Locale associated with this rule.
+         */
+        CollationOrderingMatchingRuleImpl(Locale locale) {
+            super(locale);
+
+            orderingMatchingRule = new AbstractOrderingMatchingRuleImpl(indexName) {
+                @Override
+                public ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException {
+                    return CollationOrderingMatchingRuleImpl.this.normalizeAttributeValue(schema, value);
+                }
+            };
+        }
+    }
+
+    /** Defines the collation less than matching rule. */
+    private static final class CollationLessThanMatchingRuleImpl extends CollationOrderingMatchingRuleImpl {
+        CollationLessThanMatchingRuleImpl(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Assertion getAssertion(Schema schema, ByteSequence assertionValue) throws DecodeException {
+            return orderingMatchingRule.getAssertion(schema, assertionValue);
+        }
+    }
+
+    /** Defines the collation less than or equal matching rule. */
+    private static final class CollationLessThanOrEqualToMatchingRuleImpl extends CollationOrderingMatchingRuleImpl {
+        CollationLessThanOrEqualToMatchingRuleImpl(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Assertion getAssertion(Schema schema, ByteSequence assertionValue) throws DecodeException {
+            return orderingMatchingRule.getLessOrEqualAssertion(schema, assertionValue);
+        }
+    }
+
+    /** Defines the collation greater than matching rule. */
+    private static final class CollationGreaterThanMatchingRuleImpl extends CollationOrderingMatchingRuleImpl {
+        CollationGreaterThanMatchingRuleImpl(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Assertion getAssertion(Schema schema, ByteSequence assertionValue)
+                throws DecodeException {
+            final ByteString normAssertion = normalizeAttributeValue(schema, assertionValue);
+            return new Assertion() {
+                @Override
+                public ConditionResult matches(final ByteSequence attributeValue) {
+                    return ConditionResult.valueOf(attributeValue.compareTo(normAssertion) > 0);
+                }
+
+                @Override
+                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                    return factory.createRangeMatchQuery(indexName, normAssertion,
+                            ByteString.empty(), false, false);
+                }
+            };
+        }
+    }
+
+    /** Defines the collation greater than or equal matching rule. */
+    private static final class CollationGreaterThanOrEqualToMatchingRuleImpl
+        extends CollationOrderingMatchingRuleImpl {
+        CollationGreaterThanOrEqualToMatchingRuleImpl(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Assertion getAssertion(Schema schema, ByteSequence assertionValue) throws DecodeException {
+            return orderingMatchingRule.getGreaterOrEqualAssertion(schema, assertionValue);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ConflictingSchemaElementException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ConflictingSchemaElementException.java
new file mode 100644
index 0000000..a26ca4c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ConflictingSchemaElementException.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+/**
+ * Thrown when addition of a schema element to a schema builder fails because
+ * the OID of the schema element conflicts with an existing schema element and
+ * the caller explicitly requested not to override existing schema elements.
+ */
+@SuppressWarnings("serial")
+public class ConflictingSchemaElementException extends LocalizedIllegalArgumentException {
+    /**
+     * Creates a new conflicting schema element exception with the provided
+     * message.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     */
+    public ConflictingSchemaElementException(final LocalizableMessage message) {
+        super(message);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java
new file mode 100644
index 0000000..663cf3e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java
@@ -0,0 +1,2362 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+
+// DON'T EDIT THIS FILE!
+// It is automatically generated using GenerateCoreSchema class.
+
+/**
+ * The OpenDJ SDK core schema contains standard LDAP RFC schema elements. These include:
+ * <ul>
+ * <li><a href="http://tools.ietf.org/html/rfc4512">RFC 4512 -
+ * Lightweight Directory Access Protocol (LDAP): Directory Information
+ * Models </a>
+ * <li><a href="http://tools.ietf.org/html/rfc4517">RFC 4517 -
+ * Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching
+ * Rules </a>
+ * <li><a href="http://tools.ietf.org/html/rfc4519">RFC 4519 -
+ * Lightweight Directory Access Protocol (LDAP): Schema for User
+ * Applications </a>
+ * <li><a href="http://tools.ietf.org/html/rfc4530">RFC 4530 -
+ * Lightweight Directory Access Protocol (LDAP): entryUUID Operational
+ * Attribute </a>
+ * <li><a href="http://tools.ietf.org/html/rfc3045">RFC 3045 - Storing
+ * Vendor Information in the LDAP Root DSE </a>
+ * <li><a href="http://tools.ietf.org/html/rfc3112">RFC 3112 - LDAP
+ * Authentication Password Schema </a>
+ * </ul>
+ * <p>
+ * The core schema is non-strict: attempts to retrieve
+ * non-existent Attribute Types will return a temporary
+ * Attribute Type having the Octet String syntax.
+ */
+public final class CoreSchema {
+    // Core Syntaxes
+    private static final Syntax ATTRIBUTE_TYPE_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.3");
+    private static final Syntax AUTHENTICATION_PASSWORD_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.4203.1.1.2");
+    private static final Syntax BINARY_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.5");
+    private static final Syntax BIT_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.6");
+    private static final Syntax BOOLEAN_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.7");
+    private static final Syntax CERTIFICATE_LIST_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.9");
+    private static final Syntax CERTIFICATE_PAIR_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.10");
+    private static final Syntax CERTIFICATE_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.8");
+    private static final Syntax COUNTRY_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.11");
+    private static final Syntax DELIVERY_METHOD_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.14");
+    private static final Syntax DIRECTORY_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.15");
+    private static final Syntax DIT_CONTENT_RULE_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.16");
+    private static final Syntax DIT_STRUCTURE_RULE_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.17");
+    private static final Syntax DN_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.12");
+    private static final Syntax ENHANCED_GUIDE_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.21");
+    private static final Syntax FACSIMILE_TELEPHONE_NUMBER_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.22");
+    private static final Syntax FAX_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.23");
+    private static final Syntax GENERALIZED_TIME_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.24");
+    private static final Syntax GUIDE_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.25");
+    private static final Syntax IA5_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.26");
+    private static final Syntax INTEGER_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.27");
+    private static final Syntax JPEG_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.28");
+    private static final Syntax LDAP_SYNTAX_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.54");
+    private static final Syntax MATCHING_RULE_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.30");
+    private static final Syntax MATCHING_RULE_USE_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.31");
+    private static final Syntax NAME_AND_OPTIONAL_UID_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.34");
+    private static final Syntax NAME_FORM_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.35");
+    private static final Syntax NUMERIC_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.36");
+    private static final Syntax OBJECT_CLASS_DESCRIPTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.37");
+    private static final Syntax OCTET_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.40");
+    private static final Syntax OID_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.38");
+    private static final Syntax OTHER_MAILBOX_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.39");
+    private static final Syntax POSTAL_ADDRESS_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.41");
+    private static final Syntax PRESENTATION_ADDRESS_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.43");
+    private static final Syntax PRINTABLE_STRING_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.44");
+    private static final Syntax PROTOCOL_INFORMATION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.42");
+    private static final Syntax SUBSTRING_ASSERTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.58");
+    private static final Syntax SUPPORTED_ALGORITHM_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.49");
+    private static final Syntax TELEPHONE_NUMBER_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.50");
+    private static final Syntax TELETEX_TERMINAL_IDENTIFIER_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.51");
+    private static final Syntax TELEX_NUMBER_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.52");
+    private static final Syntax UTC_TIME_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.4.1.1466.115.121.1.53");
+    private static final Syntax UUID_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.1.16.1");
+    private static final Syntax X509_CERTIFICATE_EXACT_ASSERTION_SYNTAX =
+        CoreSchemaImpl.getInstance().getSyntax("1.3.6.1.1.15.1");
+
+    // Core Matching Rules
+    private static final MatchingRule AUTH_PASSWORD_EXACT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.4.1.4203.1.2.2");
+    private static final MatchingRule BIT_STRING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.16");
+    private static final MatchingRule BOOLEAN_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.13");
+    private static final MatchingRule CASE_EXACT_IA5_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.4.1.1466.109.114.1");
+    private static final MatchingRule CASE_EXACT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.5");
+    private static final MatchingRule CASE_EXACT_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.6");
+    private static final MatchingRule CASE_EXACT_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.7");
+    private static final MatchingRule CASE_IGNORE_IA5_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.4.1.1466.109.114.2");
+    private static final MatchingRule CASE_IGNORE_IA5_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.4.1.1466.109.114.3");
+    private static final MatchingRule CASE_IGNORE_LIST_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.11");
+    private static final MatchingRule CASE_IGNORE_LIST_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.12");
+    private static final MatchingRule CASE_IGNORE_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.2");
+    private static final MatchingRule CASE_IGNORE_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.3");
+    private static final MatchingRule CASE_IGNORE_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.4");
+    private static final MatchingRule CERTIFICATE_EXACT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.34");
+    private static final MatchingRule DIRECTORY_STRING_FIRST_COMPONENT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.31");
+    private static final MatchingRule DISTINGUISHED_NAME_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.1");
+    private static final MatchingRule GENERALIZED_TIME_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.27");
+    private static final MatchingRule GENERALIZED_TIME_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.28");
+    private static final MatchingRule INTEGER_FIRST_COMPONENT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.29");
+    private static final MatchingRule INTEGER_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.14");
+    private static final MatchingRule INTEGER_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.15");
+    private static final MatchingRule KEYWORD_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.33");
+    private static final MatchingRule NUMERIC_STRING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.8");
+    private static final MatchingRule NUMERIC_STRING_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.9");
+    private static final MatchingRule NUMERIC_STRING_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.10");
+    private static final MatchingRule OBJECT_IDENTIFIER_FIRST_COMPONENT_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.30");
+    private static final MatchingRule OBJECT_IDENTIFIER_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.0");
+    private static final MatchingRule OCTET_STRING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.17");
+    private static final MatchingRule OCTET_STRING_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.18");
+    private static final MatchingRule OCTET_STRING_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.19");
+    private static final MatchingRule PRESENTATION_ADDRESS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.22");
+    private static final MatchingRule PROTOCOL_INFORMATION_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.24");
+    private static final MatchingRule TELEPHONE_NUMBER_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.20");
+    private static final MatchingRule TELEPHONE_NUMBER_SUBSTRINGS_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.21");
+    private static final MatchingRule UNIQUE_MEMBER_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.23");
+    private static final MatchingRule UUID_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.1.16.2");
+    private static final MatchingRule UUID_ORDERING_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("1.3.6.1.1.16.3");
+    private static final MatchingRule WORD_MATCHING_RULE =
+        CoreSchemaImpl.getInstance().getMatchingRule("2.5.13.32");
+
+    // Core Attribute Types
+    private static final AttributeType ALIASED_OBJECT_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.1");
+    private static final AttributeType ALT_SERVER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.6");
+    private static final AttributeType ATTRIBUTE_TYPES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.5");
+    private static final AttributeType AUTHORITY_REVOCATION_LIST_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.38");
+    private static final AttributeType AUTH_PASSWORD_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.4203.1.3.4");
+    private static final AttributeType BUSINESS_CATEGORY_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.15");
+    private static final AttributeType CERTIFICATE_REVOCATION_LIST_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.39");
+    private static final AttributeType CN_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.3");
+    private static final AttributeType CREATE_TIMESTAMP_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.1");
+    private static final AttributeType CREATORS_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.3");
+    private static final AttributeType CROSS_CERTIFICATE_PAIR_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.40");
+    private static final AttributeType C_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.6");
+    private static final AttributeType C_A_CERTIFICATE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.37");
+    private static final AttributeType DC_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("0.9.2342.19200300.100.1.25");
+    private static final AttributeType DELTA_REVOCATION_LIST_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.53");
+    private static final AttributeType DESCRIPTION_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.13");
+    private static final AttributeType DESTINATION_INDICATOR_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.27");
+    private static final AttributeType DISTINGUISHED_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.49");
+    private static final AttributeType DIT_CONTENT_RULES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.2");
+    private static final AttributeType DIT_STRUCTURE_RULES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.1");
+    private static final AttributeType DN_QUALIFIER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.46");
+    private static final AttributeType ENHANCED_SEARCH_GUIDE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.47");
+    private static final AttributeType ENTRY_DN_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.1.20");
+    private static final AttributeType ENTRY_UUID_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.1.16.4");
+    private static final AttributeType FACSIMILE_TELEPHONE_NUMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.23");
+    private static final AttributeType FULL_VENDOR_VERSION_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.36733.2.1.1.141");
+    private static final AttributeType GENERATION_QUALIFIER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.44");
+    private static final AttributeType GIVEN_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.42");
+    private static final AttributeType GOVERNING_STRUCTURE_RULE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.10");
+    private static final AttributeType HOUSE_IDENTIFIER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.51");
+    private static final AttributeType INITIALS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.43");
+    private static final AttributeType INTERNATIONAL_ISDN_NUMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.25");
+    private static final AttributeType LDAP_SYNTAXES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.16");
+    private static final AttributeType L_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.7");
+    private static final AttributeType MATCHING_RULES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.4");
+    private static final AttributeType MATCHING_RULE_USE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.8");
+    private static final AttributeType MEMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.31");
+    private static final AttributeType MODIFIERS_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.4");
+    private static final AttributeType MODIFY_TIMESTAMP_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.2");
+    private static final AttributeType NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.41");
+    private static final AttributeType NAME_FORMS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.7");
+    private static final AttributeType NAMING_CONTEXTS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.5");
+    private static final AttributeType OBJECT_CLASSES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.6");
+    private static final AttributeType OBJECT_CLASS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.0");
+    private static final AttributeType OU_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.11");
+    private static final AttributeType OWNER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.32");
+    private static final AttributeType O_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.10");
+    private static final AttributeType PHYSICAL_DELIVERY_OFFICE_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.19");
+    private static final AttributeType POSTAL_ADDRESS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.16");
+    private static final AttributeType POSTAL_CODE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.17");
+    private static final AttributeType POST_OFFICE_BOX_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.18");
+    private static final AttributeType PREFERRED_DELIVERY_METHOD_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.28");
+    private static final AttributeType REGISTERED_ADDRESS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.26");
+    private static final AttributeType ROLE_OCCUPANT_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.33");
+    private static final AttributeType SEARCH_GUIDE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.14");
+    private static final AttributeType SEE_ALSO_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.34");
+    private static final AttributeType SERIAL_NUMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.5");
+    private static final AttributeType SN_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.4");
+    private static final AttributeType STREET_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.9");
+    private static final AttributeType STRUCTURAL_OBJECT_CLASS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.9");
+    private static final AttributeType ST_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.8");
+    private static final AttributeType SUBSCHEMA_SUBENTRY_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.10");
+    private static final AttributeType SUPPORTED_ALGORITHMS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.52");
+    private static final AttributeType SUPPORTED_AUTH_PASSWORD_SCHEMES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.4203.1.3.3");
+    private static final AttributeType SUPPORTED_CONTROL_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.13");
+    private static final AttributeType SUPPORTED_EXTENSION_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.7");
+    private static final AttributeType SUPPORTED_FEATURES_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.4203.1.3.5");
+    private static final AttributeType SUPPORTED_LDAP_VERSION_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.15");
+    private static final AttributeType SUPPORTED_SASL_MECHANISMS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.14");
+    private static final AttributeType TELEPHONE_NUMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.20");
+    private static final AttributeType TELETEX_TERMINAL_IDENTIFIER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.22");
+    private static final AttributeType TELEX_NUMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.21");
+    private static final AttributeType TITLE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.12");
+    private static final AttributeType UID_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("0.9.2342.19200300.100.1.1");
+    private static final AttributeType UNIQUE_MEMBER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.50");
+    private static final AttributeType USER_CERTIFICATE_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.36");
+    private static final AttributeType USER_PASSWORD_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.35");
+    private static final AttributeType VENDOR_NAME_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.1.4");
+    private static final AttributeType VENDOR_VERSION_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.1.5");
+    private static final AttributeType X121_ADDRESS_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.24");
+    private static final AttributeType X500_UNIQUE_IDENTIFIER_ATTRIBUTE_TYPE =
+        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.45");
+
+    // Core Object Classes
+    private static final ObjectClass ALIAS_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.1");
+    private static final ObjectClass APPLICATION_PROCESS_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.11");
+    private static final ObjectClass AUTH_PASSWORD_OBJECT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("1.3.6.1.4.1.4203.1.4.7");
+    private static final ObjectClass CERTIFICATION_AUTHORITY_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.16");
+    private static final ObjectClass CERTIFICATION_AUTHORITY_V2_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.16.2");
+    private static final ObjectClass COUNTRY_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.2");
+    private static final ObjectClass C_RL_DISTRIBUTION_POINT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.19");
+    private static final ObjectClass DC_OBJECT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("1.3.6.1.4.1.1466.344");
+    private static final ObjectClass DELTA_CRL_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.23");
+    private static final ObjectClass DEVICE_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.14");
+    private static final ObjectClass EXTENSIBLE_OBJECT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("1.3.6.1.4.1.1466.101.120.111");
+    private static final ObjectClass GROUP_OF_NAMES_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.9");
+    private static final ObjectClass GROUP_OF_UNIQUE_NAMES_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.17");
+    private static final ObjectClass LOCALITY_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.3");
+    private static final ObjectClass ORGANIZATIONAL_PERSON_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.7");
+    private static final ObjectClass ORGANIZATIONAL_ROLE_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.8");
+    private static final ObjectClass ORGANIZATIONAL_UNIT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.5");
+    private static final ObjectClass ORGANIZATION_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.4");
+    private static final ObjectClass PERSON_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.6");
+    private static final ObjectClass PKI_CA_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.22");
+    private static final ObjectClass PKI_USER_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.21");
+    private static final ObjectClass RESIDENTIAL_PERSON_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.10");
+    private static final ObjectClass STRONG_AUTHENTICATION_USER_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.15");
+    private static final ObjectClass SUBSCHEMA_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.20.1");
+    private static final ObjectClass TOP_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.0");
+    private static final ObjectClass UID_OBJECT_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("1.3.6.1.1.3.1");
+    private static final ObjectClass USER_SECURITY_INFORMATION_OBJECT_CLASS =
+        CoreSchemaImpl.getInstance().getObjectClass("2.5.6.18");
+
+    // Prevent instantiation
+    private CoreSchema() {
+      // Nothing to do.
+    }
+
+    /**
+     * Returns a reference to the singleton core schema.
+     *
+     * @return The core schema.
+     */
+    public static Schema getInstance() {
+        return CoreSchemaImpl.getInstance();
+    }
+
+    /**
+     * Returns a reference to the {@code Attribute Type Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.3}.
+     *
+     * @return A reference to the {@code Attribute Type Description Syntax}.
+     */
+    public static Syntax getAttributeTypeDescriptionSyntax() {
+        return ATTRIBUTE_TYPE_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Authentication Password Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.1.2}.
+     *
+     * @return A reference to the {@code Authentication Password Syntax}.
+     */
+    public static Syntax getAuthenticationPasswordSyntax() {
+        return AUTHENTICATION_PASSWORD_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Binary Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.5}.
+     *
+     * @return A reference to the {@code Binary Syntax}.
+     */
+    public static Syntax getBinarySyntax() {
+        return BINARY_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Bit String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.6}.
+     *
+     * @return A reference to the {@code Bit String Syntax}.
+     */
+    public static Syntax getBitStringSyntax() {
+        return BIT_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Boolean Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.7}.
+     *
+     * @return A reference to the {@code Boolean Syntax}.
+     */
+    public static Syntax getBooleanSyntax() {
+        return BOOLEAN_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Certificate List Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.9}.
+     *
+     * @return A reference to the {@code Certificate List Syntax}.
+     */
+    public static Syntax getCertificateListSyntax() {
+        return CERTIFICATE_LIST_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Certificate Pair Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.10}.
+     *
+     * @return A reference to the {@code Certificate Pair Syntax}.
+     */
+    public static Syntax getCertificatePairSyntax() {
+        return CERTIFICATE_PAIR_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Certificate Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.8}.
+     *
+     * @return A reference to the {@code Certificate Syntax}.
+     */
+    public static Syntax getCertificateSyntax() {
+        return CERTIFICATE_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Country String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.11}.
+     *
+     * @return A reference to the {@code Country String Syntax}.
+     */
+    public static Syntax getCountryStringSyntax() {
+        return COUNTRY_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Delivery Method Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.14}.
+     *
+     * @return A reference to the {@code Delivery Method Syntax}.
+     */
+    public static Syntax getDeliveryMethodSyntax() {
+        return DELIVERY_METHOD_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Directory String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.15}.
+     *
+     * @return A reference to the {@code Directory String Syntax}.
+     */
+    public static Syntax getDirectoryStringSyntax() {
+        return DIRECTORY_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code DIT Content Rule Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.16}.
+     *
+     * @return A reference to the {@code DIT Content Rule Description Syntax}.
+     */
+    public static Syntax getDITContentRuleDescriptionSyntax() {
+        return DIT_CONTENT_RULE_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code DIT Structure Rule Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.17}.
+     *
+     * @return A reference to the {@code DIT Structure Rule Description Syntax}.
+     */
+    public static Syntax getDITStructureRuleDescriptionSyntax() {
+        return DIT_STRUCTURE_RULE_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code DN Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.12}.
+     *
+     * @return A reference to the {@code DN Syntax}.
+     */
+    public static Syntax getDNSyntax() {
+        return DN_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Enhanced Guide Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.21}.
+     *
+     * @return A reference to the {@code Enhanced Guide Syntax}.
+     */
+    public static Syntax getEnhancedGuideSyntax() {
+        return ENHANCED_GUIDE_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Facsimile Telephone Number Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.22}.
+     *
+     * @return A reference to the {@code Facsimile Telephone Number Syntax}.
+     */
+    public static Syntax getFacsimileTelephoneNumberSyntax() {
+        return FACSIMILE_TELEPHONE_NUMBER_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Fax Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.23}.
+     *
+     * @return A reference to the {@code Fax Syntax}.
+     */
+    public static Syntax getFaxSyntax() {
+        return FAX_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Generalized Time Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.24}.
+     *
+     * @return A reference to the {@code Generalized Time Syntax}.
+     */
+    public static Syntax getGeneralizedTimeSyntax() {
+        return GENERALIZED_TIME_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Guide Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.25}.
+     *
+     * @return A reference to the {@code Guide Syntax}.
+     */
+    public static Syntax getGuideSyntax() {
+        return GUIDE_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code IA5 String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.26}.
+     *
+     * @return A reference to the {@code IA5 String Syntax}.
+     */
+    public static Syntax getIA5StringSyntax() {
+        return IA5_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Integer Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.27}.
+     *
+     * @return A reference to the {@code Integer Syntax}.
+     */
+    public static Syntax getIntegerSyntax() {
+        return INTEGER_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code JPEG Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.28}.
+     *
+     * @return A reference to the {@code JPEG Syntax}.
+     */
+    public static Syntax getJPEGSyntax() {
+        return JPEG_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code LDAP Syntax Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.54}.
+     *
+     * @return A reference to the {@code LDAP Syntax Description Syntax}.
+     */
+    public static Syntax getLDAPSyntaxDescriptionSyntax() {
+        return LDAP_SYNTAX_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Matching Rule Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.30}.
+     *
+     * @return A reference to the {@code Matching Rule Description Syntax}.
+     */
+    public static Syntax getMatchingRuleDescriptionSyntax() {
+        return MATCHING_RULE_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Matching Rule Use Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.31}.
+     *
+     * @return A reference to the {@code Matching Rule Use Description Syntax}.
+     */
+    public static Syntax getMatchingRuleUseDescriptionSyntax() {
+        return MATCHING_RULE_USE_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Name and Optional UID Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.34}.
+     *
+     * @return A reference to the {@code Name and Optional UID Syntax}.
+     */
+    public static Syntax getNameAndOptionalUIDSyntax() {
+        return NAME_AND_OPTIONAL_UID_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Name Form Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.35}.
+     *
+     * @return A reference to the {@code Name Form Description Syntax}.
+     */
+    public static Syntax getNameFormDescriptionSyntax() {
+        return NAME_FORM_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Numeric String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.36}.
+     *
+     * @return A reference to the {@code Numeric String Syntax}.
+     */
+    public static Syntax getNumericStringSyntax() {
+        return NUMERIC_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Object Class Description Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.37}.
+     *
+     * @return A reference to the {@code Object Class Description Syntax}.
+     */
+    public static Syntax getObjectClassDescriptionSyntax() {
+        return OBJECT_CLASS_DESCRIPTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Octet String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.40}.
+     *
+     * @return A reference to the {@code Octet String Syntax}.
+     */
+    public static Syntax getOctetStringSyntax() {
+        return OCTET_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code OID Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.38}.
+     *
+     * @return A reference to the {@code OID Syntax}.
+     */
+    public static Syntax getOIDSyntax() {
+        return OID_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Other Mailbox Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.39}.
+     *
+     * @return A reference to the {@code Other Mailbox Syntax}.
+     */
+    public static Syntax getOtherMailboxSyntax() {
+        return OTHER_MAILBOX_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Postal Address Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.41}.
+     *
+     * @return A reference to the {@code Postal Address Syntax}.
+     */
+    public static Syntax getPostalAddressSyntax() {
+        return POSTAL_ADDRESS_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Presentation Address Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.43}.
+     *
+     * @return A reference to the {@code Presentation Address Syntax}.
+     */
+    public static Syntax getPresentationAddressSyntax() {
+        return PRESENTATION_ADDRESS_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Printable String Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.44}.
+     *
+     * @return A reference to the {@code Printable String Syntax}.
+     */
+    public static Syntax getPrintableStringSyntax() {
+        return PRINTABLE_STRING_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Protocol Information Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.42}.
+     *
+     * @return A reference to the {@code Protocol Information Syntax}.
+     */
+    public static Syntax getProtocolInformationSyntax() {
+        return PROTOCOL_INFORMATION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Substring Assertion Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.58}.
+     *
+     * @return A reference to the {@code Substring Assertion Syntax}.
+     */
+    public static Syntax getSubstringAssertionSyntax() {
+        return SUBSTRING_ASSERTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Supported Algorithm Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.49}.
+     *
+     * @return A reference to the {@code Supported Algorithm Syntax}.
+     */
+    public static Syntax getSupportedAlgorithmSyntax() {
+        return SUPPORTED_ALGORITHM_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Telephone Number Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.50}.
+     *
+     * @return A reference to the {@code Telephone Number Syntax}.
+     */
+    public static Syntax getTelephoneNumberSyntax() {
+        return TELEPHONE_NUMBER_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Teletex Terminal Identifier Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.51}.
+     *
+     * @return A reference to the {@code Teletex Terminal Identifier Syntax}.
+     */
+    public static Syntax getTeletexTerminalIdentifierSyntax() {
+        return TELETEX_TERMINAL_IDENTIFIER_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code Telex Number Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.52}.
+     *
+     * @return A reference to the {@code Telex Number Syntax}.
+     */
+    public static Syntax getTelexNumberSyntax() {
+        return TELEX_NUMBER_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code UTC Time Syntax}
+     * which has the OID {@code 1.3.6.1.4.1.1466.115.121.1.53}.
+     *
+     * @return A reference to the {@code UTC Time Syntax}.
+     */
+    public static Syntax getUTCTimeSyntax() {
+        return UTC_TIME_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code UUID Syntax}
+     * which has the OID {@code 1.3.6.1.1.16.1}.
+     *
+     * @return A reference to the {@code UUID Syntax}.
+     */
+    public static Syntax getUUIDSyntax() {
+        return UUID_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code X.509 Certificate Exact Assertion Syntax}
+     * which has the OID {@code 1.3.6.1.1.15.1}.
+     *
+     * @return A reference to the {@code X.509 Certificate Exact Assertion Syntax}.
+     */
+    public static Syntax getX509CertificateExactAssertionSyntax() {
+        return X509_CERTIFICATE_EXACT_ASSERTION_SYNTAX;
+    }
+
+    /**
+     * Returns a reference to the {@code authPasswordExactMatch} Matching Rule
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.2.2}.
+     *
+     * @return A reference to the {@code authPasswordExactMatch} Matching Rule.
+     */
+    public static MatchingRule getAuthPasswordExactMatchingRule() {
+        return AUTH_PASSWORD_EXACT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code bitStringMatch} Matching Rule
+     * which has the OID {@code 2.5.13.16}.
+     *
+     * @return A reference to the {@code bitStringMatch} Matching Rule.
+     */
+    public static MatchingRule getBitStringMatchingRule() {
+        return BIT_STRING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code booleanMatch} Matching Rule
+     * which has the OID {@code 2.5.13.13}.
+     *
+     * @return A reference to the {@code booleanMatch} Matching Rule.
+     */
+    public static MatchingRule getBooleanMatchingRule() {
+        return BOOLEAN_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseExactIA5Match} Matching Rule
+     * which has the OID {@code 1.3.6.1.4.1.1466.109.114.1}.
+     *
+     * @return A reference to the {@code caseExactIA5Match} Matching Rule.
+     */
+    public static MatchingRule getCaseExactIA5MatchingRule() {
+        return CASE_EXACT_IA5_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseExactMatch} Matching Rule
+     * which has the OID {@code 2.5.13.5}.
+     *
+     * @return A reference to the {@code caseExactMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseExactMatchingRule() {
+        return CASE_EXACT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseExactOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.6}.
+     *
+     * @return A reference to the {@code caseExactOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseExactOrderingMatchingRule() {
+        return CASE_EXACT_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseExactSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.7}.
+     *
+     * @return A reference to the {@code caseExactSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseExactSubstringsMatchingRule() {
+        return CASE_EXACT_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreIA5Match} Matching Rule
+     * which has the OID {@code 1.3.6.1.4.1.1466.109.114.2}.
+     *
+     * @return A reference to the {@code caseIgnoreIA5Match} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreIA5MatchingRule() {
+        return CASE_IGNORE_IA5_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreIA5SubstringsMatch} Matching Rule
+     * which has the OID {@code 1.3.6.1.4.1.1466.109.114.3}.
+     *
+     * @return A reference to the {@code caseIgnoreIA5SubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreIA5SubstringsMatchingRule() {
+        return CASE_IGNORE_IA5_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreListMatch} Matching Rule
+     * which has the OID {@code 2.5.13.11}.
+     *
+     * @return A reference to the {@code caseIgnoreListMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreListMatchingRule() {
+        return CASE_IGNORE_LIST_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreListSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.12}.
+     *
+     * @return A reference to the {@code caseIgnoreListSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreListSubstringsMatchingRule() {
+        return CASE_IGNORE_LIST_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreMatch} Matching Rule
+     * which has the OID {@code 2.5.13.2}.
+     *
+     * @return A reference to the {@code caseIgnoreMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreMatchingRule() {
+        return CASE_IGNORE_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.3}.
+     *
+     * @return A reference to the {@code caseIgnoreOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreOrderingMatchingRule() {
+        return CASE_IGNORE_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code caseIgnoreSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.4}.
+     *
+     * @return A reference to the {@code caseIgnoreSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getCaseIgnoreSubstringsMatchingRule() {
+        return CASE_IGNORE_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code certificateExactMatch} Matching Rule
+     * which has the OID {@code 2.5.13.34}.
+     *
+     * @return A reference to the {@code certificateExactMatch} Matching Rule.
+     */
+    public static MatchingRule getCertificateExactMatchingRule() {
+        return CERTIFICATE_EXACT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code directoryStringFirstComponentMatch} Matching Rule
+     * which has the OID {@code 2.5.13.31}.
+     *
+     * @return A reference to the {@code directoryStringFirstComponentMatch} Matching Rule.
+     */
+    public static MatchingRule getDirectoryStringFirstComponentMatchingRule() {
+        return DIRECTORY_STRING_FIRST_COMPONENT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code distinguishedNameMatch} Matching Rule
+     * which has the OID {@code 2.5.13.1}.
+     *
+     * @return A reference to the {@code distinguishedNameMatch} Matching Rule.
+     */
+    public static MatchingRule getDistinguishedNameMatchingRule() {
+        return DISTINGUISHED_NAME_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code generalizedTimeMatch} Matching Rule
+     * which has the OID {@code 2.5.13.27}.
+     *
+     * @return A reference to the {@code generalizedTimeMatch} Matching Rule.
+     */
+    public static MatchingRule getGeneralizedTimeMatchingRule() {
+        return GENERALIZED_TIME_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code generalizedTimeOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.28}.
+     *
+     * @return A reference to the {@code generalizedTimeOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getGeneralizedTimeOrderingMatchingRule() {
+        return GENERALIZED_TIME_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code integerFirstComponentMatch} Matching Rule
+     * which has the OID {@code 2.5.13.29}.
+     *
+     * @return A reference to the {@code integerFirstComponentMatch} Matching Rule.
+     */
+    public static MatchingRule getIntegerFirstComponentMatchingRule() {
+        return INTEGER_FIRST_COMPONENT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code integerMatch} Matching Rule
+     * which has the OID {@code 2.5.13.14}.
+     *
+     * @return A reference to the {@code integerMatch} Matching Rule.
+     */
+    public static MatchingRule getIntegerMatchingRule() {
+        return INTEGER_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code integerOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.15}.
+     *
+     * @return A reference to the {@code integerOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getIntegerOrderingMatchingRule() {
+        return INTEGER_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code keywordMatch} Matching Rule
+     * which has the OID {@code 2.5.13.33}.
+     *
+     * @return A reference to the {@code keywordMatch} Matching Rule.
+     */
+    public static MatchingRule getKeywordMatchingRule() {
+        return KEYWORD_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code numericStringMatch} Matching Rule
+     * which has the OID {@code 2.5.13.8}.
+     *
+     * @return A reference to the {@code numericStringMatch} Matching Rule.
+     */
+    public static MatchingRule getNumericStringMatchingRule() {
+        return NUMERIC_STRING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code numericStringOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.9}.
+     *
+     * @return A reference to the {@code numericStringOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getNumericStringOrderingMatchingRule() {
+        return NUMERIC_STRING_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code numericStringSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.10}.
+     *
+     * @return A reference to the {@code numericStringSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getNumericStringSubstringsMatchingRule() {
+        return NUMERIC_STRING_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code objectIdentifierFirstComponentMatch} Matching Rule
+     * which has the OID {@code 2.5.13.30}.
+     *
+     * @return A reference to the {@code objectIdentifierFirstComponentMatch} Matching Rule.
+     */
+    public static MatchingRule getObjectIdentifierFirstComponentMatchingRule() {
+        return OBJECT_IDENTIFIER_FIRST_COMPONENT_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code objectIdentifierMatch} Matching Rule
+     * which has the OID {@code 2.5.13.0}.
+     *
+     * @return A reference to the {@code objectIdentifierMatch} Matching Rule.
+     */
+    public static MatchingRule getObjectIdentifierMatchingRule() {
+        return OBJECT_IDENTIFIER_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code octetStringMatch} Matching Rule
+     * which has the OID {@code 2.5.13.17}.
+     *
+     * @return A reference to the {@code octetStringMatch} Matching Rule.
+     */
+    public static MatchingRule getOctetStringMatchingRule() {
+        return OCTET_STRING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code octetStringOrderingMatch} Matching Rule
+     * which has the OID {@code 2.5.13.18}.
+     *
+     * @return A reference to the {@code octetStringOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getOctetStringOrderingMatchingRule() {
+        return OCTET_STRING_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code octetStringSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.19}.
+     *
+     * @return A reference to the {@code octetStringSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getOctetStringSubstringsMatchingRule() {
+        return OCTET_STRING_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code presentationAddressMatch} Matching Rule
+     * which has the OID {@code 2.5.13.22}.
+     *
+     * @return A reference to the {@code presentationAddressMatch} Matching Rule.
+     */
+    public static MatchingRule getPresentationAddressMatchingRule() {
+        return PRESENTATION_ADDRESS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code protocolInformationMatch} Matching Rule
+     * which has the OID {@code 2.5.13.24}.
+     *
+     * @return A reference to the {@code protocolInformationMatch} Matching Rule.
+     */
+    public static MatchingRule getProtocolInformationMatchingRule() {
+        return PROTOCOL_INFORMATION_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code telephoneNumberMatch} Matching Rule
+     * which has the OID {@code 2.5.13.20}.
+     *
+     * @return A reference to the {@code telephoneNumberMatch} Matching Rule.
+     */
+    public static MatchingRule getTelephoneNumberMatchingRule() {
+        return TELEPHONE_NUMBER_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code telephoneNumberSubstringsMatch} Matching Rule
+     * which has the OID {@code 2.5.13.21}.
+     *
+     * @return A reference to the {@code telephoneNumberSubstringsMatch} Matching Rule.
+     */
+    public static MatchingRule getTelephoneNumberSubstringsMatchingRule() {
+        return TELEPHONE_NUMBER_SUBSTRINGS_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code uniqueMemberMatch} Matching Rule
+     * which has the OID {@code 2.5.13.23}.
+     *
+     * @return A reference to the {@code uniqueMemberMatch} Matching Rule.
+     */
+    public static MatchingRule getUniqueMemberMatchingRule() {
+        return UNIQUE_MEMBER_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code uuidMatch} Matching Rule
+     * which has the OID {@code 1.3.6.1.1.16.2}.
+     *
+     * @return A reference to the {@code uuidMatch} Matching Rule.
+     */
+    public static MatchingRule getUUIDMatchingRule() {
+        return UUID_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code uuidOrderingMatch} Matching Rule
+     * which has the OID {@code 1.3.6.1.1.16.3}.
+     *
+     * @return A reference to the {@code uuidOrderingMatch} Matching Rule.
+     */
+    public static MatchingRule getUUIDOrderingMatchingRule() {
+        return UUID_ORDERING_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code wordMatch} Matching Rule
+     * which has the OID {@code 2.5.13.32}.
+     *
+     * @return A reference to the {@code wordMatch} Matching Rule.
+     */
+    public static MatchingRule getWordMatchingRule() {
+        return WORD_MATCHING_RULE;
+    }
+
+    /**
+     * Returns a reference to the {@code aliasedObjectName} Attribute Type
+     * which has the OID {@code 2.5.4.1}.
+     *
+     * @return A reference to the {@code aliasedObjectName} Attribute Type.
+     */
+    public static AttributeType getAliasedObjectNameAttributeType() {
+        return ALIASED_OBJECT_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code altServer} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.6}.
+     *
+     * @return A reference to the {@code altServer} Attribute Type.
+     */
+    public static AttributeType getAltServerAttributeType() {
+        return ALT_SERVER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code attributeTypes} Attribute Type
+     * which has the OID {@code 2.5.21.5}.
+     *
+     * @return A reference to the {@code attributeTypes} Attribute Type.
+     */
+    public static AttributeType getAttributeTypesAttributeType() {
+        return ATTRIBUTE_TYPES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code authorityRevocationList} Attribute Type
+     * which has the OID {@code 2.5.4.38}.
+     *
+     * @return A reference to the {@code authorityRevocationList} Attribute Type.
+     */
+    public static AttributeType getAuthorityRevocationListAttributeType() {
+        return AUTHORITY_REVOCATION_LIST_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code authPassword} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.3.4}.
+     *
+     * @return A reference to the {@code authPassword} Attribute Type.
+     */
+    public static AttributeType getAuthPasswordAttributeType() {
+        return AUTH_PASSWORD_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code businessCategory} Attribute Type
+     * which has the OID {@code 2.5.4.15}.
+     *
+     * @return A reference to the {@code businessCategory} Attribute Type.
+     */
+    public static AttributeType getBusinessCategoryAttributeType() {
+        return BUSINESS_CATEGORY_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code certificateRevocationList} Attribute Type
+     * which has the OID {@code 2.5.4.39}.
+     *
+     * @return A reference to the {@code certificateRevocationList} Attribute Type.
+     */
+    public static AttributeType getCertificateRevocationListAttributeType() {
+        return CERTIFICATE_REVOCATION_LIST_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code cn} Attribute Type
+     * which has the OID {@code 2.5.4.3}.
+     *
+     * @return A reference to the {@code cn} Attribute Type.
+     */
+    public static AttributeType getCNAttributeType() {
+        return CN_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code createTimestamp} Attribute Type
+     * which has the OID {@code 2.5.18.1}.
+     *
+     * @return A reference to the {@code createTimestamp} Attribute Type.
+     */
+    public static AttributeType getCreateTimestampAttributeType() {
+        return CREATE_TIMESTAMP_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code creatorsName} Attribute Type
+     * which has the OID {@code 2.5.18.3}.
+     *
+     * @return A reference to the {@code creatorsName} Attribute Type.
+     */
+    public static AttributeType getCreatorsNameAttributeType() {
+        return CREATORS_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code crossCertificatePair} Attribute Type
+     * which has the OID {@code 2.5.4.40}.
+     *
+     * @return A reference to the {@code crossCertificatePair} Attribute Type.
+     */
+    public static AttributeType getCrossCertificatePairAttributeType() {
+        return CROSS_CERTIFICATE_PAIR_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code c} Attribute Type
+     * which has the OID {@code 2.5.4.6}.
+     *
+     * @return A reference to the {@code c} Attribute Type.
+     */
+    public static AttributeType getCAttributeType() {
+        return C_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code cACertificate} Attribute Type
+     * which has the OID {@code 2.5.4.37}.
+     *
+     * @return A reference to the {@code cACertificate} Attribute Type.
+     */
+    public static AttributeType getCACertificateAttributeType() {
+        return C_A_CERTIFICATE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code dc} Attribute Type
+     * which has the OID {@code 0.9.2342.19200300.100.1.25}.
+     *
+     * @return A reference to the {@code dc} Attribute Type.
+     */
+    public static AttributeType getDCAttributeType() {
+        return DC_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code deltaRevocationList} Attribute Type
+     * which has the OID {@code 2.5.4.53}.
+     *
+     * @return A reference to the {@code deltaRevocationList} Attribute Type.
+     */
+    public static AttributeType getDeltaRevocationListAttributeType() {
+        return DELTA_REVOCATION_LIST_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code description} Attribute Type
+     * which has the OID {@code 2.5.4.13}.
+     *
+     * @return A reference to the {@code description} Attribute Type.
+     */
+    public static AttributeType getDescriptionAttributeType() {
+        return DESCRIPTION_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code destinationIndicator} Attribute Type
+     * which has the OID {@code 2.5.4.27}.
+     *
+     * @return A reference to the {@code destinationIndicator} Attribute Type.
+     */
+    public static AttributeType getDestinationIndicatorAttributeType() {
+        return DESTINATION_INDICATOR_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code distinguishedName} Attribute Type
+     * which has the OID {@code 2.5.4.49}.
+     *
+     * @return A reference to the {@code distinguishedName} Attribute Type.
+     */
+    public static AttributeType getDistinguishedNameAttributeType() {
+        return DISTINGUISHED_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code ditContentRules} Attribute Type
+     * which has the OID {@code 2.5.21.2}.
+     *
+     * @return A reference to the {@code ditContentRules} Attribute Type.
+     */
+    public static AttributeType getDITContentRulesAttributeType() {
+        return DIT_CONTENT_RULES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code ditStructureRules} Attribute Type
+     * which has the OID {@code 2.5.21.1}.
+     *
+     * @return A reference to the {@code ditStructureRules} Attribute Type.
+     */
+    public static AttributeType getDITStructureRulesAttributeType() {
+        return DIT_STRUCTURE_RULES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code dnQualifier} Attribute Type
+     * which has the OID {@code 2.5.4.46}.
+     *
+     * @return A reference to the {@code dnQualifier} Attribute Type.
+     */
+    public static AttributeType getDNQualifierAttributeType() {
+        return DN_QUALIFIER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code enhancedSearchGuide} Attribute Type
+     * which has the OID {@code 2.5.4.47}.
+     *
+     * @return A reference to the {@code enhancedSearchGuide} Attribute Type.
+     */
+    public static AttributeType getEnhancedSearchGuideAttributeType() {
+        return ENHANCED_SEARCH_GUIDE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code entryDN} Attribute Type
+     * which has the OID {@code 1.3.6.1.1.20}.
+     *
+     * @return A reference to the {@code entryDN} Attribute Type.
+     */
+    public static AttributeType getEntryDNAttributeType() {
+        return ENTRY_DN_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code entryUUID} Attribute Type
+     * which has the OID {@code 1.3.6.1.1.16.4}.
+     *
+     * @return A reference to the {@code entryUUID} Attribute Type.
+     */
+    public static AttributeType getEntryUUIDAttributeType() {
+        return ENTRY_UUID_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code facsimileTelephoneNumber} Attribute Type
+     * which has the OID {@code 2.5.4.23}.
+     *
+     * @return A reference to the {@code facsimileTelephoneNumber} Attribute Type.
+     */
+    public static AttributeType getFacsimileTelephoneNumberAttributeType() {
+        return FACSIMILE_TELEPHONE_NUMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code fullVendorVersion} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.36733.2.1.1.141}.
+     *
+     * @return A reference to the {@code fullVendorVersion} Attribute Type.
+     */
+    public static AttributeType getFullVendorVersionAttributeType() {
+        return FULL_VENDOR_VERSION_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code generationQualifier} Attribute Type
+     * which has the OID {@code 2.5.4.44}.
+     *
+     * @return A reference to the {@code generationQualifier} Attribute Type.
+     */
+    public static AttributeType getGenerationQualifierAttributeType() {
+        return GENERATION_QUALIFIER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code givenName} Attribute Type
+     * which has the OID {@code 2.5.4.42}.
+     *
+     * @return A reference to the {@code givenName} Attribute Type.
+     */
+    public static AttributeType getGivenNameAttributeType() {
+        return GIVEN_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code governingStructureRule} Attribute Type
+     * which has the OID {@code 2.5.21.10}.
+     *
+     * @return A reference to the {@code governingStructureRule} Attribute Type.
+     */
+    public static AttributeType getGoverningStructureRuleAttributeType() {
+        return GOVERNING_STRUCTURE_RULE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code houseIdentifier} Attribute Type
+     * which has the OID {@code 2.5.4.51}.
+     *
+     * @return A reference to the {@code houseIdentifier} Attribute Type.
+     */
+    public static AttributeType getHouseIdentifierAttributeType() {
+        return HOUSE_IDENTIFIER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code initials} Attribute Type
+     * which has the OID {@code 2.5.4.43}.
+     *
+     * @return A reference to the {@code initials} Attribute Type.
+     */
+    public static AttributeType getInitialsAttributeType() {
+        return INITIALS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code internationalISDNNumber} Attribute Type
+     * which has the OID {@code 2.5.4.25}.
+     *
+     * @return A reference to the {@code internationalISDNNumber} Attribute Type.
+     */
+    public static AttributeType getInternationalISDNNumberAttributeType() {
+        return INTERNATIONAL_ISDN_NUMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code ldapSyntaxes} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.16}.
+     *
+     * @return A reference to the {@code ldapSyntaxes} Attribute Type.
+     */
+    public static AttributeType getLDAPSyntaxesAttributeType() {
+        return LDAP_SYNTAXES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code l} Attribute Type
+     * which has the OID {@code 2.5.4.7}.
+     *
+     * @return A reference to the {@code l} Attribute Type.
+     */
+    public static AttributeType getLAttributeType() {
+        return L_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code matchingRules} Attribute Type
+     * which has the OID {@code 2.5.21.4}.
+     *
+     * @return A reference to the {@code matchingRules} Attribute Type.
+     */
+    public static AttributeType getMatchingRulesAttributeType() {
+        return MATCHING_RULES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code matchingRuleUse} Attribute Type
+     * which has the OID {@code 2.5.21.8}.
+     *
+     * @return A reference to the {@code matchingRuleUse} Attribute Type.
+     */
+    public static AttributeType getMatchingRuleUseAttributeType() {
+        return MATCHING_RULE_USE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code member} Attribute Type
+     * which has the OID {@code 2.5.4.31}.
+     *
+     * @return A reference to the {@code member} Attribute Type.
+     */
+    public static AttributeType getMemberAttributeType() {
+        return MEMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code modifiersName} Attribute Type
+     * which has the OID {@code 2.5.18.4}.
+     *
+     * @return A reference to the {@code modifiersName} Attribute Type.
+     */
+    public static AttributeType getModifiersNameAttributeType() {
+        return MODIFIERS_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code modifyTimestamp} Attribute Type
+     * which has the OID {@code 2.5.18.2}.
+     *
+     * @return A reference to the {@code modifyTimestamp} Attribute Type.
+     */
+    public static AttributeType getModifyTimestampAttributeType() {
+        return MODIFY_TIMESTAMP_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code name} Attribute Type
+     * which has the OID {@code 2.5.4.41}.
+     *
+     * @return A reference to the {@code name} Attribute Type.
+     */
+    public static AttributeType getNameAttributeType() {
+        return NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code nameForms} Attribute Type
+     * which has the OID {@code 2.5.21.7}.
+     *
+     * @return A reference to the {@code nameForms} Attribute Type.
+     */
+    public static AttributeType getNameFormsAttributeType() {
+        return NAME_FORMS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code namingContexts} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.5}.
+     *
+     * @return A reference to the {@code namingContexts} Attribute Type.
+     */
+    public static AttributeType getNamingContextsAttributeType() {
+        return NAMING_CONTEXTS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code objectClasses} Attribute Type
+     * which has the OID {@code 2.5.21.6}.
+     *
+     * @return A reference to the {@code objectClasses} Attribute Type.
+     */
+    public static AttributeType getObjectClassesAttributeType() {
+        return OBJECT_CLASSES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code objectClass} Attribute Type
+     * which has the OID {@code 2.5.4.0}.
+     *
+     * @return A reference to the {@code objectClass} Attribute Type.
+     */
+    public static AttributeType getObjectClassAttributeType() {
+        return OBJECT_CLASS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code ou} Attribute Type
+     * which has the OID {@code 2.5.4.11}.
+     *
+     * @return A reference to the {@code ou} Attribute Type.
+     */
+    public static AttributeType getOUAttributeType() {
+        return OU_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code owner} Attribute Type
+     * which has the OID {@code 2.5.4.32}.
+     *
+     * @return A reference to the {@code owner} Attribute Type.
+     */
+    public static AttributeType getOwnerAttributeType() {
+        return OWNER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code o} Attribute Type
+     * which has the OID {@code 2.5.4.10}.
+     *
+     * @return A reference to the {@code o} Attribute Type.
+     */
+    public static AttributeType getOAttributeType() {
+        return O_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code physicalDeliveryOfficeName} Attribute Type
+     * which has the OID {@code 2.5.4.19}.
+     *
+     * @return A reference to the {@code physicalDeliveryOfficeName} Attribute Type.
+     */
+    public static AttributeType getPhysicalDeliveryOfficeNameAttributeType() {
+        return PHYSICAL_DELIVERY_OFFICE_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code postalAddress} Attribute Type
+     * which has the OID {@code 2.5.4.16}.
+     *
+     * @return A reference to the {@code postalAddress} Attribute Type.
+     */
+    public static AttributeType getPostalAddressAttributeType() {
+        return POSTAL_ADDRESS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code postalCode} Attribute Type
+     * which has the OID {@code 2.5.4.17}.
+     *
+     * @return A reference to the {@code postalCode} Attribute Type.
+     */
+    public static AttributeType getPostalCodeAttributeType() {
+        return POSTAL_CODE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code postOfficeBox} Attribute Type
+     * which has the OID {@code 2.5.4.18}.
+     *
+     * @return A reference to the {@code postOfficeBox} Attribute Type.
+     */
+    public static AttributeType getPostOfficeBoxAttributeType() {
+        return POST_OFFICE_BOX_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code preferredDeliveryMethod} Attribute Type
+     * which has the OID {@code 2.5.4.28}.
+     *
+     * @return A reference to the {@code preferredDeliveryMethod} Attribute Type.
+     */
+    public static AttributeType getPreferredDeliveryMethodAttributeType() {
+        return PREFERRED_DELIVERY_METHOD_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code registeredAddress} Attribute Type
+     * which has the OID {@code 2.5.4.26}.
+     *
+     * @return A reference to the {@code registeredAddress} Attribute Type.
+     */
+    public static AttributeType getRegisteredAddressAttributeType() {
+        return REGISTERED_ADDRESS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code roleOccupant} Attribute Type
+     * which has the OID {@code 2.5.4.33}.
+     *
+     * @return A reference to the {@code roleOccupant} Attribute Type.
+     */
+    public static AttributeType getRoleOccupantAttributeType() {
+        return ROLE_OCCUPANT_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code searchGuide} Attribute Type
+     * which has the OID {@code 2.5.4.14}.
+     *
+     * @return A reference to the {@code searchGuide} Attribute Type.
+     */
+    public static AttributeType getSearchGuideAttributeType() {
+        return SEARCH_GUIDE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code seeAlso} Attribute Type
+     * which has the OID {@code 2.5.4.34}.
+     *
+     * @return A reference to the {@code seeAlso} Attribute Type.
+     */
+    public static AttributeType getSeeAlsoAttributeType() {
+        return SEE_ALSO_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code serialNumber} Attribute Type
+     * which has the OID {@code 2.5.4.5}.
+     *
+     * @return A reference to the {@code serialNumber} Attribute Type.
+     */
+    public static AttributeType getSerialNumberAttributeType() {
+        return SERIAL_NUMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code sn} Attribute Type
+     * which has the OID {@code 2.5.4.4}.
+     *
+     * @return A reference to the {@code sn} Attribute Type.
+     */
+    public static AttributeType getSNAttributeType() {
+        return SN_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code street} Attribute Type
+     * which has the OID {@code 2.5.4.9}.
+     *
+     * @return A reference to the {@code street} Attribute Type.
+     */
+    public static AttributeType getStreetAttributeType() {
+        return STREET_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code structuralObjectClass} Attribute Type
+     * which has the OID {@code 2.5.21.9}.
+     *
+     * @return A reference to the {@code structuralObjectClass} Attribute Type.
+     */
+    public static AttributeType getStructuralObjectClassAttributeType() {
+        return STRUCTURAL_OBJECT_CLASS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code st} Attribute Type
+     * which has the OID {@code 2.5.4.8}.
+     *
+     * @return A reference to the {@code st} Attribute Type.
+     */
+    public static AttributeType getSTAttributeType() {
+        return ST_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code subschemaSubentry} Attribute Type
+     * which has the OID {@code 2.5.18.10}.
+     *
+     * @return A reference to the {@code subschemaSubentry} Attribute Type.
+     */
+    public static AttributeType getSubschemaSubentryAttributeType() {
+        return SUBSCHEMA_SUBENTRY_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedAlgorithms} Attribute Type
+     * which has the OID {@code 2.5.4.52}.
+     *
+     * @return A reference to the {@code supportedAlgorithms} Attribute Type.
+     */
+    public static AttributeType getSupportedAlgorithmsAttributeType() {
+        return SUPPORTED_ALGORITHMS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedAuthPasswordSchemes} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.3.3}.
+     *
+     * @return A reference to the {@code supportedAuthPasswordSchemes} Attribute Type.
+     */
+    public static AttributeType getSupportedAuthPasswordSchemesAttributeType() {
+        return SUPPORTED_AUTH_PASSWORD_SCHEMES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedControl} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.13}.
+     *
+     * @return A reference to the {@code supportedControl} Attribute Type.
+     */
+    public static AttributeType getSupportedControlAttributeType() {
+        return SUPPORTED_CONTROL_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedExtension} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.7}.
+     *
+     * @return A reference to the {@code supportedExtension} Attribute Type.
+     */
+    public static AttributeType getSupportedExtensionAttributeType() {
+        return SUPPORTED_EXTENSION_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedFeatures} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.3.5}.
+     *
+     * @return A reference to the {@code supportedFeatures} Attribute Type.
+     */
+    public static AttributeType getSupportedFeaturesAttributeType() {
+        return SUPPORTED_FEATURES_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedLDAPVersion} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.15}.
+     *
+     * @return A reference to the {@code supportedLDAPVersion} Attribute Type.
+     */
+    public static AttributeType getSupportedLDAPVersionAttributeType() {
+        return SUPPORTED_LDAP_VERSION_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code supportedSASLMechanisms} Attribute Type
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.14}.
+     *
+     * @return A reference to the {@code supportedSASLMechanisms} Attribute Type.
+     */
+    public static AttributeType getSupportedSASLMechanismsAttributeType() {
+        return SUPPORTED_SASL_MECHANISMS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code telephoneNumber} Attribute Type
+     * which has the OID {@code 2.5.4.20}.
+     *
+     * @return A reference to the {@code telephoneNumber} Attribute Type.
+     */
+    public static AttributeType getTelephoneNumberAttributeType() {
+        return TELEPHONE_NUMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code teletexTerminalIdentifier} Attribute Type
+     * which has the OID {@code 2.5.4.22}.
+     *
+     * @return A reference to the {@code teletexTerminalIdentifier} Attribute Type.
+     */
+    public static AttributeType getTeletexTerminalIdentifierAttributeType() {
+        return TELETEX_TERMINAL_IDENTIFIER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code telexNumber} Attribute Type
+     * which has the OID {@code 2.5.4.21}.
+     *
+     * @return A reference to the {@code telexNumber} Attribute Type.
+     */
+    public static AttributeType getTelexNumberAttributeType() {
+        return TELEX_NUMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code title} Attribute Type
+     * which has the OID {@code 2.5.4.12}.
+     *
+     * @return A reference to the {@code title} Attribute Type.
+     */
+    public static AttributeType getTitleAttributeType() {
+        return TITLE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code uid} Attribute Type
+     * which has the OID {@code 0.9.2342.19200300.100.1.1}.
+     *
+     * @return A reference to the {@code uid} Attribute Type.
+     */
+    public static AttributeType getUIDAttributeType() {
+        return UID_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code uniqueMember} Attribute Type
+     * which has the OID {@code 2.5.4.50}.
+     *
+     * @return A reference to the {@code uniqueMember} Attribute Type.
+     */
+    public static AttributeType getUniqueMemberAttributeType() {
+        return UNIQUE_MEMBER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code userCertificate} Attribute Type
+     * which has the OID {@code 2.5.4.36}.
+     *
+     * @return A reference to the {@code userCertificate} Attribute Type.
+     */
+    public static AttributeType getUserCertificateAttributeType() {
+        return USER_CERTIFICATE_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code userPassword} Attribute Type
+     * which has the OID {@code 2.5.4.35}.
+     *
+     * @return A reference to the {@code userPassword} Attribute Type.
+     */
+    public static AttributeType getUserPasswordAttributeType() {
+        return USER_PASSWORD_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code vendorName} Attribute Type
+     * which has the OID {@code 1.3.6.1.1.4}.
+     *
+     * @return A reference to the {@code vendorName} Attribute Type.
+     */
+    public static AttributeType getVendorNameAttributeType() {
+        return VENDOR_NAME_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code vendorVersion} Attribute Type
+     * which has the OID {@code 1.3.6.1.1.5}.
+     *
+     * @return A reference to the {@code vendorVersion} Attribute Type.
+     */
+    public static AttributeType getVendorVersionAttributeType() {
+        return VENDOR_VERSION_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code x121Address} Attribute Type
+     * which has the OID {@code 2.5.4.24}.
+     *
+     * @return A reference to the {@code x121Address} Attribute Type.
+     */
+    public static AttributeType getX121AddressAttributeType() {
+        return X121_ADDRESS_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code x500UniqueIdentifier} Attribute Type
+     * which has the OID {@code 2.5.4.45}.
+     *
+     * @return A reference to the {@code x500UniqueIdentifier} Attribute Type.
+     */
+    public static AttributeType getX500UniqueIdentifierAttributeType() {
+        return X500_UNIQUE_IDENTIFIER_ATTRIBUTE_TYPE;
+    }
+
+    /**
+     * Returns a reference to the {@code alias} Object Class
+     * which has the OID {@code 2.5.6.1}.
+     *
+     * @return A reference to the {@code alias} Object Class.
+     */
+    public static ObjectClass getAliasObjectClass() {
+        return ALIAS_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code applicationProcess} Object Class
+     * which has the OID {@code 2.5.6.11}.
+     *
+     * @return A reference to the {@code applicationProcess} Object Class.
+     */
+    public static ObjectClass getApplicationProcessObjectClass() {
+        return APPLICATION_PROCESS_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code authPasswordObject} Object Class
+     * which has the OID {@code 1.3.6.1.4.1.4203.1.4.7}.
+     *
+     * @return A reference to the {@code authPasswordObject} Object Class.
+     */
+    public static ObjectClass getAuthPasswordObjectObjectClass() {
+        return AUTH_PASSWORD_OBJECT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code certificationAuthority} Object Class
+     * which has the OID {@code 2.5.6.16}.
+     *
+     * @return A reference to the {@code certificationAuthority} Object Class.
+     */
+    public static ObjectClass getCertificationAuthorityObjectClass() {
+        return CERTIFICATION_AUTHORITY_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code certificationAuthority-V2} Object Class
+     * which has the OID {@code 2.5.6.16.2}.
+     *
+     * @return A reference to the {@code certificationAuthority-V2} Object Class.
+     */
+    public static ObjectClass getCertificationAuthorityV2ObjectClass() {
+        return CERTIFICATION_AUTHORITY_V2_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code country} Object Class
+     * which has the OID {@code 2.5.6.2}.
+     *
+     * @return A reference to the {@code country} Object Class.
+     */
+    public static ObjectClass getCountryObjectClass() {
+        return COUNTRY_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code cRLDistributionPoint} Object Class
+     * which has the OID {@code 2.5.6.19}.
+     *
+     * @return A reference to the {@code cRLDistributionPoint} Object Class.
+     */
+    public static ObjectClass getCRlDistributionPointObjectClass() {
+        return C_RL_DISTRIBUTION_POINT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code dcObject} Object Class
+     * which has the OID {@code 1.3.6.1.4.1.1466.344}.
+     *
+     * @return A reference to the {@code dcObject} Object Class.
+     */
+    public static ObjectClass getDCObjectObjectClass() {
+        return DC_OBJECT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code deltaCRL} Object Class
+     * which has the OID {@code 2.5.6.23}.
+     *
+     * @return A reference to the {@code deltaCRL} Object Class.
+     */
+    public static ObjectClass getDeltaCrlObjectClass() {
+        return DELTA_CRL_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code device} Object Class
+     * which has the OID {@code 2.5.6.14}.
+     *
+     * @return A reference to the {@code device} Object Class.
+     */
+    public static ObjectClass getDeviceObjectClass() {
+        return DEVICE_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code extensibleObject} Object Class
+     * which has the OID {@code 1.3.6.1.4.1.1466.101.120.111}.
+     *
+     * @return A reference to the {@code extensibleObject} Object Class.
+     */
+    public static ObjectClass getExtensibleObjectObjectClass() {
+        return EXTENSIBLE_OBJECT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code groupOfNames} Object Class
+     * which has the OID {@code 2.5.6.9}.
+     *
+     * @return A reference to the {@code groupOfNames} Object Class.
+     */
+    public static ObjectClass getGroupOfNamesObjectClass() {
+        return GROUP_OF_NAMES_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code groupOfUniqueNames} Object Class
+     * which has the OID {@code 2.5.6.17}.
+     *
+     * @return A reference to the {@code groupOfUniqueNames} Object Class.
+     */
+    public static ObjectClass getGroupOfUniqueNamesObjectClass() {
+        return GROUP_OF_UNIQUE_NAMES_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code locality} Object Class
+     * which has the OID {@code 2.5.6.3}.
+     *
+     * @return A reference to the {@code locality} Object Class.
+     */
+    public static ObjectClass getLocalityObjectClass() {
+        return LOCALITY_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code organizationalPerson} Object Class
+     * which has the OID {@code 2.5.6.7}.
+     *
+     * @return A reference to the {@code organizationalPerson} Object Class.
+     */
+    public static ObjectClass getOrganizationalPersonObjectClass() {
+        return ORGANIZATIONAL_PERSON_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code organizationalRole} Object Class
+     * which has the OID {@code 2.5.6.8}.
+     *
+     * @return A reference to the {@code organizationalRole} Object Class.
+     */
+    public static ObjectClass getOrganizationalRoleObjectClass() {
+        return ORGANIZATIONAL_ROLE_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code organizationalUnit} Object Class
+     * which has the OID {@code 2.5.6.5}.
+     *
+     * @return A reference to the {@code organizationalUnit} Object Class.
+     */
+    public static ObjectClass getOrganizationalUnitObjectClass() {
+        return ORGANIZATIONAL_UNIT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code organization} Object Class
+     * which has the OID {@code 2.5.6.4}.
+     *
+     * @return A reference to the {@code organization} Object Class.
+     */
+    public static ObjectClass getOrganizationObjectClass() {
+        return ORGANIZATION_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code person} Object Class
+     * which has the OID {@code 2.5.6.6}.
+     *
+     * @return A reference to the {@code person} Object Class.
+     */
+    public static ObjectClass getPersonObjectClass() {
+        return PERSON_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code pkiCA} Object Class
+     * which has the OID {@code 2.5.6.22}.
+     *
+     * @return A reference to the {@code pkiCA} Object Class.
+     */
+    public static ObjectClass getPkiCaObjectClass() {
+        return PKI_CA_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code pkiUser} Object Class
+     * which has the OID {@code 2.5.6.21}.
+     *
+     * @return A reference to the {@code pkiUser} Object Class.
+     */
+    public static ObjectClass getPkiUserObjectClass() {
+        return PKI_USER_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code residentialPerson} Object Class
+     * which has the OID {@code 2.5.6.10}.
+     *
+     * @return A reference to the {@code residentialPerson} Object Class.
+     */
+    public static ObjectClass getResidentialPersonObjectClass() {
+        return RESIDENTIAL_PERSON_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code strongAuthenticationUser} Object Class
+     * which has the OID {@code 2.5.6.15}.
+     *
+     * @return A reference to the {@code strongAuthenticationUser} Object Class.
+     */
+    public static ObjectClass getStrongAuthenticationUserObjectClass() {
+        return STRONG_AUTHENTICATION_USER_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code subschema} Object Class
+     * which has the OID {@code 2.5.20.1}.
+     *
+     * @return A reference to the {@code subschema} Object Class.
+     */
+    public static ObjectClass getSubschemaObjectClass() {
+        return SUBSCHEMA_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code top} Object Class
+     * which has the OID {@code 2.5.6.0}.
+     *
+     * @return A reference to the {@code top} Object Class.
+     */
+    public static ObjectClass getTopObjectClass() {
+        return TOP_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code uidObject} Object Class
+     * which has the OID {@code 1.3.6.1.1.3.1}.
+     *
+     * @return A reference to the {@code uidObject} Object Class.
+     */
+    public static ObjectClass getUIDObjectObjectClass() {
+        return UID_OBJECT_OBJECT_CLASS;
+    }
+
+    /**
+     * Returns a reference to the {@code userSecurityInformation} Object Class
+     * which has the OID {@code 2.5.6.18}.
+     *
+     * @return A reference to the {@code userSecurityInformation} Object Class.
+     */
+    public static ObjectClass getUserSecurityInformationObjectClass() {
+        return USER_SECURITY_INFORMATION_OBJECT_CLASS;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
new file mode 100644
index 0000000..01fb4c2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
@@ -0,0 +1,1768 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2015 ForgeRock AS.
+ * Portions copyright 2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.forgerock.opendj.ldap.schema.CollationMatchingRulesImpl.*;
+import static org.forgerock.opendj.ldap.schema.ObjectClassType.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.TimeBasedMatchingRulesImpl.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+final class CoreSchemaImpl {
+    private static final Map<String, List<String>> X500_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("X.500"));
+
+    private static final Map<String, List<String>> RFC2252_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 2252"));
+    private static final Map<String, List<String>> RFC3045_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 3045"));
+    private static final Map<String, List<String>> RFC3112_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 3112"));
+    private static final Map<String, List<String>> RFC4512_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 4512"));
+    private static final Map<String, List<String>> RFC4517_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 4517"));
+    private static final Map<String, List<String>> RFC4519_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 4519"));
+    private static final Map<String, List<String>> RFC4523_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 4523"));
+    private static final Map<String, List<String>> RFC4530_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 4530"));
+    private static final Map<String, List<String>> RFC5020_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("RFC 5020"));
+
+    static final Map<String, List<String>> OPENDS_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("OpenDS Directory Server"));
+    private static final Map<String, List<String>> OPENDJ_ORIGIN = Collections.singletonMap(
+            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("OpenDJ Directory Server"));
+
+    private static final Schema SINGLETON;
+
+    public static final Map<String, String> JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS =
+            CoreSchemaSupportedLocales.getJvmSupportedLocaleNamesToOids();
+
+    static {
+        final SchemaBuilder builder = new SchemaBuilder("Core Schema");
+        defaultSyntaxes(builder);
+        defaultMatchingRules(builder);
+        defaultAttributeTypes(builder);
+        defaultObjectClasses(builder);
+
+        addRFC4519(builder);
+        addRFC4523(builder);
+        addRFC4530(builder);
+        addRFC3045(builder);
+        addRFC3112(builder);
+        addRFC5020(builder);
+        addSunProprietary(builder);
+        addForgeRockProprietary(builder);
+
+        SINGLETON = builder.toSchema().asNonStrictSchema();
+    }
+
+    static Schema getInstance() {
+        return SINGLETON;
+    }
+
+    private static void addRFC3045(final SchemaBuilder builder) {
+        builder.buildAttributeType("1.3.6.1.1.4")
+               .names("vendorName")
+               .equalityMatchingRule(EMR_CASE_EXACT_IA5_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC3045_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.1.5")
+               .names("vendorVersion")
+               .equalityMatchingRule(EMR_CASE_EXACT_IA5_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC3045_ORIGIN)
+               .addToSchema();
+    }
+
+    private static void addRFC3112(final SchemaBuilder builder) {
+        builder.buildSyntax(SYNTAX_AUTH_PASSWORD_OID)
+               .description(SYNTAX_AUTH_PASSWORD_DESCRIPTION)
+               .extraProperties(RFC3112_ORIGIN)
+               .implementation(new AuthPasswordSyntaxImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_AUTH_PASSWORD_EXACT_OID)
+                .names(EMR_AUTH_PASSWORD_EXACT_NAME)
+                .description(EMR_AUTH_PASSWORD_EXACT_DESCRIPTION)
+                .syntaxOID(SYNTAX_AUTH_PASSWORD_OID)
+                .extraProperties(RFC3112_ORIGIN)
+                .implementation(new AuthPasswordExactEqualityMatchingRuleImpl())
+                .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.4203.1.3.3")
+               .names("supportedAuthPasswordSchemes")
+               .description("supported password storage schemes")
+               .equalityMatchingRule(EMR_CASE_EXACT_IA5_OID)
+               .syntax(SYNTAX_IA5_STRING_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC3112_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.4203.1.3.4")
+               .names("authPassword")
+               .description("password authentication information")
+               .equalityMatchingRule(EMR_AUTH_PASSWORD_EXACT_OID)
+               .syntax(SYNTAX_AUTH_PASSWORD_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC3112_ORIGIN)
+               .addToSchema();
+
+        builder.buildObjectClass("1.3.6.1.4.1.4203.1.4.7")
+                .names("authPasswordObject")
+                .type(AUXILIARY)
+                .description("authentication password mix in class")
+                .optionalAttributes("authPassword")
+                .extraProperties(RFC3112_ORIGIN)
+                .addToSchema();
+    }
+
+    private static void addRFC4519(final SchemaBuilder builder) {
+        builder.buildAttributeType("2.5.4.15")
+               .names("businessCategory")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.41")
+               .names("name")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.6")
+               .names("c", "countryName")
+               .superiorType("name")
+               .syntax(SYNTAX_COUNTRY_STRING_OID)
+               .singleValue(true)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.3")
+               .names("cn", "commonName")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("0.9.2342.19200300.100.1.25")
+               .names("dc", "domainComponent")
+               .equalityMatchingRule(EMR_CASE_IGNORE_IA5_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_IA5_OID)
+               .syntax(SYNTAX_IA5_STRING_OID)
+               .singleValue(true)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.13")
+               .names("description")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.27")
+               .names("destinationIndicator")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_PRINTABLE_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.49")
+               .names("distinguishedName")
+               .equalityMatchingRule(EMR_DN_OID)
+               .syntax(SYNTAX_DN_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.46")
+               .names("dnQualifier")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .orderingMatchingRule(OMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_PRINTABLE_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.47")
+               .names("enhancedSearchGuide")
+               .syntax(SYNTAX_ENHANCED_GUIDE_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.23")
+               .names("facsimileTelephoneNumber")
+               .syntax(SYNTAX_FAXNUMBER_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.44")
+               .names("generationQualifier")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.42")
+               .names("givenName")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.51")
+               .names("houseIdentifier")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.43")
+               .names("initials")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.25")
+               .names("internationalISDNNumber")
+               .equalityMatchingRule(EMR_NUMERIC_STRING_OID)
+               .substringMatchingRule(SMR_NUMERIC_STRING_OID)
+               .syntax(SYNTAX_NUMERIC_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.7")
+               .names("l", "localityName")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.31")
+               .names("member")
+               .superiorType("distinguishedName")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.10")
+               .names("o", "organizationName")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.11")
+               .names("ou", "organizationalUnitName")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.32")
+               .names("owner")
+               .superiorType("distinguishedName")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.19")
+               .names("physicalDeliveryOfficeName")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.16")
+               .names("postalAddress")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.17")
+               .names("postalCode")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.18")
+               .names("postOfficeBox")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.28")
+               .names("preferredDeliveryMethod")
+               .syntax(SYNTAX_DELIVERY_METHOD_OID)
+               .singleValue(true)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.26")
+               .names("registeredAddress")
+               .superiorType("postalAddress")
+               .syntax(SYNTAX_POSTAL_ADDRESS_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.33")
+               .names("roleOccupant")
+               .superiorType("distinguishedName")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.14")
+               .names("searchGuide")
+               .syntax(SYNTAX_GUIDE_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.34")
+               .names("seeAlso")
+               .superiorType("distinguishedName")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.5")
+               .names("serialNumber")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_PRINTABLE_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.4")
+               .names("sn", "surname")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.8")
+               .names("st", "stateOrProvinceName")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.9")
+               .names("street", "streetAddress")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.20")
+               .names("telephoneNumber")
+               .equalityMatchingRule(EMR_TELEPHONE_OID)
+               .substringMatchingRule(SMR_TELEPHONE_OID)
+               .syntax(SYNTAX_TELEPHONE_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.22")
+               .names("teletexTerminalIdentifier")
+               .syntax(SYNTAX_TELETEX_TERM_ID_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.21")
+               .names("telexNumber")
+               .syntax(SYNTAX_TELEX_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.12")
+               .names("title")
+               .superiorType("name")
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("0.9.2342.19200300.100.1.1")
+               .names("uid", "userid")
+               .equalityMatchingRule(EMR_CASE_IGNORE_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.50")
+               .names("uniqueMember")
+               .equalityMatchingRule(EMR_UNIQUE_MEMBER_OID)
+               .syntax(SYNTAX_NAME_AND_OPTIONAL_UID_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.35")
+               .names("userPassword")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_OCTET_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.24")
+               .names("x121Address")
+               .equalityMatchingRule(EMR_NUMERIC_STRING_OID)
+               .substringMatchingRule(SMR_NUMERIC_STRING_OID)
+               .syntax(SYNTAX_NUMERIC_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.45")
+               .names("x500UniqueIdentifier")
+               .equalityMatchingRule(EMR_BIT_STRING_OID)
+               .syntax(SYNTAX_BIT_STRING_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4519_ORIGIN)
+               .addToSchema();
+
+        builder.buildObjectClass("2.5.6.11")
+                .names("applicationProcess")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("cn")
+                .optionalAttributes("seeAlso", "ou", "l", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.2")
+                .names("country")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("c")
+                .optionalAttributes("searchGuide", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("1.3.6.1.4.1.1466.344")
+                .names("dcObject")
+                .type(AUXILIARY)
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("dc")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.14")
+                .names("device")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("cn")
+                .optionalAttributes("serialNumber", "seeAlso", "owner", "ou", "o", "l", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.9")
+                .names("groupOfNames")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("member", "cn")
+                .optionalAttributes("businessCategory", "seeAlso", "owner", "ou", "o", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.17")
+                .names("groupOfUniqueNames")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("member", "cn")
+                .optionalAttributes("businessCategory", "seeAlso", "owner", "ou", "o", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.3")
+                .names("locality")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .optionalAttributes("street", "seeAlso", "searchGuide", "st", "l", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.4")
+                .names("organization")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("o")
+                .optionalAttributes("userPassword", "searchGuide", "seeAlso", "businessCategory", "x121Address",
+                        "registeredAddress", "destinationIndicator", "preferredDeliveryMethod", "telexNumber",
+                        "teletexTerminalIdentifier", "telephoneNumber", "internationalISDNNumber",
+                        "facsimileTelephoneNumber", "street", "postOfficeBox", "postalCode", "postalAddress",
+                        "physicalDeliveryOfficeName", "st", "l", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.7")
+                .names("organizationalPerson")
+                .superiorObjectClasses("person")
+                .optionalAttributes("title", "x121Address", "registeredAddress", "destinationIndicator",
+                        "preferredDeliveryMethod", "telexNumber", "teletexTerminalIdentifier", "telephoneNumber",
+                        "internationalISDNNumber", "facsimileTelephoneNumber", "street", "postOfficeBox",
+                        "postalCode", "postalAddress", "physicalDeliveryOfficeName", "ou", "st", "l")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.8")
+                .names("organizationalRole")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("cn")
+                .optionalAttributes("x121Address", "registeredAddress", "destinationIndicator",
+                        "preferredDeliveryMethod", "telexNumber", "teletexTerminalIdentifier", "telephoneNumber",
+                        "internationalISDNNumber", "facsimileTelephoneNumber", "seeAlso", "roleOccupant",
+                        "preferredDeliveryMethod", "street", "postOfficeBox", "postalCode", "postalAddress",
+                        "physicalDeliveryOfficeName", "ou", "st", "l", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.5")
+                .names("organizationalUnit")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("ou")
+                .optionalAttributes("businessCategory", "description", "destinationIndicator",
+                        "facsimileTelephoneNumber", "internationalISDNNumber", "l", "physicalDeliveryOfficeName",
+                        "postalAddress", "postalCode", "postOfficeBox", "preferredDeliveryMethod",
+                        "registeredAddress", "searchGuide", "seeAlso", "st", "street", "telephoneNumber",
+                        "teletexTerminalIdentifier", "telexNumber", "userPassword", "x121Address")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.6")
+                .names("person")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("userPassword", "telephoneNumber", "destinationIndicator", "seeAlso", "description")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.10")
+                .names("residentialPerson")
+                .superiorObjectClasses("person")
+                .requiredAttributes("l")
+                .optionalAttributes("businessCategory", "x121Address", "registeredAddress", "destinationIndicator",
+                        "preferredDeliveryMethod", "telexNumber", "teletexTerminalIdentifier", "telephoneNumber",
+                        "internationalISDNNumber", "facsimileTelephoneNumber", "preferredDeliveryMethod", "street",
+                        "postOfficeBox", "postalCode", "postalAddress", "physicalDeliveryOfficeName", "st", "l")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("1.3.6.1.1.3.1")
+                .names("uidObject")
+                .type(AUXILIARY)
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("uid")
+                .optionalAttributes("businessCategory", "x121Address", "registeredAddress", "destinationIndicator",
+                        "preferredDeliveryMethod", "telexNumber", "teletexTerminalIdentifier", "telephoneNumber",
+                        "internationalISDNNumber", "facsimileTelephoneNumber", "preferredDeliveryMethod", "street",
+                        "postOfficeBox", "postalCode", "postalAddress", "physicalDeliveryOfficeName", "st", "l")
+                .extraProperties(RFC4519_ORIGIN)
+                .addToSchema();
+    }
+
+    private static void addRFC4523(final SchemaBuilder builder) {
+        builder.buildSyntax(SYNTAX_CERTLIST_OID)
+               .description(SYNTAX_CERTLIST_DESCRIPTION)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new CertificateListSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_CERTPAIR_OID)
+               .description(SYNTAX_CERTPAIR_DESCRIPTION)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new CertificatePairSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_CERTIFICATE_OID)
+               .description(SYNTAX_CERTIFICATE_DESCRIPTION)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new CertificateSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_CERTIFICATE_EXACT_ASSERTION_OID)
+               .description(SYNTAX_CERTIFICATE_EXACT_ASSERTION_DESCRIPTION)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new CertificateExactAssertionSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_SUPPORTED_ALGORITHM_OID)
+               .description(SYNTAX_SUPPORTED_ALGORITHM_DESCRIPTION)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new SupportedAlgorithmSyntaxImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CERTIFICATE_EXACT_OID)
+               .names(EMR_CERTIFICATE_EXACT_NAME)
+               .syntaxOID(SYNTAX_CERTIFICATE_EXACT_ASSERTION_OID)
+               .extraProperties(RFC4523_ORIGIN)
+               .implementation(new CertificateExactMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.36")
+               .names("userCertificate")
+               .description("X.509 user certificate")
+               .equalityMatchingRule(EMR_CERTIFICATE_EXACT_OID)
+               .syntax(SYNTAX_CERTIFICATE_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.37")
+               .names("cACertificate")
+               .description("X.509 CA certificate")
+               .equalityMatchingRule(EMR_CERTIFICATE_EXACT_OID)
+               .syntax(SYNTAX_CERTIFICATE_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.38")
+               .names("authorityRevocationList")
+               .description("X.509 authority revocation list")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_CERTLIST_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.39")
+               .names("certificateRevocationList")
+               .description("X.509 certificate revocation list")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_CERTLIST_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.40")
+               .names("crossCertificatePair")
+               .description("X.509 cross certificate pair")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_CERTPAIR_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.52")
+               .names("supportedAlgorithms")
+               .description("X.509 supported algorithms")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_SUPPORTED_ALGORITHM_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.53")
+               .names("deltaRevocationList")
+               .description("X.509 delta revocation list")
+               .equalityMatchingRule(EMR_OCTET_STRING_OID)
+               .syntax(SYNTAX_CERTLIST_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4523_ORIGIN)
+               .addToSchema();
+
+        builder.buildObjectClass("2.5.6.21")
+                .names("pkiUser")
+                .type(AUXILIARY)
+                .description("X.509 PKI User")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .optionalAttributes("userCertificate")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.22")
+                .names("pkiCA")
+                .type(AUXILIARY)
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .description("X.509 PKI Certificate Authority")
+                .optionalAttributes("cACertificate", "certificateRevocationList",
+                    "authorityRevocationList", "crossCertificatePair")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.19")
+                .names("cRLDistributionPoint")
+                .description("X.509 CRL distribution point")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("cn")
+                .optionalAttributes("certificateRevocationList", "authorityRevocationList", "deltaRevocationList")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.23")
+                .names("deltaCRL")
+                .type(AUXILIARY)
+                .description("X.509 delta CRL")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .optionalAttributes("deltaRevocationList")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.15")
+                .names("strongAuthenticationUser")
+                .type(AUXILIARY)
+                .description("X.521 strong authentication user")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("userCertificate")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.18")
+                .names("userSecurityInformation")
+                .type(AUXILIARY)
+                .description("X.521 user security information")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .optionalAttributes("supportedAlgorithms")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.16")
+                .names("certificationAuthority")
+                .type(AUXILIARY)
+                .description("X.509 certificate authority")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("authorityRevocationList", "certificateRevocationList", "cACertificate")
+                .optionalAttributes("crossCertificatePair")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.16.2")
+                .names("certificationAuthority-V2")
+                .type(AUXILIARY)
+                .description("X.509 certificate authority, version 2")
+                .superiorObjectClasses("certificationAuthority")
+                .optionalAttributes("deltaRevocationList")
+                .extraProperties(RFC4523_ORIGIN)
+                .addToSchema();
+    }
+
+    private static void addRFC4530(final SchemaBuilder builder) {
+        builder.buildSyntax(SYNTAX_UUID_OID)
+               .description(SYNTAX_UUID_DESCRIPTION)
+               .extraProperties(RFC4530_ORIGIN)
+               .implementation(new UUIDSyntaxImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_UUID_OID)
+               .names(EMR_UUID_NAME)
+               .syntaxOID(SYNTAX_UUID_OID)
+               .extraProperties(RFC4530_ORIGIN)
+               .implementation(new UUIDEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_UUID_OID)
+               .names(OMR_UUID_NAME)
+               .syntaxOID(SYNTAX_UUID_OID)
+               .extraProperties(RFC4530_ORIGIN)
+               .implementation(new UUIDOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.1.16.4")
+               .names("entryUUID")
+               .description("UUID of the entry")
+               .equalityMatchingRule(EMR_UUID_OID)
+               .orderingMatchingRule(OMR_UUID_OID)
+               .syntax(SYNTAX_UUID_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4530_ORIGIN)
+               .addToSchema();
+    }
+
+    private static void addRFC5020(final SchemaBuilder builder) {
+        builder.buildAttributeType("1.3.6.1.1.20")
+               .names("entryDN")
+               .description("DN of the entry")
+               .equalityMatchingRule(EMR_DN_OID)
+               .syntax(SYNTAX_DN_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC5020_ORIGIN)
+               .addToSchema();
+    }
+
+    private static void addSunProprietary(final SchemaBuilder builder) {
+        builder.buildSyntax(SYNTAX_USER_PASSWORD_OID)
+               .description(SYNTAX_USER_PASSWORD_DESCRIPTION)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(new UserPasswordSyntaxImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_USER_PASSWORD_EXACT_OID)
+               .names(singletonList(EMR_USER_PASSWORD_EXACT_NAME))
+               .description(EMR_USER_PASSWORD_EXACT_DESCRIPTION)
+               .syntaxOID(SYNTAX_USER_PASSWORD_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(new UserPasswordExactEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(AMR_DOUBLE_METAPHONE_OID)
+               .names(singletonList(AMR_DOUBLE_METAPHONE_NAME))
+               .description(AMR_DOUBLE_METAPHONE_DESCRIPTION)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(new DoubleMetaphoneApproximateMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_RELATIVE_TIME_GREATER_THAN_OID)
+               .names(OMR_RELATIVE_TIME_GREATER_THAN_NAME, OMR_RELATIVE_TIME_GREATER_THAN_ALT_NAME)
+               .description(OMR_RELATIVE_TIME_GREATER_THAN_DESCRIPTION)
+               .syntaxOID(SYNTAX_GENERALIZED_TIME_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(relativeTimeGTOMatchingRule())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_RELATIVE_TIME_LESS_THAN_OID)
+               .names(OMR_RELATIVE_TIME_LESS_THAN_NAME, OMR_RELATIVE_TIME_LESS_THAN_ALT_NAME)
+               .description(OMR_RELATIVE_TIME_LESS_THAN_DESCRIPTION)
+               .syntaxOID(SYNTAX_GENERALIZED_TIME_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(relativeTimeLTOMatchingRule())
+               .addToSchema();
+
+        builder.buildMatchingRule(MR_PARTIAL_DATE_AND_TIME_OID)
+               .names(singletonList(MR_PARTIAL_DATE_AND_TIME_NAME))
+               .description(MR_PARTIAL_DATE_AND_TIME_DESCRIPTION)
+               .syntaxOID(SYNTAX_GENERALIZED_TIME_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(partialDateAndTimeMatchingRule())
+               .addToSchema();
+
+        addCollationMatchingRules(builder);
+    }
+
+    private static void addForgeRockProprietary(SchemaBuilder builder) {
+        builder.buildAttributeType("1.3.6.1.4.1.36733.2.1.1.141")
+               .names("fullVendorVersion")
+               .equalityMatchingRule(EMR_CASE_EXACT_IA5_OID)
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(OPENDJ_ORIGIN)
+               .addToSchema();
+    }
+
+    /**
+     * Adds the collation matching rules.
+     * <p>
+     * A set of collation matching rules is registered for each locale that is both available in the java runtime
+     * environment and has an oid defined in the {@code LOCALE_NAMES_TO_OIDS} map. Note that the same oid can be used
+     * for multiple locales (e.g., matching rule for "en" and "en-US" uses the same oid).
+     * <p>
+     * To add support for a new locale, add a corresponding entry in the {@code LOCALE_NAMES_TO_OIDS} map.
+     */
+    private static void addCollationMatchingRules(final SchemaBuilder builder) {
+        // Build an intermediate map to ensure each locale name appears only once
+        final Map<String, Locale> localesCache = new TreeMap<>();
+        for (Locale locale : Locale.getAvailableLocales()) {
+            localesCache.put(localeName(locale), locale);
+        }
+
+        // Build a intermediate map to list all available oids with their locale names
+        // An oid can be associated to multiple locale names
+        final Map<String, List<String>> oidsCache = new HashMap<>();
+        for (final String localeName: localesCache.keySet()) {
+            String oid = JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS.get(localeName);
+            if (oid != null) {
+                List<String> names = oidsCache.get(oid);
+                if (names == null) {
+                    names = new ArrayList<>(5);
+                    oidsCache.put(oid, names);
+                }
+                names.add(localeName);
+            }
+        }
+
+        // Now build the matching rules from all available oids
+        for (final Entry<String, List<String>> entry : oidsCache.entrySet()) {
+            final String oid = entry.getKey();
+            final List<String> names = entry.getValue();
+            // take locale from first name - all locales of names are considered equivalent here
+            final Locale locale = localesCache.get(names.get(0));
+            addCollationMatchingRule(builder, oid, names, 1, "lt", collationLessThanMatchingRule(locale));
+            addCollationMatchingRule(builder, oid, names, 2, "lte", collationLessThanOrEqualMatchingRule(locale));
+            MatchingRuleImpl collationEqualityMatchingRule = collationEqualityMatchingRule(locale);
+            addCollationMatchingRule(builder, oid, names, 3, "eq", collationEqualityMatchingRule);
+            // the default oid is registered with equality matching rule
+            final int ignored = 0;
+            addCollationMatchingRule(builder, oid, names, ignored, "", collationEqualityMatchingRule);
+            addCollationMatchingRule(builder, oid, names, 4, "gte", collationGreaterThanOrEqualToMatchingRule(locale));
+            addCollationMatchingRule(builder, oid, names, 5, "gt", collationGreaterThanMatchingRule(locale));
+            addCollationMatchingRule(builder, oid, names, 6, "sub", collationSubstringMatchingRule(locale));
+        }
+    }
+
+    /** Add a specific collation matching rule to the schema. */
+    private static void addCollationMatchingRule(final SchemaBuilder builder, final String baseOid,
+            final List<String> names, final int numericSuffix, final String symbolicSuffix,
+            final MatchingRuleImpl matchingRuleImplementation) {
+        final String oid = symbolicSuffix.isEmpty() ? baseOid : baseOid + "." + numericSuffix;
+        builder.buildMatchingRule(oid)
+               .names(collationMatchingRuleNames(names, numericSuffix, symbolicSuffix))
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(OPENDS_ORIGIN)
+               .implementation(matchingRuleImplementation)
+               .addToSchema();
+    }
+
+    /**
+     * Build the complete list of names for a collation matching rule.
+     *
+     * @param localeNames
+     *            List of locale names that correspond to the matching rule (e.g., "en", "en-US")
+     * @param numSuffix
+     *            numeric suffix corresponding to type of matching rule (e.g, 5 for greater than matching rule). It is
+     *            ignored if equal to zero (0).
+     * @param symbolicSuffix
+     *            symbolic suffix corresponding to type of matching rule (e.g, "gt" for greater than matching rule). It
+     *            may be empty ("") to indicate the default rule.
+     * @return the names list (e.g, "en.5", "en.gt", "en-US.5", "en-US.gt")
+     */
+    private static String[] collationMatchingRuleNames(final List<String> localeNames, final int numSuffix,
+        final String symbolicSuffix) {
+        final List<String> names = new ArrayList<>();
+        for (String localeName : localeNames) {
+            if (symbolicSuffix.isEmpty()) {
+                // the default rule
+                names.add(localeName);
+            } else {
+                names.add(localeName + "." + numSuffix);
+                names.add(localeName + "." + symbolicSuffix);
+            }
+        }
+        return names.toArray(new String[names.size()]);
+    }
+
+    /**
+     * Returns the name corresponding to the provided locale.
+     * <p>
+     * The name is using format:
+     * <pre>
+     *   language code (lower case) + "-" + country code (upper case) + "-" + variant (upper case)
+     * </pre>
+     * Country code and variant are optional, they may not appear.
+     * See LOCALE_NAMES_TO_OIDS keys for examples of names
+     *
+     * @param locale
+     *          The locale
+     * @return the name associated to the locale
+     */
+    private static String localeName(final Locale locale) {
+        final StringBuilder name = new StringBuilder(locale.getLanguage());
+        final String country = locale.getCountry();
+        if (!country.isEmpty()) {
+            name.append('-').append(country);
+        }
+        final String variant = locale.getVariant();
+        if (!variant.isEmpty()) {
+            name.append('-').append(variant.toUpperCase());
+        }
+        return name.toString();
+    }
+
+    private static void defaultAttributeTypes(final SchemaBuilder builder) {
+        builder.buildAttributeType("2.5.4.0")
+               .names("objectClass")
+               .equalityMatchingRule(EMR_OID_NAME)
+               .syntax(SYNTAX_OID_OID)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.4.1")
+               .names("aliasedObjectName")
+               .equalityMatchingRule(EMR_DN_NAME)
+               .syntax(SYNTAX_DN_OID)
+               .singleValue(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.18.1")
+               .names("createTimestamp")
+               .equalityMatchingRule(EMR_GENERALIZED_TIME_NAME)
+               .orderingMatchingRule(OMR_GENERALIZED_TIME_NAME)
+               .syntax(SYNTAX_GENERALIZED_TIME_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.18.2")
+               .names("modifyTimestamp")
+               .equalityMatchingRule(EMR_GENERALIZED_TIME_NAME)
+               .orderingMatchingRule(OMR_GENERALIZED_TIME_NAME)
+               .syntax(SYNTAX_GENERALIZED_TIME_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.18.3")
+               .names("creatorsName")
+               .equalityMatchingRule(EMR_DN_NAME)
+               .syntax(SYNTAX_DN_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.18.4")
+               .names("modifiersName")
+               .equalityMatchingRule(EMR_DN_NAME)
+               .syntax(SYNTAX_DN_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.18.10")
+               .names("subschemaSubentry")
+               .equalityMatchingRule(EMR_DN_NAME)
+               .syntax(SYNTAX_DN_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.5")
+               .names("attributeTypes")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_ATTRIBUTE_TYPE_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.6")
+               .names("objectClasses")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_OBJECTCLASS_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.4")
+               .names("matchingRules")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_MATCHING_RULE_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.8")
+               .names("matchingRuleUse")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_MATCHING_RULE_USE_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.9")
+               .names("structuralObjectClass")
+               .equalityMatchingRule(EMR_OID_NAME)
+               .syntax(SYNTAX_OID_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.10")
+               .names("governingStructureRule")
+               .equalityMatchingRule(EMR_INTEGER_NAME)
+               .syntax(SYNTAX_INTEGER_OID)
+               .singleValue(true)
+               .noUserModification(true)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.5")
+               .names("namingContexts")
+               .syntax(SYNTAX_DN_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.6")
+               .names("altServer")
+               .syntax(SYNTAX_IA5_STRING_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.7")
+               .names("supportedExtension")
+               .syntax(SYNTAX_OID_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.13")
+               .names("supportedControl")
+               .syntax(SYNTAX_OID_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.14")
+               .names("supportedSASLMechanisms")
+               .syntax(SYNTAX_DIRECTORY_STRING_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.4203.1.3.5")
+               .names("supportedFeatures")
+               .equalityMatchingRule(EMR_OID_NAME)
+               .syntax(SYNTAX_OID_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.15")
+               .names("supportedLDAPVersion")
+               .syntax(SYNTAX_INTEGER_OID)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("1.3.6.1.4.1.1466.101.120.16")
+               .names("ldapSyntaxes")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_LDAP_SYNTAX_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.1")
+               .names("ditStructureRules")
+               .equalityMatchingRule(EMR_INTEGER_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_DIT_STRUCTURE_RULE_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.7")
+               .names("nameForms")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_NAME_FORM_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+
+        builder.buildAttributeType("2.5.21.2")
+               .names("ditContentRules")
+               .equalityMatchingRule(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntax(SYNTAX_DIT_CONTENT_RULE_OID)
+               .usage(AttributeUsage.DIRECTORY_OPERATION)
+               .extraProperties(RFC4512_ORIGIN)
+               .addToSchema();
+    }
+
+    private static void defaultMatchingRules(final SchemaBuilder builder) {
+        builder.buildMatchingRule(EMR_BIT_STRING_OID)
+               .names(EMR_BIT_STRING_NAME)
+               .syntaxOID(SYNTAX_BIT_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new BitStringEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_BOOLEAN_OID)
+               .names(EMR_BOOLEAN_NAME)
+               .syntaxOID(SYNTAX_BOOLEAN_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new BooleanEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CASE_EXACT_IA5_OID)
+               .names(EMR_CASE_EXACT_IA5_NAME)
+               .syntaxOID(SYNTAX_IA5_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseExactIA5EqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_CASE_EXACT_IA5_OID)
+               .names(SMR_CASE_EXACT_IA5_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseExactIA5SubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CASE_EXACT_OID)
+               .names(EMR_CASE_EXACT_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseExactEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_CASE_EXACT_OID)
+               .names(OMR_CASE_EXACT_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseExactOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_CASE_EXACT_OID)
+               .names(SMR_CASE_EXACT_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseExactSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CASE_IGNORE_IA5_OID)
+               .names(EMR_CASE_IGNORE_IA5_NAME)
+               .syntaxOID(SYNTAX_IA5_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreIA5EqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_CASE_IGNORE_IA5_OID)
+               .names(SMR_CASE_IGNORE_IA5_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreIA5SubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CASE_IGNORE_LIST_OID)
+               .names(EMR_CASE_IGNORE_LIST_NAME)
+               .syntaxOID(SYNTAX_POSTAL_ADDRESS_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreListEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_CASE_IGNORE_LIST_OID)
+               .names(SMR_CASE_IGNORE_LIST_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreListSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_CASE_IGNORE_OID)
+               .names(EMR_CASE_IGNORE_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_CASE_IGNORE_OID)
+               .names(OMR_CASE_IGNORE_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_CASE_IGNORE_OID)
+               .names(SMR_CASE_IGNORE_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CaseIgnoreSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_DIRECTORY_STRING_FIRST_COMPONENT_OID)
+               .names(singletonList(EMR_DIRECTORY_STRING_FIRST_COMPONENT_NAME))
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_DN_OID)
+               .names(EMR_DN_NAME)
+               .syntaxOID(SYNTAX_DN_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DistinguishedNameEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_GENERALIZED_TIME_OID)
+               .names(EMR_GENERALIZED_TIME_NAME)
+               .syntaxOID(SYNTAX_GENERALIZED_TIME_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new GeneralizedTimeEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_GENERALIZED_TIME_OID)
+               .names(OMR_GENERALIZED_TIME_NAME)
+               .syntaxOID(SYNTAX_GENERALIZED_TIME_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new GeneralizedTimeOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_INTEGER_FIRST_COMPONENT_OID)
+               .names(EMR_INTEGER_FIRST_COMPONENT_NAME)
+               .syntaxOID(SYNTAX_INTEGER_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new IntegerFirstComponentEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_INTEGER_OID)
+               .names(EMR_INTEGER_NAME)
+               .syntaxOID(SYNTAX_INTEGER_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new IntegerEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_INTEGER_OID)
+               .names(OMR_INTEGER_NAME)
+               .syntaxOID(SYNTAX_INTEGER_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new IntegerOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_KEYWORD_OID)
+               .names(EMR_KEYWORD_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new KeywordEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_NUMERIC_STRING_OID)
+               .names(EMR_NUMERIC_STRING_NAME)
+               .syntaxOID(SYNTAX_NUMERIC_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new NumericStringEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_NUMERIC_STRING_OID)
+               .names(OMR_NUMERIC_STRING_NAME)
+               .syntaxOID(SYNTAX_NUMERIC_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new NumericStringOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_NUMERIC_STRING_OID)
+               .names(SMR_NUMERIC_STRING_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new NumericStringSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_OID_FIRST_COMPONENT_OID)
+               .names(EMR_OID_FIRST_COMPONENT_NAME)
+               .syntaxOID(SYNTAX_OID_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new ObjectIdentifierFirstComponentEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_OID_OID)
+               .names(EMR_OID_NAME)
+               .syntaxOID(SYNTAX_OID_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new ObjectIdentifierEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_OCTET_STRING_OID)
+               .names(EMR_OCTET_STRING_NAME)
+               .syntaxOID(SYNTAX_OCTET_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new OctetStringEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(OMR_OCTET_STRING_OID)
+               .names(OMR_OCTET_STRING_NAME)
+               .syntaxOID(SYNTAX_OCTET_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new OctetStringOrderingMatchingRuleImpl())
+               .addToSchema();
+
+        // SMR octet string is not in any LDAP RFC and its from X.500
+        builder.buildMatchingRule(SMR_OCTET_STRING_OID)
+               .names(SMR_OCTET_STRING_NAME)
+               .syntaxOID(SYNTAX_OCTET_STRING_OID)
+               .extraProperties(X500_ORIGIN)
+               .implementation(new OctetStringSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        // Depreciated in RFC 4512
+        builder.buildMatchingRule(EMR_PROTOCOL_INFORMATION_OID)
+               .names(EMR_PROTOCOL_INFORMATION_NAME)
+               .syntaxOID(SYNTAX_PROTOCOL_INFORMATION_OID)
+               .extraProperties(RFC2252_ORIGIN)
+               .implementation(new ProtocolInformationEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        // Depreciated in RFC 4512
+        builder.buildMatchingRule(EMR_PRESENTATION_ADDRESS_OID)
+               .names(EMR_PRESENTATION_ADDRESS_NAME)
+               .syntaxOID(SYNTAX_PRESENTATION_ADDRESS_OID)
+               .extraProperties(RFC2252_ORIGIN)
+               .implementation(new PresentationAddressEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_TELEPHONE_OID)
+               .names(EMR_TELEPHONE_NAME)
+               .syntaxOID(SYNTAX_TELEPHONE_OID)
+               .extraProperties(RFC2252_ORIGIN)
+               .implementation(new TelephoneNumberEqualityMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(SMR_TELEPHONE_OID)
+               .names(SMR_TELEPHONE_NAME)
+               .syntaxOID(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new TelephoneNumberSubstringMatchingRuleImpl())
+               .addToSchema();
+
+        builder.buildMatchingRule(EMR_UNIQUE_MEMBER_OID)
+                .names(EMR_UNIQUE_MEMBER_NAME)
+                .syntaxOID(SYNTAX_NAME_AND_OPTIONAL_UID_OID)
+                .extraProperties(RFC4512_ORIGIN)
+                .implementation(new UniqueMemberEqualityMatchingRuleImpl())
+                .addToSchema();
+
+        builder.buildMatchingRule(EMR_WORD_OID)
+               .names(EMR_WORD_NAME)
+               .syntaxOID(SYNTAX_DIRECTORY_STRING_OID)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new KeywordEqualityMatchingRuleImpl())
+               .addToSchema();
+    }
+
+    private static void defaultObjectClasses(final SchemaBuilder builder) {
+        builder.buildObjectClass(TOP_OBJECTCLASS_OID)
+                .names(TOP_OBJECTCLASS_NAME)
+                .type(ABSTRACT)
+                .description(TOP_OBJECTCLASS_DESCRIPTION)
+                .requiredAttributes("objectClass")
+                .extraProperties(RFC4512_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.6.1")
+                .names("alias")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("aliasedObjectName")
+                .extraProperties(RFC4512_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass(EXTENSIBLE_OBJECT_OBJECTCLASS_OID)
+                .names(EXTENSIBLE_OBJECT_OBJECTCLASS_NAME)
+                .type(AUXILIARY)
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .extraProperties(RFC4512_ORIGIN)
+                .addToSchema();
+
+        builder.buildObjectClass("2.5.20.1")
+                .names("subschema")
+                .type(AUXILIARY)
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .optionalAttributes("dITStructureRules", "nameForms", "ditContentRules", "objectClasses",
+                    "attributeTypes", "matchingRules", "matchingRuleUse")
+                .extraProperties(RFC4512_ORIGIN)
+                .addToSchema();
+    }
+
+    private static void defaultSyntaxes(final SchemaBuilder builder) {
+        // All RFC 4512 / 4517
+        builder.buildSyntax(SYNTAX_ATTRIBUTE_TYPE_OID)
+               .description(SYNTAX_ATTRIBUTE_TYPE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new AttributeTypeSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_BINARY_OID)
+               .description(SYNTAX_BINARY_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new BinarySyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_BIT_STRING_OID)
+               .description(SYNTAX_BIT_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new BitStringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_BOOLEAN_OID)
+               .description(SYNTAX_BOOLEAN_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new BooleanSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_COUNTRY_STRING_OID)
+               .description(SYNTAX_COUNTRY_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new CountryStringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_DELIVERY_METHOD_OID)
+               .description(SYNTAX_DELIVERY_METHOD_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DeliveryMethodSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_DIRECTORY_STRING_OID)
+               .description(SYNTAX_DIRECTORY_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DirectoryStringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_DIT_CONTENT_RULE_OID)
+               .description(SYNTAX_DIT_CONTENT_RULE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DITContentRuleSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_DIT_STRUCTURE_RULE_OID)
+               .description(SYNTAX_DIT_STRUCTURE_RULE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DITStructureRuleSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_DN_OID)
+               .description(SYNTAX_DN_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new DistinguishedNameSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_ENHANCED_GUIDE_OID)
+               .description(SYNTAX_ENHANCED_GUIDE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new EnhancedGuideSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_FAXNUMBER_OID)
+               .description(SYNTAX_FAXNUMBER_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new FacsimileNumberSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_FAX_OID)
+               .description(SYNTAX_FAX_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new FaxSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_GENERALIZED_TIME_OID)
+               .description(SYNTAX_GENERALIZED_TIME_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new GeneralizedTimeSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_GUIDE_OID)
+               .description(SYNTAX_GUIDE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new GuideSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_IA5_STRING_OID)
+               .description(SYNTAX_IA5_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new IA5StringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_INTEGER_OID)
+               .description(SYNTAX_INTEGER_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new IntegerSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_JPEG_OID)
+               .description(SYNTAX_JPEG_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new JPEGSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_MATCHING_RULE_OID)
+               .description(SYNTAX_MATCHING_RULE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new MatchingRuleSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_MATCHING_RULE_USE_OID)
+               .description(SYNTAX_MATCHING_RULE_USE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new MatchingRuleUseSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_LDAP_SYNTAX_OID)
+               .description(SYNTAX_LDAP_SYNTAX_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new LDAPSyntaxDescriptionSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_NAME_AND_OPTIONAL_UID_OID)
+               .description(SYNTAX_NAME_AND_OPTIONAL_UID_DESCRIPTION)
+               .extraProperties(RFC4517_ORIGIN)
+               .implementation(new NameAndOptionalUIDSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_NAME_FORM_OID)
+               .description(SYNTAX_NAME_FORM_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new NameFormSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_NUMERIC_STRING_OID)
+               .description(SYNTAX_NUMERIC_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new NumericStringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_OBJECTCLASS_OID)
+               .description(SYNTAX_OBJECTCLASS_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new ObjectClassSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_OCTET_STRING_OID)
+               .description(SYNTAX_OCTET_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new OctetStringSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_OID_OID)
+               .description(SYNTAX_OID_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new OIDSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_OTHER_MAILBOX_OID)
+               .description(SYNTAX_OTHER_MAILBOX_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new OtherMailboxSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_POSTAL_ADDRESS_OID)
+               .description(SYNTAX_POSTAL_ADDRESS_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new PostalAddressSyntaxImpl())
+               .addToSchema();
+
+        // Depreciated in RFC 4512
+        builder.buildSyntax(SYNTAX_PRESENTATION_ADDRESS_OID)
+               .description(SYNTAX_PRESENTATION_ADDRESS_DESCRIPTION)
+               .extraProperties(RFC2252_ORIGIN)
+               .implementation(new PresentationAddressSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_PRINTABLE_STRING_OID)
+               .description(SYNTAX_PRINTABLE_STRING_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new PrintableStringSyntaxImpl())
+               .addToSchema();
+
+        // Depreciated in RFC 4512
+        builder.buildSyntax(SYNTAX_PROTOCOL_INFORMATION_OID)
+               .description(SYNTAX_PROTOCOL_INFORMATION_DESCRIPTION)
+               .extraProperties(RFC2252_ORIGIN)
+               .implementation(new ProtocolInformationSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_SUBSTRING_ASSERTION_OID)
+               .description(SYNTAX_SUBSTRING_ASSERTION_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new SubstringAssertionSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_TELEPHONE_OID)
+               .description(SYNTAX_TELEPHONE_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new TelephoneNumberSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_TELETEX_TERM_ID_OID)
+               .description(SYNTAX_TELETEX_TERM_ID_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new TeletexTerminalIdentifierSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_TELEX_OID)
+               .description(SYNTAX_TELEX_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new TelexNumberSyntaxImpl())
+               .addToSchema();
+
+        builder.buildSyntax(SYNTAX_UTC_TIME_OID)
+               .description(SYNTAX_UTC_TIME_DESCRIPTION)
+               .extraProperties(RFC4512_ORIGIN)
+               .implementation(new UTCTimeSyntaxImpl())
+               .addToSchema();
+
+    }
+
+    private CoreSchemaImpl() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaSupportedLocales.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaSupportedLocales.java
new file mode 100644
index 0000000..f71a3cf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaSupportedLocales.java
@@ -0,0 +1,250 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Provides a map of supported locale tags to OIDs.
+ */
+public final class CoreSchemaSupportedLocales {
+    /**
+     * Returns the unmodifiable map of JVM supported locale tags to OIDs.
+     * @return The unmodifiable map of JVM supported locale tags to OIDs.
+     */
+    public static Map<String, String> getJvmSupportedLocaleNamesToOids() {
+        return Collections.unmodifiableMap(JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS);
+    }
+
+    /**
+     * Provides the oid associated to each locale, for the registration of collation matching rules.
+     * <p>
+     * To add support for a new locale to collation matching rules, add its name as key and the corresponding oid as
+     * value.
+     */
+    private static final Map<String, String> LOCALE_NAMES_TO_OIDS = new HashMap<>();
+
+    /**
+     * Same as {@link CoreSchemaSupportedLocales#LOCALE_NAMES_TO_OIDS}, but it contains the old locale names
+     * that will be used for the registration of collation matching rules.
+     * It is automatically populated on static initialization of current class.
+     * It allows the initialization process to complete when newer locales are referenced by config.ldif,
+     * but the JVM only works with the old locale names.
+     * @see CoreSchemaSupportedLocales#LOCALE_NAMES_TO_OIDS
+     */
+    private static final Map<String, String> JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS = new HashMap<>();
+
+    static {
+        LOCALE_NAMES_TO_OIDS.put("af", "1.3.6.1.4.1.42.2.27.9.4.1.1");
+        LOCALE_NAMES_TO_OIDS.put("am", "1.3.6.1.4.1.42.2.27.9.4.2.1");
+        LOCALE_NAMES_TO_OIDS.put("ar", "1.3.6.1.4.1.42.2.27.9.4.3.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-AE", "1.3.6.1.4.1.42.2.27.9.4.4.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-BH", "1.3.6.1.4.1.42.2.27.9.4.5.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-DZ", "1.3.6.1.4.1.42.2.27.9.4.6.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-EG", "1.3.6.1.4.1.42.2.27.9.4.7.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-IN", "1.3.6.1.4.1.42.2.27.9.4.8.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-IQ", "1.3.6.1.4.1.42.2.27.9.4.9.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-JO", "1.3.6.1.4.1.42.2.27.9.4.10.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-KW", "1.3.6.1.4.1.42.2.27.9.4.11.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-LB", "1.3.6.1.4.1.42.2.27.9.4.12.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-LY", "1.3.6.1.4.1.42.2.27.9.4.13.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-MA", "1.3.6.1.4.1.42.2.27.9.4.14.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-OM", "1.3.6.1.4.1.42.2.27.9.4.15.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-QA", "1.3.6.1.4.1.42.2.27.9.4.16.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-SA", "1.3.6.1.4.1.42.2.27.9.4.17.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-SD", "1.3.6.1.4.1.42.2.27.9.4.18.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-SY", "1.3.6.1.4.1.42.2.27.9.4.19.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-TN", "1.3.6.1.4.1.42.2.27.9.4.20.1");
+        LOCALE_NAMES_TO_OIDS.put("ar-YE", "1.3.6.1.4.1.42.2.27.9.4.21.1");
+        LOCALE_NAMES_TO_OIDS.put("be", "1.3.6.1.4.1.42.2.27.9.4.22.1");
+        LOCALE_NAMES_TO_OIDS.put("bg", "1.3.6.1.4.1.42.2.27.9.4.23.1");
+        LOCALE_NAMES_TO_OIDS.put("bn", "1.3.6.1.4.1.42.2.27.9.4.24.1");
+        LOCALE_NAMES_TO_OIDS.put("ca", "1.3.6.1.4.1.42.2.27.9.4.25.1");
+        LOCALE_NAMES_TO_OIDS.put("cs", "1.3.6.1.4.1.42.2.27.9.4.26.1");
+        LOCALE_NAMES_TO_OIDS.put("da", "1.3.6.1.4.1.42.2.27.9.4.27.1");
+        LOCALE_NAMES_TO_OIDS.put("de", "1.3.6.1.4.1.42.2.27.9.4.28.1");
+        LOCALE_NAMES_TO_OIDS.put("de-DE", "1.3.6.1.4.1.42.2.27.9.4.28.1");
+        LOCALE_NAMES_TO_OIDS.put("de-AT", "1.3.6.1.4.1.42.2.27.9.4.29.1");
+        LOCALE_NAMES_TO_OIDS.put("de-BE", "1.3.6.1.4.1.42.2.27.9.4.30.1");
+        LOCALE_NAMES_TO_OIDS.put("de-CH", "1.3.6.1.4.1.42.2.27.9.4.31.1");
+        LOCALE_NAMES_TO_OIDS.put("de-LU", "1.3.6.1.4.1.42.2.27.9.4.32.1");
+        LOCALE_NAMES_TO_OIDS.put("el", "1.3.6.1.4.1.42.2.27.9.4.33.1");
+        LOCALE_NAMES_TO_OIDS.put("en", "1.3.6.1.4.1.42.2.27.9.4.34.1");
+        LOCALE_NAMES_TO_OIDS.put("en-US", "1.3.6.1.4.1.42.2.27.9.4.34.1");
+        LOCALE_NAMES_TO_OIDS.put("en-AU", "1.3.6.1.4.1.42.2.27.9.4.35.1");
+        LOCALE_NAMES_TO_OIDS.put("en-CA", "1.3.6.1.4.1.42.2.27.9.4.36.1");
+        LOCALE_NAMES_TO_OIDS.put("en-GB", "1.3.6.1.4.1.42.2.27.9.4.37.1");
+        LOCALE_NAMES_TO_OIDS.put("en-HK", "1.3.6.1.4.1.42.2.27.9.4.38.1");
+        LOCALE_NAMES_TO_OIDS.put("en-IE", "1.3.6.1.4.1.42.2.27.9.4.39.1");
+        LOCALE_NAMES_TO_OIDS.put("en-IN", "1.3.6.1.4.1.42.2.27.9.4.40.1");
+        LOCALE_NAMES_TO_OIDS.put("en-MT", "1.3.6.1.4.1.42.2.27.9.4.41.1");
+        LOCALE_NAMES_TO_OIDS.put("en-NZ", "1.3.6.1.4.1.42.2.27.9.4.42.1");
+        LOCALE_NAMES_TO_OIDS.put("en-PH", "1.3.6.1.4.1.42.2.27.9.4.43.1");
+        LOCALE_NAMES_TO_OIDS.put("en-SG", "1.3.6.1.4.1.42.2.27.9.4.44.1");
+        LOCALE_NAMES_TO_OIDS.put("en-VI", "1.3.6.1.4.1.42.2.27.9.4.45.1");
+        LOCALE_NAMES_TO_OIDS.put("en-ZA", "1.3.6.1.4.1.42.2.27.9.4.46.1");
+        LOCALE_NAMES_TO_OIDS.put("en-ZW", "1.3.6.1.4.1.42.2.27.9.4.47.1");
+        LOCALE_NAMES_TO_OIDS.put("eo", "1.3.6.1.4.1.42.2.27.9.4.48.1");
+        LOCALE_NAMES_TO_OIDS.put("es", "1.3.6.1.4.1.42.2.27.9.4.49.1");
+        LOCALE_NAMES_TO_OIDS.put("es-ES", "1.3.6.1.4.1.42.2.27.9.4.49.1");
+        LOCALE_NAMES_TO_OIDS.put("es-AR", "1.3.6.1.4.1.42.2.27.9.4.50.1");
+        LOCALE_NAMES_TO_OIDS.put("es-BO", "1.3.6.1.4.1.42.2.27.9.4.51.1");
+        LOCALE_NAMES_TO_OIDS.put("es-CL", "1.3.6.1.4.1.42.2.27.9.4.52.1");
+        LOCALE_NAMES_TO_OIDS.put("es-CO", "1.3.6.1.4.1.42.2.27.9.4.53.1");
+        LOCALE_NAMES_TO_OIDS.put("es-CR", "1.3.6.1.4.1.42.2.27.9.4.54.1");
+        LOCALE_NAMES_TO_OIDS.put("es-DO", "1.3.6.1.4.1.42.2.27.9.4.55.1");
+        LOCALE_NAMES_TO_OIDS.put("es-EC", "1.3.6.1.4.1.42.2.27.9.4.56.1");
+        LOCALE_NAMES_TO_OIDS.put("es-GT", "1.3.6.1.4.1.42.2.27.9.4.57.1");
+        LOCALE_NAMES_TO_OIDS.put("es-HN", "1.3.6.1.4.1.42.2.27.9.4.58.1");
+        LOCALE_NAMES_TO_OIDS.put("es-MX", "1.3.6.1.4.1.42.2.27.9.4.59.1");
+        LOCALE_NAMES_TO_OIDS.put("es-NI", "1.3.6.1.4.1.42.2.27.9.4.60.1");
+        LOCALE_NAMES_TO_OIDS.put("es-PA", "1.3.6.1.4.1.42.2.27.9.4.61.1");
+        LOCALE_NAMES_TO_OIDS.put("es-PE", "1.3.6.1.4.1.42.2.27.9.4.62.1");
+        LOCALE_NAMES_TO_OIDS.put("es-PR", "1.3.6.1.4.1.42.2.27.9.4.63.1");
+        LOCALE_NAMES_TO_OIDS.put("es-PY", "1.3.6.1.4.1.42.2.27.9.4.64.1");
+        LOCALE_NAMES_TO_OIDS.put("es-SV", "1.3.6.1.4.1.42.2.27.9.4.65.1");
+        LOCALE_NAMES_TO_OIDS.put("es-US", "1.3.6.1.4.1.42.2.27.9.4.66.1");
+        LOCALE_NAMES_TO_OIDS.put("es-UY", "1.3.6.1.4.1.42.2.27.9.4.67.1");
+        LOCALE_NAMES_TO_OIDS.put("es-VE", "1.3.6.1.4.1.42.2.27.9.4.68.1");
+        LOCALE_NAMES_TO_OIDS.put("et", "1.3.6.1.4.1.42.2.27.9.4.69.1");
+        LOCALE_NAMES_TO_OIDS.put("eu", "1.3.6.1.4.1.42.2.27.9.4.70.1");
+        LOCALE_NAMES_TO_OIDS.put("fa", "1.3.6.1.4.1.42.2.27.9.4.71.1");
+        LOCALE_NAMES_TO_OIDS.put("fa-IN", "1.3.6.1.4.1.42.2.27.9.4.72.1");
+        LOCALE_NAMES_TO_OIDS.put("fa-IR", "1.3.6.1.4.1.42.2.27.9.4.73.1");
+        LOCALE_NAMES_TO_OIDS.put("fi", "1.3.6.1.4.1.42.2.27.9.4.74.1");
+        LOCALE_NAMES_TO_OIDS.put("fo", "1.3.6.1.4.1.42.2.27.9.4.75.1");
+        LOCALE_NAMES_TO_OIDS.put("fr", "1.3.6.1.4.1.42.2.27.9.4.76.1");
+        LOCALE_NAMES_TO_OIDS.put("fr-FR", "1.3.6.1.4.1.42.2.27.9.4.76.1");
+        LOCALE_NAMES_TO_OIDS.put("fr-BE", "1.3.6.1.4.1.42.2.27.9.4.77.1");
+        LOCALE_NAMES_TO_OIDS.put("fr-CA", "1.3.6.1.4.1.42.2.27.9.4.78.1");
+        LOCALE_NAMES_TO_OIDS.put("fr-CH", "1.3.6.1.4.1.42.2.27.9.4.79.1");
+        LOCALE_NAMES_TO_OIDS.put("fr-LU", "1.3.6.1.4.1.42.2.27.9.4.80.1");
+        LOCALE_NAMES_TO_OIDS.put("ga", "1.3.6.1.4.1.42.2.27.9.4.81.1");
+        LOCALE_NAMES_TO_OIDS.put("gl", "1.3.6.1.4.1.42.2.27.9.4.82.1");
+        LOCALE_NAMES_TO_OIDS.put("gu", "1.3.6.1.4.1.42.2.27.9.4.83.1");
+        LOCALE_NAMES_TO_OIDS.put("gv", "1.3.6.1.4.1.42.2.27.9.4.84.1");
+        LOCALE_NAMES_TO_OIDS.put("he", "1.3.6.1.4.1.42.2.27.9.4.85.1");
+        LOCALE_NAMES_TO_OIDS.put("hi", "1.3.6.1.4.1.42.2.27.9.4.86.1");
+        LOCALE_NAMES_TO_OIDS.put("hr", "1.3.6.1.4.1.42.2.27.9.4.87.1");
+        LOCALE_NAMES_TO_OIDS.put("hu", "1.3.6.1.4.1.42.2.27.9.4.88.1");
+        LOCALE_NAMES_TO_OIDS.put("hy", "1.3.6.1.4.1.42.2.27.9.4.89.1");
+        LOCALE_NAMES_TO_OIDS.put("id", "1.3.6.1.4.1.42.2.27.9.4.90.1");
+        LOCALE_NAMES_TO_OIDS.put("is", "1.3.6.1.4.1.42.2.27.9.4.91.1");
+        LOCALE_NAMES_TO_OIDS.put("it", "1.3.6.1.4.1.42.2.27.9.4.92.1");
+        LOCALE_NAMES_TO_OIDS.put("it-CH", "1.3.6.1.4.1.42.2.27.9.4.93.1");
+        LOCALE_NAMES_TO_OIDS.put("ja", "1.3.6.1.4.1.42.2.27.9.4.94.1");
+        LOCALE_NAMES_TO_OIDS.put("kl", "1.3.6.1.4.1.42.2.27.9.4.95.1");
+        LOCALE_NAMES_TO_OIDS.put("kn", "1.3.6.1.4.1.42.2.27.9.4.96.1");
+        LOCALE_NAMES_TO_OIDS.put("ko", "1.3.6.1.4.1.42.2.27.9.4.97.1");
+        LOCALE_NAMES_TO_OIDS.put("kok", "1.3.6.1.4.1.42.2.27.9.4.98.1");
+        LOCALE_NAMES_TO_OIDS.put("kw", "1.3.6.1.4.1.42.2.27.9.4.99.1");
+        LOCALE_NAMES_TO_OIDS.put("lt", "1.3.6.1.4.1.42.2.27.9.4.100.1");
+        LOCALE_NAMES_TO_OIDS.put("lv", "1.3.6.1.4.1.42.2.27.9.4.101.1");
+        LOCALE_NAMES_TO_OIDS.put("mk", "1.3.6.1.4.1.42.2.27.9.4.102.1");
+        LOCALE_NAMES_TO_OIDS.put("mr", "1.3.6.1.4.1.42.2.27.9.4.103.1");
+        LOCALE_NAMES_TO_OIDS.put("mt", "1.3.6.1.4.1.42.2.27.9.4.104.1");
+        LOCALE_NAMES_TO_OIDS.put("nl", "1.3.6.1.4.1.42.2.27.9.4.105.1");
+        LOCALE_NAMES_TO_OIDS.put("nl-NL", "1.3.6.1.4.1.42.2.27.9.4.105.1");
+        LOCALE_NAMES_TO_OIDS.put("nl-BE", "1.3.6.1.4.1.42.2.27.9.4.106.1");
+        LOCALE_NAMES_TO_OIDS.put("no", "1.3.6.1.4.1.42.2.27.9.4.107.1");
+        LOCALE_NAMES_TO_OIDS.put("no-NO", "1.3.6.1.4.1.42.2.27.9.4.107.1");
+        LOCALE_NAMES_TO_OIDS.put("no-NO-NY", "1.3.6.1.4.1.42.2.27.9.4.108.1");
+        LOCALE_NAMES_TO_OIDS.put("nn", "1.3.6.1.4.1.42.2.27.9.4.109.1");
+        LOCALE_NAMES_TO_OIDS.put("nb", "1.3.6.1.4.1.42.2.27.9.4.110.1");
+        LOCALE_NAMES_TO_OIDS.put("no-NO-B", "1.3.6.1.4.1.42.2.27.9.4.110.1");
+        LOCALE_NAMES_TO_OIDS.put("om", "1.3.6.1.4.1.42.2.27.9.4.111.1");
+        LOCALE_NAMES_TO_OIDS.put("om-ET", "1.3.6.1.4.1.42.2.27.9.4.112.1");
+        LOCALE_NAMES_TO_OIDS.put("om-KE", "1.3.6.1.4.1.42.2.27.9.4.113.1");
+        LOCALE_NAMES_TO_OIDS.put("pl", "1.3.6.1.4.1.42.2.27.9.4.114.1");
+        LOCALE_NAMES_TO_OIDS.put("pt", "1.3.6.1.4.1.42.2.27.9.4.115.1");
+        LOCALE_NAMES_TO_OIDS.put("pt-PT", "1.3.6.1.4.1.42.2.27.9.4.115.1");
+        LOCALE_NAMES_TO_OIDS.put("pt-BR", "1.3.6.1.4.1.42.2.27.9.4.116.1");
+        LOCALE_NAMES_TO_OIDS.put("ro", "1.3.6.1.4.1.42.2.27.9.4.117.1");
+        LOCALE_NAMES_TO_OIDS.put("ru", "1.3.6.1.4.1.42.2.27.9.4.118.1");
+        LOCALE_NAMES_TO_OIDS.put("ru-RU", "1.3.6.1.4.1.42.2.27.9.4.118.1");
+        LOCALE_NAMES_TO_OIDS.put("ru-UA", "1.3.6.1.4.1.42.2.27.9.4.119.1");
+        LOCALE_NAMES_TO_OIDS.put("sh", "1.3.6.1.4.1.42.2.27.9.4.120.1");
+        LOCALE_NAMES_TO_OIDS.put("sk", "1.3.6.1.4.1.42.2.27.9.4.121.1");
+        LOCALE_NAMES_TO_OIDS.put("sl", "1.3.6.1.4.1.42.2.27.9.4.122.1");
+        LOCALE_NAMES_TO_OIDS.put("so", "1.3.6.1.4.1.42.2.27.9.4.123.1");
+        LOCALE_NAMES_TO_OIDS.put("so-SO", "1.3.6.1.4.1.42.2.27.9.4.123.1");
+        LOCALE_NAMES_TO_OIDS.put("so-DJ", "1.3.6.1.4.1.42.2.27.9.4.124.1");
+        LOCALE_NAMES_TO_OIDS.put("so-ET", "1.3.6.1.4.1.42.2.27.9.4.125.1");
+        LOCALE_NAMES_TO_OIDS.put("so-KE", "1.3.6.1.4.1.42.2.27.9.4.126.1");
+        LOCALE_NAMES_TO_OIDS.put("sq", "1.3.6.1.4.1.42.2.27.9.4.127.1");
+        LOCALE_NAMES_TO_OIDS.put("sr", "1.3.6.1.4.1.42.2.27.9.4.128.1");
+        LOCALE_NAMES_TO_OIDS.put("sv", "1.3.6.1.4.1.42.2.27.9.4.129.1");
+        LOCALE_NAMES_TO_OIDS.put("sv-SE", "1.3.6.1.4.1.42.2.27.9.4.129.1");
+        LOCALE_NAMES_TO_OIDS.put("sv-FI", "1.3.6.1.4.1.42.2.27.9.4.130.1");
+        LOCALE_NAMES_TO_OIDS.put("sw", "1.3.6.1.4.1.42.2.27.9.4.131.1");
+        LOCALE_NAMES_TO_OIDS.put("sw-KE", "1.3.6.1.4.1.42.2.27.9.4.132.1");
+        LOCALE_NAMES_TO_OIDS.put("sw-TZ", "1.3.6.1.4.1.42.2.27.9.4.133.1");
+        LOCALE_NAMES_TO_OIDS.put("ta", "1 3  1.3.6.1.4.1.42.2.27.9.4.134.1");
+        LOCALE_NAMES_TO_OIDS.put("te", "1.3.6.1.4.1.42.2.27.9.4.135.1");
+        LOCALE_NAMES_TO_OIDS.put("th", "1.3.6.1.4.1.42.2.27.9.4.136.1");
+        LOCALE_NAMES_TO_OIDS.put("ti", "1.3.6.1.4.1.42.2.27.9.4.137.1");
+        LOCALE_NAMES_TO_OIDS.put("ti-ER", "1.3.6.1.4.1.42.2.27.9.4.138.1");
+        LOCALE_NAMES_TO_OIDS.put("ti-ET", "1.3.6.1.4.1.42.2.27.9.4.139.1");
+        LOCALE_NAMES_TO_OIDS.put("tr", "1.3.6.1.4.1.42.2.27.9.4.140.1");
+        LOCALE_NAMES_TO_OIDS.put("uk", "1.3.6.1.4.1.42.2.27.9.4.141.1");
+        LOCALE_NAMES_TO_OIDS.put("vi", "1.3.6.1.4.1.42.2.27.9.4.142.1");
+        LOCALE_NAMES_TO_OIDS.put("zh", "1.3.6.1.4.1.42.2.27.9.4.143.1");
+        LOCALE_NAMES_TO_OIDS.put("zh-CN", "1.3.6.1.4.1.42.2.27.9.4.144.1");
+        LOCALE_NAMES_TO_OIDS.put("zh-HK", "1.3.6.1.4.1.42.2.27.9.4.145.1");
+        LOCALE_NAMES_TO_OIDS.put("zh-MO", "1.3.6.1.4.1.42.2.27.9.4.146.1");
+        LOCALE_NAMES_TO_OIDS.put("zh-SG", "1.3.6.1.4.1.42.2.27.9.4.147.1");
+        LOCALE_NAMES_TO_OIDS.put("zh-TW", "1.3.6.1.4.1.42.2.27.9.4.148.1");
+
+        initializeJvmSupportedLocaleNamesToOids();
+    }
+
+    private static void initializeJvmSupportedLocaleNamesToOids() {
+        for (Map.Entry<String, String> entry : LOCALE_NAMES_TO_OIDS.entrySet()) {
+            final String localeName = entry.getKey();
+            final String oid = entry.getValue();
+            final String oldLocaleName = new Locale(localeName).toString();
+
+            final int idx = oldLocaleName.indexOf('-');
+            if (idx == -1) {
+                // no dash, oldLocaleName is lowercase, which is correct for the language tag
+                JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS.put(oldLocaleName, oid);
+            } else if (oldLocaleName.equalsIgnoreCase(localeName)) {
+                // fast path to avoid the string computation in the else clause
+                JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS.put(localeName, oid);
+            } else {
+                // Old locale is different from locale and there are country, and/or variants.
+                // Ensure the country and variants are uppercase as in LOCALE_NAMES_TO_OIDS
+                // to avoid problems during matching rules initialization.
+                // e.g. locale name "zh-SG" is converted to "zh-sg" in old locale name
+                final StringBuilder sb = new StringBuilder();
+                sb.append(oldLocaleName, 0, idx + 1)
+                        .append(oldLocaleName.substring(idx + 1, oldLocaleName.length()).toUpperCase(Locale.ENGLISH));
+                JVM_SUPPORTED_LOCALE_NAMES_TO_OIDS.put(sb.toString(), oid);
+            }
+        }
+    }
+
+    /**
+     * Prevents construction.
+     */
+    private CoreSchemaSupportedLocales() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxImpl.java
new file mode 100644
index 0000000..9900d45
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxImpl.java
@@ -0,0 +1,84 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012 Manuel Gaupp
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_COUNTRY_STRING_NO_VALID_ISO_CODE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the country string attribute syntax, which should be a
+ * two-character ISO 3166 country code. However, for maintainability, it will
+ * accept any value consisting entirely of two upper-case characters. In most
+ * ways, it will behave like the directory string attribute syntax.
+ */
+final class CountryStringSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_COUNTRY_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String stringValue = value.toString();
+        if (stringValue.length() != 2) {
+            invalidReason.append(ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH.get(stringValue));
+            return false;
+        }
+
+        // Check for a string containing [A-Z][A-Z]
+        if (stringValue.charAt(0) < 'A' || stringValue.charAt(0) > 'Z'
+            || stringValue.charAt(1) < 'A' || stringValue.charAt(1) > 'Z') {
+            invalidReason.append(ERR_ATTR_SYNTAX_COUNTRY_STRING_NO_VALID_ISO_CODE.get(stringValue));
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRule.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRule.java
new file mode 100644
index 0000000..131a386
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRule.java
@@ -0,0 +1,868 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static java.util.Arrays.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines a DIT content rule, which defines the set of allowed,
+ * required, and prohibited attributes for entries with a given structural
+ * objectclass, and also indicates which auxiliary classes may be included in
+ * the entry.
+ */
+public final class DITContentRule extends SchemaElement {
+
+    /** A fluent API for incrementally constructing DIT content rule. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private String structuralClassOID;
+        private final List<String> names = new LinkedList<>();
+        private boolean isObsolete;
+        private final Set<String> auxiliaryClassOIDs = new LinkedHashSet<>();
+        private final Set<String> optionalAttributeOIDs = new LinkedHashSet<>();
+        private final Set<String> prohibitedAttributeOIDs = new LinkedHashSet<>();
+        private final Set<String> requiredAttributeOIDs = new LinkedHashSet<>();
+
+        Builder(final DITContentRule contentRule, final SchemaBuilder schemaBuilder) {
+            super(schemaBuilder, contentRule);
+            structuralClassOID = contentRule.structuralClassOID;
+            names.addAll(contentRule.getNames());
+            isObsolete = contentRule.isObsolete;
+            auxiliaryClassOIDs.addAll(contentRule.auxiliaryClassOIDs);
+            optionalAttributeOIDs.addAll(contentRule.optionalAttributeOIDs);
+            prohibitedAttributeOIDs.addAll(contentRule.prohibitedAttributeOIDs);
+            requiredAttributeOIDs.addAll(contentRule.requiredAttributeOIDs);
+        }
+
+        Builder(final String structuralClassOID, final SchemaBuilder builder) {
+            super(builder);
+            this.structuralClassOID = structuralClassOID;
+        }
+
+        /**
+         * Adds this DIT content rule to the schema, throwing a
+         * {@code  ConflictingSchemaElementException} if there is an existing DIT
+         * content rule with the same structural object class OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing DIT content rule with the same
+         *             structural object class OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addDITContentRule(new DITContentRule(this), false);
+        }
+
+        /**
+         * Adds this DIT content rule to the schema overwriting any existing
+         * content rule with the same structural class OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addDITContentRule(new DITContentRule(this), true);
+        }
+
+        /**
+         * Adds this DIT content rule to the schema, overwriting any existing DIT content rule
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any DIT content rule with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        /**
+         * Adds the provided auxiliary classes to the list of auxiliary object
+         * classes that entries subject to this DIT content rule may belong to.
+         *
+         * @param objectClassNamesOrOIDs
+         *            The list of auxiliary class names or OIDs.
+         * @return This builder.
+         */
+        public Builder auxiliaryObjectClasses(final Collection<String> objectClassNamesOrOIDs) {
+            this.auxiliaryClassOIDs.addAll(objectClassNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided auxiliary classes to the list of auxiliary object
+         * classes that entries subject to this DIT content rule may belong to.
+         *
+         * @param objectClassNamesOrOIDs
+         *            The list of auxiliary class names or OIDs.
+         * @return This builder.
+         */
+        public Builder auxiliaryObjectClasses(String... objectClassNamesOrOIDs) {
+            this.auxiliaryClassOIDs.addAll(asList(objectClassNamesOrOIDs));
+            return this;
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(asList(names));
+        }
+
+        /**
+         * Specifies whether this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete (default
+         *            is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes to the list of attribute types
+         * that entries subject to this DIT content rule may contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of optional attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final Collection<String> attributeNamesOrOIDs) {
+            this.optionalAttributeOIDs.addAll(attributeNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes to the list of attribute types
+         * that entries subject to this DIT content rule may contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of optional attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final String... attributeNamesOrOIDs) {
+            this.optionalAttributeOIDs.addAll(asList(attributeNamesOrOIDs));
+            return this;
+        }
+
+        /**
+         * Adds the provided prohibited attributes to the list of attribute types
+         * that entries subject to this DIT content rule must not contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of prohibited attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder prohibitedAttributes(final Collection<String> attributeNamesOrOIDs) {
+            this.prohibitedAttributeOIDs.addAll(attributeNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided prohibited attributes to the list of attribute types
+         * that entries subject to this DIT content rule must not contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of prohibited attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder prohibitedAttributes(final String... attributeNamesOrOIDs) {
+            this.prohibitedAttributeOIDs.addAll(asList(attributeNamesOrOIDs));
+            return this;
+        }
+
+        /**
+         * Clears the list of auxiliary object classes that entries subject to
+         * this DIT content rule may belong to.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllAuxiliaryObjectClasses() {
+            this.auxiliaryClassOIDs.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user defined names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        /**
+         * Clears the list of attribute types that entries subject to this DIT
+         * content rule may contain.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllOptionalAttributes() {
+            this.optionalAttributeOIDs.clear();
+            return this;
+        }
+
+        /**
+         * Clears the list of attribute types that entries subject to this DIT
+         * content rule must not contain.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllProhibitedAttributes() {
+            this.prohibitedAttributeOIDs.clear();
+            return this;
+        }
+
+        /**
+         * Clears the list of attribute types that entries subject to this DIT
+         * content rule must contain.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllRequiredAttributes() {
+            this.requiredAttributeOIDs.clear();
+            return this;
+        }
+
+        /**
+         * Removes the provided object class in the list of auxiliary object classes that entries subject to
+         * this DIT content rule may belong to.
+         *
+         * @param objectClassNameOrOID
+         *            The auxiliary object class name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeAuxiliaryObjectClass(String objectClassNameOrOID) {
+            this.auxiliaryClassOIDs.remove(objectClassNameOrOID);
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(String extensionName, String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user defined name.
+         *
+         * @param name
+         *            The user defined name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(String name) {
+            this.names.remove(name);
+            return this;
+        }
+
+        /**
+         * Removes the provided optional attribute in the list of attribute
+         * types that entries subject to this DIT content rule may contain.
+         *
+         * @param attributeNameOrOID
+         *            The optional attribute name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeOptionalAttribute(String attributeNameOrOID) {
+            this.optionalAttributeOIDs.remove(attributeNameOrOID);
+            return this;
+        }
+
+        /**
+         * Removes the provided prohibited attribute in the list of attribute
+         * types that entries subject to this DIT content rule must not contain.
+         *
+         * @param attributeNameOrOID
+         *            The prohibited attribute name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeProhibitedAttribute(String attributeNameOrOID) {
+            this.prohibitedAttributeOIDs.remove(attributeNameOrOID);
+            return this;
+        }
+
+        /**
+         * Removes the provided required attribute in the list of attribute
+         * types that entries subject to this DIT content rule must contain.
+         *
+         * @param attributeNameOrOID
+         *            The provided required attribute name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeRequiredAttribute(String attributeNameOrOID) {
+            this.requiredAttributeOIDs.remove(attributeNameOrOID);
+            return this;
+        }
+
+        /**
+         * Adds the provided attribute to the list of attribute types that
+         * entries subject to this DIT content rule must contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of required attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final Collection<String> attributeNamesOrOIDs) {
+            this.requiredAttributeOIDs.addAll(attributeNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided attribute to the list of attribute types that
+         * entries subject to this DIT content rule must contain.
+         *
+         * @param attributeNamesOrOIDs
+         *            The list of required attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final String... attributeNamesOrOIDs) {
+            this.requiredAttributeOIDs.addAll(asList(attributeNamesOrOIDs));
+            return this;
+        }
+
+        /**
+         * Sets the structural class OID which uniquely identifies this DIT
+         * content rule.
+         *
+         * @param strucuralClassOID
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder structuralClassOID(String strucuralClassOID) {
+            this.structuralClassOID = strucuralClassOID;
+            return this;
+        }
+
+    }
+
+    /** The structural objectclass for this DIT content rule. */
+    private final String structuralClassOID;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /**
+     * The set of auxiliary objectclasses that entries with this content
+     * rule may contain, in a mapping between the objectclass and the
+     * user-defined name for that class.
+     */
+    private final Set<String> auxiliaryClassOIDs;
+
+    /** The set of optional attribute types for this DIT content rule. */
+    private final Set<String> optionalAttributeOIDs;
+
+    /** The set of prohibited attribute types for this DIT content rule. */
+    private final Set<String> prohibitedAttributeOIDs;
+
+    /** The set of required attribute types for this DIT content rule. */
+    private final Set<String> requiredAttributeOIDs;
+
+    private ObjectClass structuralClass;
+    private Set<ObjectClass> auxiliaryClasses = Collections.emptySet();
+    private Set<AttributeType> optionalAttributes = Collections.emptySet();
+    private Set<AttributeType> prohibitedAttributes = Collections.emptySet();
+    private Set<AttributeType> requiredAttributes = Collections.emptySet();
+
+    private DITContentRule(final Builder builder) {
+        super(builder);
+        Reject.ifNull(builder.structuralClassOID);
+
+        structuralClassOID = builder.structuralClassOID;
+        names = unmodifiableCopyOfList(builder.names);
+        isObsolete = builder.isObsolete;
+        auxiliaryClassOIDs = unmodifiableCopyOfSet(builder.auxiliaryClassOIDs);
+        optionalAttributeOIDs = unmodifiableCopyOfSet(builder.optionalAttributeOIDs);
+        prohibitedAttributeOIDs = unmodifiableCopyOfSet(builder.prohibitedAttributeOIDs);
+        requiredAttributeOIDs = unmodifiableCopyOfSet(builder.requiredAttributeOIDs);
+    }
+
+    /**
+     * Returns {@code true} if the provided object is a DIT content rule having
+     * the same structural object class OID as this DIT content rule.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a DIT content rule having
+     *         the same numeric OID as this DIT content rule.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof DITContentRule) {
+            final DITContentRule other = (DITContentRule) o;
+            return structuralClassOID.equals(other.structuralClassOID);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an unmodifiable set containing the auxiliary objectclasses that
+     * may be used for entries associated with this DIT content rule.
+     *
+     * @return An unmodifiable set containing the auxiliary objectclasses that
+     *         may be used for entries associated with this DIT content rule.
+     */
+    public Set<ObjectClass> getAuxiliaryClasses() {
+        return auxiliaryClasses;
+    }
+
+    /**
+     * Returns the name or structural class OID for this schema definition. If
+     * it has one or more names, then the primary name will be returned. If it
+     * does not have any names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return structuralClassOID;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the optional attributes for this
+     * DIT content rule.
+     *
+     * @return An unmodifiable set containing the optional attributes for this
+     *         DIT content rule.
+     */
+    public Set<AttributeType> getOptionalAttributes() {
+        return optionalAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the prohibited attributes for this
+     * DIT content rule.
+     *
+     * @return An unmodifiable set containing the prohibited attributes for this
+     *         DIT content rule.
+     */
+    public Set<AttributeType> getProhibitedAttributes() {
+        return prohibitedAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the required attributes for this
+     * DIT content rule.
+     *
+     * @return An unmodifiable set containing the required attributes for this
+     *         DIT content rule.
+     */
+    public Set<AttributeType> getRequiredAttributes() {
+        return requiredAttributes;
+    }
+
+    /**
+     * Returns the structural objectclass for this DIT content rule.
+     *
+     * @return The structural objectclass for this DIT content rule.
+     */
+    public ObjectClass getStructuralClass() {
+        return structuralClass;
+    }
+
+    /**
+     * Returns the structural class OID for this schema definition.
+     *
+     * @return The structural class OID for this schema definition.
+     */
+    public String getStructuralClassOID() {
+        return structuralClassOID;
+    }
+
+    /**
+     * Returns the hash code for this DIT content rule. It will be calculated as
+     * the hash code of the structural object class OID.
+     *
+     * @return The hash code for this DIT content rule.
+     */
+    @Override
+    public int hashCode() {
+        return structuralClassOID.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return <code>true</code> if the specified name is assigned to this
+     *         schema definition, or <code>false</code> if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name or
+     * structural class OID.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <code>true</code> if the provided value matches the OID or one of
+     *         the names assigned to this schema definition, or
+     *         <code>false</code> if not.
+     */
+    public boolean hasNameOrOID(final String value) {
+        return hasName(value) || structuralClassOID.equals(value);
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return <code>true</code> if this schema definition is declared
+     *         "obsolete", or <code>false</code> if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    /**
+     * Indicates whether the provided attribute type is included in the optional
+     * attribute list for this DIT content rule.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is optional for
+     *         this DIT content rule, or <code>false</code> if not.
+     */
+    public boolean isOptional(final AttributeType attributeType) {
+        return optionalAttributes.contains(attributeType);
+    }
+
+    /**
+     * Indicates whether the provided attribute type is included in the required
+     * attribute list for this DIT content rule.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is required by
+     *         this DIT content rule, or <code>false</code> if not.
+     */
+    public boolean isRequired(final AttributeType attributeType) {
+        return requiredAttributes.contains(attributeType);
+    }
+
+    /**
+     * Indicates whether the provided attribute type is in the list of required
+     * or optional attributes for this DIT content rule.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is required or
+     *         allowed for this DIT content rule, or <code>false</code> if it is
+     *         not.
+     */
+    public boolean isRequiredOrOptional(final AttributeType attributeType) {
+        return isRequired(attributeType) || isOptional(attributeType);
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(structuralClassOID);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        if (!auxiliaryClassOIDs.isEmpty()) {
+            final Iterator<String> iterator = auxiliaryClassOIDs.iterator();
+
+            final String firstClass = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" AUX (");
+                buffer.append(firstClass);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" AUX ");
+                buffer.append(firstClass);
+            }
+        }
+
+        if (!requiredAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = requiredAttributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MUST ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" MUST ");
+                buffer.append(firstName);
+            }
+        }
+
+        if (!optionalAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = optionalAttributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MAY ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" MAY ");
+                buffer.append(firstName);
+            }
+        }
+
+        if (!prohibitedAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = prohibitedAttributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NOT ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" NOT ");
+                buffer.append(firstName);
+            }
+        }
+    }
+
+    void validate(final Schema schema, final List<LocalizableMessage> warnings)
+            throws SchemaException {
+        // Get the objectclass with the specified OID. If it does not exist
+        // or is not structural, then fail.
+        if (structuralClassOID != null) {
+            try {
+                structuralClass = schema.getObjectClass(structuralClassOID);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_DCR_UNKNOWN_STRUCTURAL_CLASS1.get(getNameOrOID(),
+                                structuralClassOID);
+                throw new SchemaException(message, e);
+            }
+            if (structuralClass.getObjectClassType() != ObjectClassType.STRUCTURAL) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_DCR_STRUCTURAL_CLASS_NOT_STRUCTURAL1.get(getNameOrOID(),
+                                structuralClass.getNameOrOID(), structuralClass
+                                        .getObjectClassType().toString());
+                warnings.add(message);
+            }
+        }
+
+        if (!auxiliaryClassOIDs.isEmpty()) {
+            auxiliaryClasses = new HashSet<>(auxiliaryClassOIDs.size());
+            ObjectClass objectClass;
+            for (final String oid : auxiliaryClassOIDs) {
+                try {
+                    objectClass = schema.getObjectClass(oid);
+                } catch (final UnknownSchemaElementException e) {
+                    // This isn't good because it is an unknown auxiliary class.
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_DCR_UNKNOWN_AUXILIARY_CLASS1.get(getNameOrOID(), oid);
+                    throw new SchemaException(message, e);
+                }
+                if (objectClass.getObjectClassType() != ObjectClassType.AUXILIARY) {
+                    // This isn't good because it isn't an auxiliary class.
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_DCR_AUXILIARY_CLASS_NOT_AUXILIARY1.get(getNameOrOID(),
+                                    structuralClass.getOID(), structuralClass.getObjectClassType()
+                                            .toString());
+                    throw new SchemaException(message);
+                }
+                auxiliaryClasses.add(objectClass);
+            }
+        }
+
+        if (!requiredAttributeOIDs.isEmpty()) {
+            requiredAttributes =
+                getAttributeTypes(schema, requiredAttributeOIDs, ERR_ATTR_SYNTAX_DCR_UNKNOWN_REQUIRED_ATTR1);
+        }
+
+        if (!optionalAttributeOIDs.isEmpty()) {
+            optionalAttributes =
+                getAttributeTypes(schema, optionalAttributeOIDs, ERR_ATTR_SYNTAX_DCR_UNKNOWN_OPTIONAL_ATTR1);
+        }
+
+        if (!prohibitedAttributeOIDs.isEmpty()) {
+            prohibitedAttributes =
+                getAttributeTypes(schema, prohibitedAttributeOIDs, ERR_ATTR_SYNTAX_DCR_UNKNOWN_PROHIBITED_ATTR1);
+        }
+
+        // Make sure that none of the prohibited attributes is required by
+        // the structural or any of the auxiliary classes.
+        for (final AttributeType t : prohibitedAttributes) {
+            if (structuralClass.isRequired(t)) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL.get(getNameOrOID(), t
+                                .getNameOrOID(), structuralClass.getNameOrOID());
+                throw new SchemaException(message);
+            }
+
+            for (final ObjectClass oc : auxiliaryClasses) {
+                if (oc.isRequired(t)) {
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY.get(
+                                    getNameOrOID(), t.getNameOrOID(), oc.getNameOrOID());
+                    throw new SchemaException(message);
+                }
+            }
+        }
+
+        auxiliaryClasses = Collections.unmodifiableSet(auxiliaryClasses);
+        optionalAttributes = Collections.unmodifiableSet(optionalAttributes);
+        prohibitedAttributes = Collections.unmodifiableSet(prohibitedAttributes);
+        requiredAttributes = Collections.unmodifiableSet(requiredAttributes);
+    }
+
+    private Set<AttributeType> getAttributeTypes(final Schema schema, Set<String> oids, Arg2<Object, Object> errorMsg)
+            throws SchemaException {
+        Set<AttributeType> attrTypes = new HashSet<>(oids.size());
+        for (final String oid : oids) {
+            try {
+                attrTypes.add(schema.getAttributeType(oid));
+            } catch (final UnknownSchemaElementException e) {
+                // This isn't good because it means that the DIT content rule
+                // requires an attribute type that we don't know anything about.
+                throw new SchemaException(errorMsg.get(getNameOrOID(), oid), e);
+            }
+        }
+        return attrTypes;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxImpl.java
new file mode 100644
index 0000000..f13923e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxImpl.java
@@ -0,0 +1,141 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the DIT content rule description syntax, which is used
+ * to hold DIT content rule definitions in the server schema. The format of this
+ * syntax is defined in RFC 2252.
+ */
+final class DITContentRuleSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_NAME;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_DIT_CONTENT_RULE_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeDITContentRule method to determine if the
+        // value is acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_DCR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+            // The next set of characters must be the OID.
+            readOID(reader, allowMalformedNamesAndOptions);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("aux".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("not".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_DCR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_DCR_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRule.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRule.java
new file mode 100644
index 0000000..6c0ccaf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRule.java
@@ -0,0 +1,505 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2015-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Arrays.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines a DIT structure rule, which is used to indicate the types
+ * of children that entries may have.
+ */
+public final class DITStructureRule extends SchemaElement {
+
+    /** A fluent API for incrementally constructing DIT structure rules. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private int ruleID;
+        private final List<String> names = new LinkedList<>();
+        private boolean isObsolete;
+        private String nameFormOID;
+        private final Set<Integer> superiorRuleIDs = new LinkedHashSet<>();
+
+        Builder(final DITStructureRule structureRule, final SchemaBuilder builder) {
+            super(builder);
+            this.ruleID = structureRule.ruleID;
+            this.names.addAll(structureRule.names);
+            this.isObsolete = structureRule.isObsolete;
+            this.nameFormOID = structureRule.nameFormOID;
+            this.superiorRuleIDs.addAll(structureRule.superiorRuleIDs);
+        }
+
+        Builder(final Integer ruleID, final SchemaBuilder schemaBuilder) {
+            super(schemaBuilder);
+            this.ruleID = ruleID;
+        }
+
+        /**
+         * Adds this DIT structure rule to the schema, throwing a
+         * {@code  ConflictingSchemaElementException} if there is an existing DIT
+         * structure rule with the same numeric ID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing structure rule with the same
+         *             numeric ID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addDITStructureRule(new DITStructureRule(this), false);
+        }
+
+        /**
+         * Adds this DIT structure rule to the schema overwriting any existing
+         * DIT structure rule with the same numeric ID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addDITStructureRule(new DITStructureRule(this), true);
+        }
+
+        /**
+         * Adds this DIT structure rule to the schema, overwriting any existing DIT structure rule
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any DIT structure rule with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Sets the name form associated with the DIT structure rule.
+         *
+         * @param nameFormOID
+         *            The name form numeric OID.
+         * @return This builder.
+         */
+        public Builder nameForm(final String nameFormOID) {
+            this.nameFormOID = nameFormOID;
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(asList(names));
+        }
+
+        /**
+         * Specifies whether this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete
+         *            (default is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user defined names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        /**
+         * Removes all superior rules.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllSuperiorRules() {
+            this.superiorRuleIDs.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user defined name.
+         *
+         * @param name
+         *            The user defined name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(final String name) {
+            this.names.remove(name);
+            return this;
+        }
+
+        /**
+         * Removes the provided superior rule.
+         *
+         * @param superiorRuleID
+         *            The superior rule ID to be removed.
+         * @return This builder.
+         */
+        public Builder removeSuperiorRule(final int superiorRuleID) {
+            this.superiorRuleIDs.remove(superiorRuleID);
+            return this;
+        }
+
+        /**
+         * Sets the the numeric ID which uniquely identifies this structure rule.
+         *
+         * @param ruleID
+         *            The numeric ID.
+         * @return This builder.
+         */
+        public Builder ruleID(final int ruleID) {
+            this.ruleID = ruleID;
+            return this;
+        }
+
+        /**
+         * Adds the provided superior rule identifiers.
+         *
+         * @param superiorRuleIDs
+         *            Structure rule identifiers.
+         * @return This builder.
+         */
+        public Builder superiorRules(final int... superiorRuleIDs) {
+            for (int ruleID : superiorRuleIDs) {
+                this.superiorRuleIDs.add(ruleID);
+            }
+            return this;
+        }
+
+        Builder superiorRules(final Collection<Integer> superiorRuleIDs) {
+            this.superiorRuleIDs.addAll(superiorRuleIDs);
+            return this;
+        }
+
+    }
+
+    /** The rule ID for this DIT structure rule. */
+    private final Integer ruleID;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /** The name form for this DIT structure rule. */
+    private final String nameFormOID;
+
+    /** The set of superior DIT structure rules. */
+    private final Set<Integer> superiorRuleIDs;
+
+    private NameForm nameForm;
+    private Set<DITStructureRule> superiorRules = Collections.emptySet();
+
+    /** Indicates whether or not validation has been performed. */
+    private boolean needsValidating = true;
+
+    /** The indicates whether or not validation failed. */
+    private boolean isValid;
+
+    DITStructureRule(final Builder builder) {
+        super(builder);
+        Reject.ifNull(builder.nameFormOID);
+
+        this.ruleID = builder.ruleID;
+        this.names = unmodifiableCopyOfList(builder.names);
+        this.isObsolete = builder.isObsolete;
+        this.nameFormOID = builder.nameFormOID;
+        this.superiorRuleIDs = unmodifiableCopyOfSet(builder.superiorRuleIDs);
+    }
+
+    /**
+     * Returns {@code true} if the provided object is a DIT structure rule
+     * having the same rule ID as this DIT structure rule.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a DIT structure rule
+     *         having the same rule ID as this DIT structure rule.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof DITStructureRule) {
+            final DITStructureRule other = (DITStructureRule) o;
+            return ruleID.equals(other.ruleID);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieves the name form for this DIT structure rule.
+     *
+     * @return The name form for this DIT structure rule.
+     */
+    public NameForm getNameForm() {
+        return nameForm;
+    }
+
+    /**
+     * Retrieves the name or rule ID for this schema definition. If it has one
+     * or more names, then the primary name will be returned. If it does not
+     * have any names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrRuleID() {
+        if (names.isEmpty()) {
+            return ruleID.toString();
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Retrieves the rule ID for this DIT structure rule.
+     *
+     * @return The rule ID for this DIT structure rule.
+     */
+    public Integer getRuleID() {
+        return ruleID;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the superior rules for this DIT
+     * structure rule.
+     *
+     * @return An unmodifiable set containing the superior rules for this DIT
+     *         structure rule.
+     */
+    public Set<DITStructureRule> getSuperiorRules() {
+        return superiorRules;
+    }
+
+    /**
+     * Returns the hash code for this DIT structure rule. It will be calculated
+     * as the hash code of the rule ID.
+     *
+     * @return The hash code for this DIT structure rule.
+     */
+    @Override
+    public int hashCode() {
+        return ruleID.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return <code>true</code> if the specified name is assigned to this
+     *         schema definition, or <code>false</code> if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return <code>true</code> if this schema definition is declared
+     *         "obsolete", or <code>false</code> if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(ruleID);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        buffer.append(" FORM ");
+        buffer.append(nameFormOID);
+
+        if (superiorRuleIDs != null && !superiorRuleIDs.isEmpty()) {
+            final Iterator<Integer> iterator = superiorRuleIDs.iterator();
+
+            final Integer firstRule = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" SUP ( ");
+                buffer.append(firstRule);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" SUP ");
+                buffer.append(firstRule);
+            }
+        }
+    }
+
+    boolean validate(final Schema schema, final List<DITStructureRule> invalidSchemaElements,
+            final List<LocalizableMessage> warnings) {
+        // Avoid validating this schema element more than once. This may occur
+        // if
+        // multiple rules specify the same superior.
+        if (!needsValidating) {
+            return isValid;
+        }
+
+        // Prevent re-validation.
+        needsValidating = false;
+
+        try {
+            nameForm = schema.getNameForm(nameFormOID);
+        } catch (final UnknownSchemaElementException e) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM.get(getNameOrRuleID(), nameFormOID);
+            failValidation(invalidSchemaElements, warnings, message);
+            return false;
+        }
+
+        if (!superiorRuleIDs.isEmpty()) {
+            superiorRules = new HashSet<>(superiorRuleIDs.size());
+            for (final Integer id : superiorRuleIDs) {
+                try {
+                    superiorRules.add(schema.getDITStructureRule(id));
+                } catch (final UnknownSchemaElementException e) {
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID.get(getNameOrRuleID(), id);
+                    failValidation(invalidSchemaElements, warnings, message);
+                    return false;
+                }
+            }
+        }
+        superiorRules = Collections.unmodifiableSet(superiorRules);
+
+        return isValid = true;
+    }
+
+    private void failValidation(final List<DITStructureRule> invalidSchemaElements,
+            final List<LocalizableMessage> warnings, final LocalizableMessage message) {
+        invalidSchemaElements.add(this);
+        warnings.add(ERR_DSR_VALIDATION_FAIL.get(toString(), message));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRuleSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRuleSyntaxImpl.java
new file mode 100644
index 0000000..2473bcd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DITStructureRuleSyntaxImpl.java
@@ -0,0 +1,141 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the DIT structure rule description syntax, which is
+ * used to hold DIT structure rule definitions in the server schema. The format
+ * of this syntax is defined in RFC 2252.
+ */
+final class DITStructureRuleSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_INTEGER_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_DIT_STRUCTURE_RULE_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeDITStructureRule method to determine if the
+        // value is acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_DSR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            readRuleID(reader);
+
+            String nameForm = null;
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("form".equalsIgnoreCase(tokenName)) {
+                    nameForm = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    readRuleIDs(reader);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_DSR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            if (nameForm == null) {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM.get(definition));
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_DSR_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DelayedSchema.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DelayedSchema.java
new file mode 100644
index 0000000..9fb213c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DelayedSchema.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+/**
+ * This class is used to maintain a reference to the global schemas and avoids
+ * having to reference the core schema in the {@link Schema} class since this
+ * can cause initialization errors because the CoreSchema depends on Schema.
+ */
+final class DelayedSchema {
+    static final Schema EMPTY_SCHEMA = new SchemaBuilder("Empty Schema").toSchema().asNonStrictSchema();
+    static volatile Schema defaultSchema = Schema.getCoreSchema();
+
+    private DelayedSchema() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DeliveryMethodSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DeliveryMethodSyntaxImpl.java
new file mode 100644
index 0000000..f923184
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DeliveryMethodSyntaxImpl.java
@@ -0,0 +1,118 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.util.HashSet;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the delivery method attribute syntax. This contains one or
+ * more of a fixed set of values. If there are multiple values, then they are
+ * separated by spaces with a dollar sign between them. The allowed values
+ * include:
+ * <UL>
+ * <LI>any</LI>
+ * <LI>mhs</LI>
+ * <LI>physical</LI>
+ * <LI>telex</LI>
+ * <LI>teletex</LI>
+ * <LI>g3fax</LI>
+ * <LI>g4fax</LI>
+ * <LI>ia5</LI>
+ * <LI>videotex</LI>
+ * <LI>telephone</LI>
+ * </UL>
+ */
+final class DeliveryMethodSyntaxImpl extends AbstractSyntaxImpl {
+    /**
+     * The set of values that may be used as delivery methods.
+     */
+    private static final HashSet<String> ALLOWED_VALUES = new HashSet<>();
+    {
+        ALLOWED_VALUES.add("any");
+        ALLOWED_VALUES.add("mhs");
+        ALLOWED_VALUES.add("physical");
+        ALLOWED_VALUES.add("telex");
+        ALLOWED_VALUES.add("teletex");
+        ALLOWED_VALUES.add("g3fax");
+        ALLOWED_VALUES.add("g4fax");
+        ALLOWED_VALUES.add("ia5");
+        ALLOWED_VALUES.add("videotex");
+        ALLOWED_VALUES.add("telephone");
+    }
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_DELIVERY_METHOD_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String stringValue = toLowerCase(value.toString());
+        final StringTokenizer tokenizer = new StringTokenizer(stringValue, " $");
+        if (!tokenizer.hasMoreTokens()) {
+            invalidReason.append(ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS.get(value.toString()));
+            return false;
+        }
+
+        while (tokenizer.hasMoreTokens()) {
+            final String token = tokenizer.nextToken();
+            if (!ALLOWED_VALUES.contains(token)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT.get(value
+                        .toString(), token));
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..5343fce
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleImpl.java
@@ -0,0 +1,90 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StringPrepProfile.CASE_FOLD;
+import static com.forgerock.opendj.util.StringPrepProfile.TRIM;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_EMPTY_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_EXPECTED_OPEN_PARENTHESIS;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_DIRECTORY_STRING_FIRST_COMPONENT_NAME;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements the directoryStringFirstComponentMatch matching rule
+ * defined in X.520 and referenced in RFC 2252. This rule is intended for use
+ * with attributes whose values contain a set of parentheses enclosing a
+ * space-delimited set of names and/or name-value pairs (like attribute type or
+ * objectclass descriptions) in which the "first component" is the first item
+ * after the opening parenthesis.
+ */
+final class DirectoryStringFirstComponentEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    DirectoryStringFirstComponentEqualityMatchingRuleImpl() {
+      super(EMR_DIRECTORY_STRING_FIRST_COMPONENT_NAME);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) {
+        return named(EMR_DIRECTORY_STRING_FIRST_COMPONENT_NAME,
+                SchemaUtils.normalizeStringAttributeValue(assertionValue, TRIM, CASE_FOLD));
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final String definition = value.toString();
+        final SubstringReader reader = new SubstringReader(definition);
+
+        // We'll do this a character at a time. First, skip over any leading
+        // whitespace.
+        reader.skipWhitespaces();
+
+        if (reader.remaining() <= 0) {
+            // This means that the value was empty or contained only whitespace.
+            // That is illegal.
+            throw DecodeException.error(ERR_ATTR_SYNTAX_EMPTY_VALUE.get());
+        }
+
+        // The next character must be an open parenthesis. If it is not,
+        // then that is an error.
+        final char c = reader.read();
+        if (c != '(') {
+            throw DecodeException.error(
+                    ERR_ATTR_SYNTAX_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+        }
+
+        // Skip over any spaces immediately following the opening parenthesis.
+        reader.skipWhitespaces();
+
+        // The next set of characters must be the OID.
+        final String string = SchemaUtils.readQuotedString(reader);
+
+        // Grab the substring between the start pos and the current pos
+        return ByteString.valueOfUtf8(string);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringSyntaxImpl.java
new file mode 100644
index 0000000..e27e7f9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DirectoryStringSyntaxImpl.java
@@ -0,0 +1,73 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_DIRECTORYSTRING_INVALID_ZEROLENGTH_VALUE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the directory string attribute syntax, which is simply a
+ * set of UTF-8 characters. By default, they will be treated in a
+ * case-insensitive manner, and equality, ordering, substring, and approximate
+ * matching will be allowed.
+ */
+final class DirectoryStringSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_DIRECTORY_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        if (value.length() > 0 || schema.getOption(ALLOW_ZERO_LENGTH_DIRECTORY_STRINGS)) {
+            return true;
+        }
+        invalidReason.append(ERR_ATTR_SYNTAX_DIRECTORYSTRING_INVALID_ZEROLENGTH_VALUE.get());
+        return false;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..3bd5973
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the distinguishedNameMatch matching rule defined in X.520
+ * and referenced in RFC 2252.
+ */
+final class DistinguishedNameEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    DistinguishedNameEqualityMatchingRuleImpl() {
+        super(EMR_DN_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        try {
+            DN dn = DN.valueOf(value.toString(), schema.asNonStrictSchema());
+            return dn.toNormalizedByteString();
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toByteString().toASCIIString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameSyntaxImpl.java
new file mode 100644
index 0000000..9d6409f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameSyntaxImpl.java
@@ -0,0 +1,67 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_DN_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_DN_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DN;
+
+/**
+ * This class defines the distinguished name attribute syntax, which is used for
+ * attributes that hold distinguished names (DNs). Equality and substring
+ * matching will be allowed by default.
+ */
+final class DistinguishedNameSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_DN_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_DN_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        try {
+            DN.valueOf(value.toString(), schema);
+        } catch (final LocalizedIllegalArgumentException de) {
+            invalidReason.append(de.getMessageObject());
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DoubleMetaphoneApproximateMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DoubleMetaphoneApproximateMatchingRuleImpl.java
new file mode 100644
index 0000000..0478e5e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DoubleMetaphoneApproximateMatchingRuleImpl.java
@@ -0,0 +1,942 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines an approximate matching rule based on the Double Metaphone
+ * algorithm. The Metaphone and Double Metaphone algorithms were originally
+ * devised by Lawrence Philips (published in the December 1990 issue of
+ * <I>Computer Language</I> and the <A
+ * HREF="http://www.cuj.com/documents/s=8038/cuj0006philips/">June 2000 issue of
+ * <I>C/C++ Users Journal</I></A>, respectively), and this version of the
+ * algorithm is based on a version modified by Kevin Atkinson to include
+ * bugfixes and additional functionality (source is available <A
+ * HREF="http://aspell.net/metaphone/dmetaph.cpp">here</A> and additional
+ * Metaphone and Double Metaphone information is available at <A
+ * HREF="http://aspell.net/metaphone/">http://aspell.net/ metaphone/</A>). This
+ * implementation is largely the same as the one provided by Kevin Atkinson, but
+ * it has been re-written for better readability, for more efficiency, to get
+ * rid of checks for conditions that can't possibly happen, and to get rid of
+ * redundant checks that aren't needed. It has also been updated to always only
+ * generate a single value rather than one or possibly two values.
+ */
+final class DoubleMetaphoneApproximateMatchingRuleImpl extends AbstractApproximateMatchingRuleImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    DoubleMetaphoneApproximateMatchingRuleImpl() {
+      super(AMR_DOUBLE_METAPHONE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        String valueString = value.toString();
+        final int length = valueString.length();
+        if (length == 0) {
+            // The value is empty, so it is already normalized.
+            return ByteString.empty();
+        }
+
+        final int last = length - 1;
+
+        // Pad the value to allow for checks to go past the end of the value.
+        valueString = valueString.toUpperCase() + "     ";
+
+        // The metaphone value that is being constructed.
+        final StringBuilder metaphone = new StringBuilder(4);
+
+        // Skip over GN, KN, PN, WR, and PS at the beginning of a word.
+        int pos = 0;
+        String substring = valueString.substring(0, 2);
+        if (substring.equals("GN") || substring.equals("KN") || substring.equals("PN")
+                || substring.equals("WR") || substring.equals("PS")) {
+            pos++;
+        } else if (valueString.charAt(0) == 'X') {
+            // 'X' at the beginning of a word will sound like Z, but Z will
+            // always be mapped to S.
+            metaphone.append("S");
+            pos++;
+        }
+
+        // Loop until we have at least four metaphone characters or have
+        // reached the end of the string.
+        while (metaphone.length() < 4 && pos < length) {
+            // Check the character at the current position against various targets.
+            char posMinusFour;
+            char posMinusThree;
+            char posMinusTwo;
+            char posMinusOne;
+            char posPlusOne;
+            char posPlusTwo;
+            switch (valueString.charAt(pos)) {
+            case 'A':
+            case 'E':
+            case 'I':
+            case 'O':
+            case 'U':
+            case 'Y':
+                // All initial vowels map to 'A'. All others will be ignored.
+                if (pos == 0) {
+                    metaphone.append("A");
+                }
+
+                pos++;
+                break;
+
+            case 'B':
+                // B and BB will be mapped to P, with the exception of "MB" as
+                // in "crumb", but that will be handled elsewhere.
+                metaphone.append("P");
+
+                if (valueString.charAt(++pos) == 'B') {
+                    pos++;
+                }
+
+                break;
+
+            case 'C':
+                // Check for various Germanic sequences, which will be mapped to 'K'.
+                // This basically includes all occurrences of "ACH" where
+                // the preceding character is not a vowel and the following
+                // character is neither an 'E' nor an 'I' except in "BACHER" and
+                // "MACHER".
+                if (pos > 1
+                        && !isVowel(posMinusTwo = valueString.charAt(pos - 2))
+                        && hasSubstring(valueString, pos - 1, "ACH")
+                        && (posPlusTwo = valueString.charAt(pos + 2)) != 'I'
+                        && (posPlusTwo != 'E'
+                            || (valueString.charAt(pos + 3) == 'R'
+                                && (posMinusTwo == 'B' || posMinusTwo == 'M')))) {
+                    metaphone.append("K");
+                    pos += 2;
+                    break;
+                }
+
+                // Check for a special case of "caesar", which will be mapped to 'S'.
+                if (pos == 0 && hasSubstring(valueString, pos + 1, "AESAR")) {
+                    metaphone.append("S");
+                    pos += 2;
+                    break;
+                }
+
+                // CH can be treated in lots of different ways.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H') {
+                    // Check for "chia" as in "chianti" and map to 'K'.
+                    if (hasSubstring(valueString, pos + 2, "IA")) {
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+
+                    // Check for "chae" as in "michael" and map to 'K'.
+                    if (hasSubstring(valueString, pos + 2, "AE")) {
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+
+                    // Check for a Greek root at the beginning of the value like
+                    // chemistry or chorus and map to 'K'.
+                    if (pos == 0
+                            && !hasSubstring(valueString, 2, "ORE")
+                            && (hasSubstring(valueString, 2, "ARAC")
+                                    || hasSubstring(valueString, 2, "ARIS")
+                                    || hasSubstring(valueString, 2, "OR")
+                                    || hasSubstring(valueString, 2, "YM")
+                                    || hasSubstring(valueString, 2, "IA") || hasSubstring(
+                                        valueString, 2, "EM"))) {
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+
+                    // Check for "CH" values that produce a "KH" sound that will
+                    // be mapped to 'K'.
+                    if (isGermanic(valueString)
+                            || hasSubstring(valueString, pos - 2, "ORCHES")
+                            || hasSubstring(valueString, pos - 2, "ARCHIT")
+                            || hasSubstring(valueString, pos - 2, "ORCHID")
+                            || (posPlusTwo = valueString.charAt(pos + 2)) == 'T'
+                            || posPlusTwo == 'S'
+                            || (pos == 0 || (posMinusOne = valueString.charAt(pos - 1)) == 'A'
+                                    || posMinusOne == 'O' || posMinusOne == 'U' || posMinusOne == 'E')
+                            && (posPlusTwo == 'L' || posPlusTwo == 'R' || posPlusTwo == 'N'
+                                    || posPlusTwo == 'M' || posPlusTwo == 'B' || posPlusTwo == 'H'
+                                    || posPlusTwo == 'F' || posPlusTwo == 'V' || posPlusTwo == 'W')) {
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+
+                    // All other "CH" values.
+                    if (pos > 0) {
+                        if (hasSubstring(valueString, 0, "MC")) {
+                            metaphone.append("K");
+                        } else {
+                            metaphone.append("X");
+                        }
+                    } else {
+                        metaphone.append("X");
+                    }
+
+                    pos += 2;
+                    break;
+                }
+
+                // Check for "CZ" as in "czerny" but not "wicz" and map to 'S'.
+                if (posPlusOne == 'Z' && !hasSubstring(valueString, pos - 2, "WI")) {
+                    metaphone.append("S");
+                    pos += 2;
+                    break;
+                }
+
+                // Check for "CIA" as in "focaccia" and map to 'X'.
+                if (posPlusOne == 'I' && valueString.charAt(pos + 2) == 'A') {
+                    metaphone.append("X");
+                    pos += 3;
+                    break;
+                }
+
+                // Check for a double C but not in values that start with "McC"
+                if (posPlusOne == 'C' && !(pos == 1 && valueString.charAt(0) == 'M')) {
+                    posPlusTwo = valueString.charAt(pos + 2);
+                    if ((posPlusTwo == 'I' || posPlusTwo == 'E' || posPlusTwo == 'H')
+                            && !(posPlusTwo == 'H' && valueString.charAt(pos + 3) == 'U')) {
+                        if ((pos == 1 && valueString.charAt(pos - 1) == 'A')
+                                || hasSubstring(valueString, pos - 1, "UCCEE")
+                                || hasSubstring(valueString, pos - 1, "UCCES")) {
+                            // Values like "accident", "accede", and "succeed".
+                            metaphone.append("K");
+                            pos += 2;
+                            break;
+                        } else {
+                            // Values like "bacci" or "bertucci".
+                            metaphone.append("X");
+                            pos += 3;
+                            break;
+                        }
+                    } else {
+                        // This is Pierce's Rule, whatever that means.
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+                }
+
+                // Check for CK, CG, or CQ and map to 'K'. Check for CI, CE, and
+                // CY and map to "S".
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'K' || posPlusOne == 'G' || posPlusOne == 'Q') {
+                    metaphone.append("K");
+                    pos += 2;
+                    break;
+                }
+
+                // Check for CI, CE, or CY and map to 'S'.
+                if (posPlusOne == 'I' || posPlusOne == 'E' || posPlusOne == 'Y') {
+                    metaphone.append("S");
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases of "C" will be mapped to 'K'. However, the
+                // number of positions that we skip ahead may vary. If there is
+                // a value that consists of two words like "mac caffrey", then
+                // skip ahead three. For the character combinations of "CK" and
+                // "CQ", then skip ahead two. For the character combinations of
+                // "CC" except "CCE" and "CCI", then skip ahead two. For all
+                // other cases, skip ahead one.
+                metaphone.append("K");
+                switch (valueString.charAt(pos + 1)) {
+                case ' ':
+                    switch (valueString.charAt(pos + 2)) {
+                    case 'C':
+                    case 'Q':
+                    case 'G':
+                        pos += 3;
+                        break;
+                    default:
+                        pos++;
+                        break;
+                    }
+                    break;
+
+                case 'K':
+                case 'Q':
+                    pos += 2;
+                    break;
+
+                case 'C':
+                    switch (valueString.charAt(pos + 2)) {
+                    case 'E':
+                    case 'I':
+                        pos++;
+                        break;
+                    default:
+                        pos += 2;
+                        break;
+                    }
+                    break;
+                default:
+                    pos++;
+                }
+                break;
+
+            case 'D':
+                // DG will be mapped to either 'J' (in cases like edge) or 'TK'
+                // (in cases like Edgar).
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'G') {
+                    posPlusTwo = valueString.charAt(pos + 2);
+                    if (posPlusTwo == 'I' || posPlusTwo == 'E' || posPlusTwo == 'Y') {
+                        metaphone.append("J");
+                        pos += 3;
+                    } else {
+                        metaphone.append("TK");
+                        pos += 2;
+                    }
+                    break;
+                }
+
+                // DT and DD will be mapped to 'T'.
+                if (posPlusOne == 'T' || posPlusOne == 'D') {
+                    metaphone.append("T");
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases will be mapped to 'T'.
+                metaphone.append("T");
+                pos++;
+                break;
+
+            case 'F':
+                // F always maps to F. If there is a double F, then skip the second one.
+                metaphone.append("F");
+                pos++;
+                if (valueString.charAt(pos) == 'F') {
+                    pos++;
+                }
+                break;
+
+            case 'G':
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H') {
+                    // A "GH" that is not preceded by a vowel will be mapped to 'K'.
+                    if (pos > 0 && !isVowel(valueString.charAt(pos - 1))) {
+                        metaphone.append("K");
+                        pos += 2;
+                        break;
+                    }
+
+                    if (pos == 0) {
+                        if (valueString.charAt(pos + 2) == 'I') {
+                            // Words like ghislane or ghiradelli
+                            metaphone.append("J");
+                        } else {
+                            metaphone.append("K");
+                        }
+
+                        pos += 2;
+                        break;
+                    }
+
+                    // A refined version of Parker's Rule.
+                    if (pos > 1
+                            && ((posMinusTwo = valueString.charAt(pos - 2)) == 'B'
+                                    || posMinusTwo == 'H' || posMinusTwo == 'D')
+                            || pos > 2
+                            && ((posMinusThree = valueString.charAt(pos - 3)) == 'B'
+                                    || posMinusThree == 'H' || posMinusThree == 'D')
+                            || pos > 3
+                            && ((posMinusFour = valueString.charAt(pos - 4)) == 'B' || posMinusFour == 'H')) {
+                        pos += 2;
+                        break;
+                    } else {
+                        if (pos > 2
+                                && valueString.charAt(pos - 1) == 'U'
+                                && ((posMinusThree = valueString.charAt(pos - 3)) == 'C'
+                                        || posMinusThree == 'G' || posMinusThree == 'L'
+                                        || posMinusThree == 'R' || posMinusThree == 'T')) {
+                            // Words like laugh, McLaughlin, cough, rough are mapped to 'F'.
+                            metaphone.append("F");
+                        } else if (pos > 0 && valueString.charAt(pos - 1) != 'I') {
+                            metaphone.append("K");
+                        }
+
+                        pos += 2;
+                        break;
+                    }
+                }
+
+                if (posPlusOne == 'N') {
+                    if (pos == 1 && isVowel(valueString.charAt(0)) && !isSlavoGermanic(valueString)) {
+                        metaphone.append("KN");
+                        pos += 2;
+                        break;
+                    } else {
+                        if (!hasSubstring(valueString, pos + 2, "EY")
+                                && !isSlavoGermanic(valueString)) {
+                            metaphone.append("N");
+                        } else {
+                            metaphone.append("KN");
+                        }
+
+                        pos += 2;
+                        break;
+                    }
+                }
+
+                // GLI as in tagliaro will be mapped to "KL".
+                if (posPlusOne == 'L' && valueString.charAt(pos + 2) == 'I') {
+                    metaphone.append("KL");
+                    pos += 2;
+                    break;
+                }
+
+                // Forms of GY, GE, and GI at the beginning of a word will map to 'K'.
+                if (pos == 0
+                        && (posPlusOne == 'Y'
+                                || (substring = valueString.substring(pos + 1, pos + 3)).equals("ES")
+                                || substring.equals("EP")
+                                || substring.equals("EB") || substring.equals("EL")
+                                || substring.equals("EY") || substring.equals("IB")
+                                || substring.equals("IL") || substring.equals("IN")
+                                || substring.equals("IE") || substring.equals("EI")
+                                || substring.equals("ER"))) {
+                    metaphone.append("K");
+                    pos += 2;
+                    break;
+                }
+
+                // Some occurrences of GER and GY in a word will be mapped to 'K'.
+                posPlusTwo = valueString.charAt(pos + 2);
+                if (((posPlusOne == 'E' && posPlusTwo == 'R') || posPlusOne == 'Y')
+                        && (posMinusOne = valueString.charAt(pos - 1)) != 'E' && posMinusOne != 'I'
+                        && !hasSubstring(valueString, 0, "DANGER")
+                        && !hasSubstring(valueString, 0, "RANGER")
+                        && !hasSubstring(valueString, 0, "MANGER")
+                        && !hasSubstring(valueString, pos - 1, "RGY")
+                        && !hasSubstring(valueString, pos - 1, "OGY")) {
+                    metaphone.append("K");
+                    pos += 2;
+                    break;
+                }
+
+                // Check for Italian uses like 'biaggi" and map to 'J'.
+                if (posPlusOne == 'E' || posPlusOne == 'I' || posPlusOne == 'Y'
+                        || hasSubstring(valueString, pos - 1, "AGGI")
+                        || hasSubstring(valueString, pos - 1, "OGGI")) {
+                    // Germanic uses will be mapped to 'K'.
+                    if (isGermanic(valueString) || hasSubstring(valueString, pos + 1, "ET")) {
+                        metaphone.append("K");
+                    } else {
+                        metaphone.append("J");
+                    }
+
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases will be mapped to 'K'. If there is a double
+                // G, then skip two. Otherwise, just skip one.
+                metaphone.append("K");
+                pos++;
+
+                if (posPlusOne == 'G') {
+                    pos++;
+                }
+
+                break;
+
+            case 'H':
+                // The letter 'H' will only be processed if it is immediately
+                // followed by a vowel and is either the start of the word or
+                // preceded by a vowel.
+                if (isVowel(valueString.charAt(pos + 1))
+                        && (pos == 0 || isVowel(valueString.charAt(pos - 1)))) {
+                    metaphone.append("H");
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'J':
+                // Take care of obvious Spanish uses that should map to 'H'.
+                if (hasSubstring(valueString, 0, "SAN ")) {
+                    metaphone.append("H");
+                    pos++;
+                    break;
+                }
+
+                if (hasSubstring(valueString, pos, "JOSE")) {
+                    if (pos == 0 && valueString.charAt(pos + 4) == ' ') {
+                        metaphone.append("H");
+                    } else {
+                        metaphone.append("J");
+                    }
+
+                    pos++;
+                    break;
+                }
+
+                // All other cases will be mapped to 'J'.
+                metaphone.append("J");
+
+                if (valueString.charAt(pos + 1) == 'J') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'K':
+                // 'K' will always be mapped to 'K'. KK will be treated like K.
+                metaphone.append("K");
+
+                if (valueString.charAt(pos + 1) == 'K') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'L':
+                // 'L' will always be mapped to 'L'. LL will be treated like L,
+                // even for potential Spanish uses.
+                metaphone.append("L");
+
+                if (valueString.charAt(pos + 1) == 'L') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'M':
+                // 'M' will always be mapped to 'M'. MM will be treated like M.
+                // UMB in cases like "dumb" and "thumb" will be treated like M.
+                metaphone.append("M");
+
+                if (valueString.charAt(pos + 1) == 'M') {
+                    pos++;
+                } else if (hasSubstring(valueString, pos - 1, "UMB")
+                        && (pos + 1 == last || hasSubstring(valueString, pos + 2, "ER"))) {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'N':
+                // 'N' will always be mapped to 'N'. NN will be treated like N.
+                metaphone.append("N");
+
+                if (valueString.charAt(pos + 1) == 'N') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'P':
+                // PH will be mapped to 'F'.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H') {
+                    metaphone.append("F");
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases will be mapped to 'P', with PP and PB being
+                // treated like P.
+                metaphone.append("P");
+
+                if (posPlusOne == 'P' || posPlusOne == 'B') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'Q':
+                // 'Q' will always be mapped to 'K'. QQ will be treated like Q.
+                metaphone.append("K");
+
+                if (valueString.charAt(pos + 1) == 'Q') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'R':
+                // Ignore R at the end of French words.
+                if (pos == last && !isSlavoGermanic(valueString)
+                        && hasSubstring(valueString, pos - 2, "IE")
+                        && !hasSubstring(valueString, pos - 4, "ME")
+                        && !hasSubstring(valueString, pos - 4, "MA")) {
+                    pos++;
+                    break;
+                }
+
+                // All other cases will be mapped to 'R', with RR treated like R.
+                metaphone.append("R");
+
+                if (valueString.charAt(pos + 1) == 'R') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'S':
+                // Special cases like isle and carlysle will be silent.
+                if (hasSubstring(valueString, pos - 1, "ISL")
+                        || hasSubstring(valueString, pos - 1, "YSL")) {
+                    pos++;
+                    break;
+                }
+
+                // Special case of sugar mapped to 'X'.
+                if (hasSubstring(valueString, pos + 1, "UGAR")) {
+                    metaphone.append("X");
+                    pos++;
+                    break;
+                }
+
+                // SH is generally mapped to 'X', but not in Germanic cases.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H') {
+                    if (hasSubstring(valueString, pos + 1, "HEIM")
+                            || hasSubstring(valueString, pos + 1, "HOEK")
+                            || hasSubstring(valueString, pos + 1, "HOLM")
+                            || hasSubstring(valueString, pos + 1, "HOLZ")) {
+                        metaphone.append("S");
+                    } else {
+                        metaphone.append("X");
+                    }
+
+                    pos += 2;
+                    break;
+                }
+
+                // Italian and Armenian cases will map to "S".
+                if (hasSubstring(valueString, pos + 1, "IO")
+                        || hasSubstring(valueString, pos + 1, "IA")) {
+                    metaphone.append("S");
+                    pos += 3;
+                    break;
+                }
+
+                // SZ should be mapped to 'S'.
+                if (posPlusOne == 'Z') {
+                    metaphone.append("S");
+                    pos += 2;
+                    break;
+                }
+
+                // Various combinations at the beginning of words will be mapped to 'S'.
+                if (pos == 0
+                        && (posPlusOne == 'M' || posPlusOne == 'N' || posPlusOne == 'L' || posPlusOne == 'W')) {
+                    metaphone.append("S");
+                    pos++;
+                    break;
+                }
+
+                // SC should be mapped to either SK, X, or S.
+                if (posPlusOne == 'C') {
+                    posPlusTwo = valueString.charAt(pos + 2);
+                    if (posPlusTwo == 'H') {
+                        if (hasSubstring(valueString, pos + 3, "OO")
+                                || hasSubstring(valueString, pos + 3, "UY")
+                                || hasSubstring(valueString, pos + 3, "ED")
+                                || hasSubstring(valueString, pos + 3, "EM")) {
+                            metaphone.append("SK");
+                        } else {
+                            metaphone.append("X");
+                        }
+
+                        pos += 3;
+                        break;
+                    }
+
+                    if (posPlusTwo == 'I' || posPlusTwo == 'E' || posPlusTwo == 'Y') {
+                        metaphone.append("S");
+                        pos += 3;
+                        break;
+                    }
+
+                    metaphone.append("SK");
+                    pos += 3;
+                    break;
+                }
+
+                // Ignore a trailing S in French words. All others will be
+                // mapped to 'S'.
+                if (!(pos == last && (hasSubstring(valueString, pos - 2, "AI") || hasSubstring(
+                        valueString, pos - 2, "OI")))) {
+                    metaphone.append("S");
+                }
+
+                if (posPlusOne == 'S' || posPlusOne == 'Z') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'T':
+                // "TION", "TIA", and "TCH" will be mapped to 'X'.
+                if (hasSubstring(valueString, pos, "TION") || hasSubstring(valueString, pos, "TIA")
+                        || hasSubstring(valueString, pos, "TCH")) {
+                    metaphone.append("X");
+                    pos += 3;
+                    break;
+                }
+
+                // TH or TTH will be mapped to either T (for Germanic cases) or
+                // 0 (zero) for the rest.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H'
+                        || (posPlusOne == 'T' && valueString.charAt(pos + 2) == 'H')) {
+                    if (isGermanic(valueString) || hasSubstring(valueString, pos + 2, "OM")
+                            || hasSubstring(valueString, pos + 2, "AM")) {
+                        metaphone.append("T");
+                    } else {
+                        metaphone.append("0");
+                    }
+
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases will map to T, with TT and TD being treated like T.
+                metaphone.append("T");
+
+                if (posPlusOne == 'T' || posPlusOne == 'D') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'V':
+                // 'V' will always be mapped to 'F', with VV treated like V.
+                metaphone.append("F");
+
+                if (valueString.charAt(pos + 1) == 'V') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'W':
+                // WR should always map to R.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'R') {
+                    metaphone.append("R");
+                    pos += 2;
+                    break;
+                }
+
+                // W[AEIOUYH] at the beginning of the word should be mapped to A.
+                if (pos == 0 && (isVowel(posPlusOne) || posPlusOne == 'H')) {
+                    metaphone.append("A");
+
+                    // FIXME -- This isn't in the algorithm as written. Should it be?
+                    pos += 2;
+                    break;
+                }
+
+                // A Polish value like WICZ or WITZ should be mapped to TS.
+                if (hasSubstring(valueString, pos + 1, "WICZ")
+                        || hasSubstring(valueString, pos + 1, "WITZ")) {
+                    metaphone.append("TS");
+                    pos += 4;
+                    break;
+                }
+
+                // Otherwise, we'll just skip it.
+                pos++;
+                break;
+
+            case 'X':
+                // X maps to KS except at the end of French words.
+                if (!(pos == last && (hasSubstring(valueString, pos - 3, "IAU")
+                        || hasSubstring(valueString, pos - 3, "EAU")
+                        || hasSubstring(valueString, pos - 2, "AU") || hasSubstring(valueString,
+                            pos - 2, "OU")))) {
+                    metaphone.append("KS");
+                }
+
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'C' || posPlusOne == 'X') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case 'Z':
+                // Chinese usages like zhao will map to J.
+                posPlusOne = valueString.charAt(pos + 1);
+                if (posPlusOne == 'H') {
+                    metaphone.append("J");
+                    pos += 2;
+                    break;
+                }
+
+                // All other cases map to "S". ZZ will be treated like Z.
+                metaphone.append("S");
+
+                if (posPlusOne == 'Z') {
+                    pos++;
+                }
+
+                pos++;
+                break;
+
+            case '\u00C7': // C with a cedilla
+                // This will always be mapped to 'S'.
+                metaphone.append("S");
+                pos++;
+                break;
+
+            case '\u00D1': // N with a tilde
+                // This will always be mapped to 'N'.
+                metaphone.append("N");
+                pos++;
+                break;
+
+            default:
+                // We don't have any special treatment for this character, so
+                // skip it.
+                pos++;
+                break;
+            }
+        }
+
+        return ByteString.valueOfUtf8(metaphone);
+    }
+
+    /**
+     * Indicates whether the provided value has the given substring at the
+     * specified position.
+     *
+     * @param value
+     *            The value containing the range for which to make the
+     *            determination.
+     * @param start
+     *            The position in the value at which to start the comparison.
+     * @param substring
+     *            The substring to compare against the specified value range.
+     * @return <CODE>true</CODE> if the specified portion of the value matches
+     *         the given substring, or <CODE>false</CODE> if it does not.
+     */
+    private boolean hasSubstring(final String value, final int start, final String substring) {
+        try {
+            // This can happen since a lot of the rules "look behind" and
+            // rightfully don't check if it's the first character
+            if (start < 0) {
+                return false;
+            }
+
+            final int end = start + substring.length();
+
+            // value isn't big enough to do the comparison
+            if (end > value.length()) {
+                return false;
+            }
+
+            for (int i = 0, pos = start; pos < end; i++, pos++) {
+                if (value.charAt(pos) != substring.charAt(i)) {
+                    return false;
+                }
+            }
+
+            return true;
+        } catch (final Exception e) {
+            logger.debug(LocalizableMessage.raw(
+                "Unable to check that '%s' has substring '%s' at position %d: %s", value, substring, start, e));
+            return false;
+        }
+    }
+
+    /**
+     * Indicates whether the provided string appears Germanic (starts with
+     * "VAN ", "VON ", or "SCH").
+     *
+     * @param s
+     *            The string for which to make the determination.
+     * @return <CODE>true</CODE> if the provided string appears Germanic, or
+     *         <CODE>false</CODE> if not.
+     */
+    private boolean isGermanic(final String s) {
+        return s.startsWith("VAN ") || s.startsWith("VON ") || s.startsWith("SCH");
+    }
+
+    /**
+     * Indicates whether the provided string appears to be Slavo-Germanic.
+     *
+     * @param s
+     *            The string for which to make the determination.
+     * @return <CODE>true</CODE> if the provided string appears to be
+     *         Slavo-Germanic, or <CODE>false</CODE> if not.
+     */
+    private boolean isSlavoGermanic(final String s) {
+        return s.contains("W") || s.contains("K") || s.contains("CZ") || s.contains("WITZ");
+    }
+
+    /**
+     * Indicates whether the provided character is a vowel (including "Y").
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided character is a vowel, or
+     *         <CODE>false</CODE> if not.
+     */
+    private boolean isVowel(final char c) {
+        switch (c) {
+        case 'A':
+        case 'E':
+        case 'I':
+        case 'O':
+        case 'U':
+        case 'Y':
+            return true;
+
+        default:
+            return false;
+        }
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnhancedGuideSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnhancedGuideSyntaxImpl.java
new file mode 100644
index 0000000..3837d16
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnhancedGuideSyntaxImpl.java
@@ -0,0 +1,121 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_ENHANCED_GUIDE_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements the enhanced guide attribute syntax, which may be used
+ * to provide criteria for generating search filters for entries of a given
+ * objectclass.
+ */
+final class EnhancedGuideSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_ENHANCED_GUIDE_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+        final LocalizableMessageBuilder invalidReason) {
+        // Get a lowercase string version of the provided value.
+        final String valueStr = toLowerCase(value.toString());
+
+        // Find the position of the first octothorpe. It should denote the
+        // end of the objectclass.
+        final int sharpPos = valueStr.indexOf('#');
+        if (sharpPos < 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SHARP1.get(valueStr));
+            return false;
+        }
+
+        // Get the objectclass and see if it is a valid name or OID.
+        final String ocName = valueStr.substring(0, sharpPos).trim();
+        final int ocLength = ocName.length();
+        if (ocLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_OC1.get(valueStr));
+            return false;
+        }
+
+        try {
+            readOID(new SubstringReader(ocName.substring(ocLength)),
+                schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+        } catch (final DecodeException de) {
+            invalidReason.append(de.getMessageObject());
+            return false;
+        }
+
+        // Find the last octothorpe and make sure it is followed by a valid
+        // scope.
+        final int lastSharpPos = valueStr.lastIndexOf('#');
+        if (lastSharpPos == sharpPos) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP.get(valueStr));
+            return false;
+        }
+
+        final String scopeStr = valueStr.substring(lastSharpPos + 1).trim();
+        if (!"baseobject".equals(scopeStr) && !"onelevel".equals(scopeStr)
+            && !"wholesubtree".equals(scopeStr) && !"subordinatesubtree".equals(scopeStr)) {
+            if (scopeStr.length() == 0) {
+                invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE.get(valueStr));
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE.get(valueStr,
+                    scopeStr));
+            }
+            return false;
+        }
+
+        // Everything between the two octothorpes must be the criteria. Make
+        // sure it is valid.
+        final String criteria = valueStr.substring(sharpPos + 1, lastSharpPos).trim();
+        final int criteriaLength = criteria.length();
+        if (criteriaLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA.get(valueStr));
+            return false;
+        }
+
+        return GuideSyntaxImpl.criteriaIsValid(schema, criteria, valueStr, invalidReason);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumOrderingMatchingRule.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumOrderingMatchingRule.java
new file mode 100644
index 0000000..05aca0d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumOrderingMatchingRule.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_INVALID_VALUE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * This class is the ordering matching rule implementation for an enum syntax
+ * implementation. The ordering is determined by the order of the entries in the
+ * X-ENUM extension value.
+ */
+final class EnumOrderingMatchingRule extends AbstractOrderingMatchingRuleImpl {
+    private final EnumSyntaxImpl syntax;
+
+    EnumOrderingMatchingRule(final EnumSyntaxImpl syntax) {
+        super(OMR_GENERIC_ENUM_NAME);
+        Reject.ifNull(syntax);
+        this.syntax = syntax;
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final int index = syntax.indexOf(value);
+        if (index < 0) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_INVALID_VALUE.get(value
+                    .toString(), syntax.getName()));
+        }
+        return ByteString.valueOfInt(index);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumSyntaxImpl.java
new file mode 100644
index 0000000..ab7807a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/EnumSyntaxImpl.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StringPrepProfile.CASE_FOLD;
+import static com.forgerock.opendj.util.StringPrepProfile.TRIM;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_INVALID_VALUE;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.AMR_DOUBLE_METAPHONE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OID_GENERIC_ENUM;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class provides an enumeration-based mechanism where a new syntax and its
+ * corresponding matching rules can be created on-the-fly. An enum syntax is an
+ * LDAPSyntaxDescriptionSyntax with X-ENUM extension.
+ */
+final class EnumSyntaxImpl extends AbstractSyntaxImpl {
+    private final String oid;
+    /** Set of read-only enum entries. */
+    private final List<String> entries;
+
+    EnumSyntaxImpl(final String oid, final List<String> entries) {
+        this.oid = oid;
+        final List<String> entryStrings = new ArrayList<>(entries.size());
+
+        for (final String entry : entries) {
+            final String normalized = normalize(ByteString.valueOfUtf8(entry));
+            if (!entryStrings.contains(normalized)) {
+                entryStrings.add(normalized);
+            }
+        }
+        this.entries = Collections.unmodifiableList(entryStrings);
+    }
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return oid;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OID_GENERIC_ENUM + "." + oid;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    public int indexOf(final ByteSequence value) {
+        return entries.indexOf(normalize(value));
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // The value is acceptable if it belongs to the set.
+        final boolean isAllowed = entries.contains(normalize(value));
+
+        if (!isAllowed) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_INVALID_VALUE.get(value.toString(), oid);
+            invalidReason.append(message);
+        }
+
+        return isAllowed;
+    }
+
+    private String normalize(final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD).toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FacsimileNumberSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FacsimileNumberSyntaxImpl.java
new file mode 100644
index 0000000..09689b1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FacsimileNumberSyntaxImpl.java
@@ -0,0 +1,168 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_FAXNUMBER_NAME;
+
+import java.util.HashSet;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the facsimile telephone number attribute syntax, which
+ * contains a printable string (the number) followed by zero or more parameters.
+ * Those parameters should start with a dollar sign may be any of the following
+ * strings:
+ * <UL>
+ * <LI>twoDimensional</LI>
+ * <LI>fineResolution</LI>
+ * <LI>unlimitedLength</LI>
+ * <LI>b4Length</LI>
+ * <LI>a3Width</LI>
+ * <LI>b4Width</LI>
+ * <LI>uncompressed</LI>
+ * </UL>
+ */
+final class FacsimileNumberSyntaxImpl extends AbstractSyntaxImpl {
+    /**
+     * The set of allowed fax parameter values, formatted entirely in lowercase
+     * characters.
+     */
+    public static final HashSet<String> ALLOWED_FAX_PARAMETERS = new HashSet<>(7);
+
+    static {
+        ALLOWED_FAX_PARAMETERS.add("twodimensional");
+        ALLOWED_FAX_PARAMETERS.add("fineresolution");
+        ALLOWED_FAX_PARAMETERS.add("unlimitedlength");
+        ALLOWED_FAX_PARAMETERS.add("b4length");
+        ALLOWED_FAX_PARAMETERS.add("a3width");
+        ALLOWED_FAX_PARAMETERS.add("b4width");
+        ALLOWED_FAX_PARAMETERS.add("uncompressed");
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_FAXNUMBER_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get a lowercase string representation of the value and find its
+        // length.
+        final String valueString = toLowerCase(value.toString());
+        final int valueLength = valueString.length();
+
+        // The value must contain at least one character.
+        if (valueLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY.get());
+            return false;
+        }
+
+        // The first character must be a printable string character.
+        char c = valueString.charAt(0);
+        if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE.get(valueString, String
+                    .valueOf(c), 0));
+            return false;
+        }
+
+        // Continue reading until we find a dollar sign or the end of the
+        // string. Every intermediate character must be a printable string
+        // character.
+        int pos = 1;
+        for (; pos < valueLength; pos++) {
+            c = valueString.charAt(pos);
+            if (c == '$') {
+                pos++;
+                break;
+            } else if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE.get(valueString, c, pos));
+            }
+        }
+
+        if (pos >= valueLength) {
+            // We're at the end of the value, so it must be valid unless the
+            // last character was a dollar sign.
+            if (c == '$') {
+                invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR.get(valueString));
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        // Continue reading until we find the end of the string. Each
+        // substring must be a valid fax parameter.
+        int paramStartPos = pos;
+        while (pos < valueLength) {
+            c = valueString.charAt(pos++);
+            if (c == '$') {
+                final String paramStr = valueString.substring(paramStartPos, pos);
+                if (!ALLOWED_FAX_PARAMETERS.contains(paramStr)) {
+                    invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER.get(
+                            valueString, paramStr, paramStartPos, pos - 1));
+                    return false;
+                }
+
+                paramStartPos = pos;
+            }
+        }
+
+        // We must be at the end of the value. Read the last parameter and
+        // make sure it is valid.
+        final String paramStr = valueString.substring(paramStartPos);
+        if (!ALLOWED_FAX_PARAMETERS.contains(paramStr)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER.get(
+                    valueString, paramStr, paramStartPos, pos - 1));
+            return false;
+        }
+
+        // If we've gotten here, then the value must be valid.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FaxSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FaxSyntaxImpl.java
new file mode 100644
index 0000000..21db782
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/FaxSyntaxImpl.java
@@ -0,0 +1,60 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_FAX_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the fax attribute syntax. This should be restricted to
+ * holding only fax message contents, but we will accept any set of bytes. It
+ * will be treated much like the octet string attribute syntax.
+ */
+final class FaxSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_FAX_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the fax syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..12b98c8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java
@@ -0,0 +1,58 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+
+/**
+ * This class defines the generalizedTimeMatch matching rule defined in X.520
+ * and referenced in RFC 2252.
+ */
+final class GeneralizedTimeEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    GeneralizedTimeEqualityMatchingRuleImpl() {
+        super(EMR_GENERALIZED_TIME_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        return normalizeAttributeValue(value);
+    }
+
+    static ByteString normalizeAttributeValue(final ByteSequence value) throws DecodeException {
+        try {
+            final GeneralizedTime time = GeneralizedTime.valueOf(value.toString());
+            return createNormalizedAttributeValue(time.getTimeInMillis());
+        } catch (LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+
+    static ByteString createNormalizedAttributeValue(final long timeInMillis) {
+        /* Dates older than 1970 will be negative and will sort after dates more recent than 1970 due to twos
+         * complement encoding. Therefore mangle the time in order to ensure that it is positive for all valid values
+         * of a generalized time.
+         */
+        return ByteString.valueOfLong(timeInMillis - GeneralizedTime.MIN_GENERALIZED_TIME_MS);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..39e8c1d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the generalizedTimeOrderingMatch matching rule defined in
+ * X.520 and referenced in RFC 2252.
+ */
+final class GeneralizedTimeOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+
+    GeneralizedTimeOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_GENERALIZED_TIME_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        return GeneralizedTimeEqualityMatchingRuleImpl.normalizeAttributeValue(value);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java
new file mode 100644
index 0000000..90865bd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java
@@ -0,0 +1,73 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_GENERALIZED_TIME_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_GENERALIZED_TIME_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GENERALIZED_TIME_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+
+/**
+ * This class implements the fax attribute syntax. This should be restricted to
+ * holding only fax message contents, but we will accept any set of bytes. It
+ * will be treated much like the octet string attribute syntax.
+ */
+final class GeneralizedTimeSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_GENERALIZED_TIME_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_GENERALIZED_TIME_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_GENERALIZED_TIME_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        try {
+            GeneralizedTime.valueOf(value.toString());
+            return true;
+        } catch (final LocalizedIllegalArgumentException e) {
+            invalidReason.append(e.getMessageObject());
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java
new file mode 100644
index 0000000..d074f3b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java
@@ -0,0 +1,328 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 Manuel Gaupp
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Tool for generating CoreSchema.java.
+ */
+final class GenerateCoreSchema {
+    private static final Set<String> ABBREVIATIONS = new HashSet<>(Arrays.asList("SASL",
+            "LDAP", "DN", "DIT", "RDN", "JPEG", "OID", "UUID", "IA5", "UID", "UTC", "X500", "X121",
+            "C", "CN", "O", "OU", "L", "DC", "ISDN", "SN", "ST"));
+
+    /**
+     * Tool for generating CoreSchema.java.
+     *
+     * @param args
+     *            The command line arguments (none required).
+     */
+    public static void main(final String[] args) {
+        testSplitNameIntoWords();
+
+        final Schema schema = Schema.getCoreSchema();
+
+        final SortedMap<String, Syntax> syntaxes = new TreeMap<>();
+        for (final Syntax syntax : schema.getSyntaxes()) {
+            if (isOpenDSOID(syntax.getOID())) {
+                continue;
+            }
+
+            final String name = syntax.getDescription().replaceAll(" Syntax$", "");
+            final String fieldName = name.replace(" ", "_").replaceAll("[.-]", "")
+                    .toUpperCase(Locale.ENGLISH).concat("_SYNTAX");
+            syntaxes.put(fieldName, syntax);
+        }
+
+        final SortedMap<String, MatchingRule> matchingRules = new TreeMap<>();
+        for (final MatchingRule matchingRule : schema.getMatchingRules()) {
+            if (isOpenDSOID(matchingRule.getOID()) || isCollationMatchingRule(matchingRule.getOID())) {
+                continue;
+            }
+
+            final String name = matchingRule.getNameOrOID().replaceAll("Match$", "");
+            final String fieldName = splitNameIntoWords(name).concat("_MATCHING_RULE");
+            matchingRules.put(fieldName, matchingRule);
+        }
+
+        final SortedMap<String, AttributeType> attributeTypes = new TreeMap<>();
+        for (final AttributeType attributeType : schema.getAttributeTypes()) {
+            if (isOpenDSOID(attributeType.getOID())) {
+                continue;
+            }
+            final String name = attributeType.getNameOrOID();
+            final String fieldName = splitNameIntoWords(name).concat("_ATTRIBUTE_TYPE");
+            attributeTypes.put(fieldName, attributeType);
+        }
+
+        final SortedMap<String, ObjectClass> objectClasses = new TreeMap<>();
+        for (final ObjectClass objectClass : schema.getObjectClasses()) {
+            if (isOpenDSOID(objectClass.getOID())) {
+                continue;
+            }
+            final String name = objectClass.getNameOrOID();
+            final String fieldName = splitNameIntoWords(name.replace("-", "")).concat("_OBJECT_CLASS");
+
+            objectClasses.put(fieldName, objectClass);
+        }
+
+        final PrintStream out = System.out;
+        out.println("/*");
+        out.println(" * The contents of this file are subject to the terms of the Common Development and");
+        out.println(" * Distribution License (the License). You may not use this file except in compliance with the");
+        out.println(" * License.");
+        out.println(" *");
+        out.println(" * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the");
+        out.println(" * specific language governing permission and limitations under the License.");
+        out.println(" *");
+        out.println(" * When distributing Covered Software, include this CDDL Header Notice in each file and include");
+        out.println(" * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL");
+        out.println(" * Header, with the fields enclosed by brackets [] replaced by your own identifying");
+        out.println(" * information: \"Portions Copyright [year] [name of copyright owner]\".");
+        out.println(" *");
+        out.println(" * Copyright 2009 Sun Microsystems, Inc.");
+        out.println(" * Portions copyright 2014-" + Calendar.getInstance().get(Calendar.YEAR) + " ForgeRock AS.");
+        out.println(" */");
+        out.println("package org.forgerock.opendj.ldap.schema;");
+        out.println();
+        out.println();
+        out.println("// DON'T EDIT THIS FILE!");
+        out.println("// It is automatically generated using GenerateCoreSchema class.");
+        out.println();
+        out.println("/**");
+        out.println(" * The OpenDJ SDK core schema contains standard LDAP "
+                + "RFC schema elements. These include:");
+        out.println(" * <ul>");
+        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4512\">RFC 4512 -");
+        out
+                .println(" * Lightweight Directory Access Protocol (LDAP): Directory Information");
+        out.println(" * Models </a>");
+        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4517\">RFC 4517 -");
+        out
+                .println(" * Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching");
+        out.println(" * Rules </a>");
+        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4519\">RFC 4519 -");
+        out.println(" * Lightweight Directory Access Protocol (LDAP): Schema for User");
+        out.println(" * Applications </a>");
+        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4530\">RFC 4530 -");
+        out
+                .println(" * Lightweight Directory Access Protocol (LDAP): entryUUID Operational");
+        out.println(" * Attribute </a>");
+        out
+                .println(" * <li><a href=\"http://tools.ietf.org/html/rfc3045\">RFC 3045 - Storing");
+        out.println(" * Vendor Information in the LDAP Root DSE </a>");
+        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc3112\">RFC 3112 - LDAP");
+        out.println(" * Authentication Password Schema </a>");
+        out.println(" * </ul>");
+        out.println(" * <p>");
+        out.println(" * The core schema is non-strict: attempts to retrieve");
+        out.println(" * non-existent Attribute Types will return a temporary");
+        out.println(" * Attribute Type having the Octet String syntax.");
+        out.println(" */");
+        out.println("public final class CoreSchema {");
+
+        out.println("    // Core Syntaxes");
+        for (final Map.Entry<String, Syntax> syntax : syntaxes.entrySet()) {
+            out.println("    private static final Syntax " + syntax.getKey() + " =");
+            out.println("        CoreSchemaImpl.getInstance().getSyntax(\""
+                    + syntax.getValue().getOID() + "\");");
+        }
+
+        out.println();
+        out.println("    // Core Matching Rules");
+        for (final Map.Entry<String, MatchingRule> matchingRule : matchingRules.entrySet()) {
+            out.println("    private static final MatchingRule " + matchingRule.getKey()
+                    + " =");
+            out.println("        CoreSchemaImpl.getInstance().getMatchingRule(\""
+                    + matchingRule.getValue().getOID() + "\");");
+        }
+
+        out.println();
+        out.println("    // Core Attribute Types");
+        for (final Map.Entry<String, AttributeType> attributeType : attributeTypes.entrySet()) {
+            out.println("    private static final AttributeType " + attributeType.getKey()
+                    + " =");
+            out.println("        CoreSchemaImpl.getInstance().getAttributeType(\""
+                    + attributeType.getValue().getOID() + "\");");
+        }
+
+        out.println();
+        out.println("    // Core Object Classes");
+        for (final Map.Entry<String, ObjectClass> objectClass : objectClasses.entrySet()) {
+            out.println("    private static final ObjectClass " + objectClass.getKey() + " =");
+            out.println("        CoreSchemaImpl.getInstance().getObjectClass(\""
+                    + objectClass.getValue().getOID() + "\");");
+        }
+
+        out.println();
+        out.println("    // Prevent instantiation");
+        out.println("    private CoreSchema() {");
+        out.println("      // Nothing to do.");
+        out.println("    }");
+
+        out.println();
+        out.println("    /**");
+        out.println("     * Returns a reference to the singleton core schema.");
+        out.println("     *");
+        out.println("     * @return The core schema.");
+        out.println("     */");
+        out.println("    public static Schema getInstance() {");
+        out.println("        return CoreSchemaImpl.getInstance();");
+        out.println("    }");
+
+        for (final Map.Entry<String, Syntax> syntax : syntaxes.entrySet()) {
+            out.println();
+
+            final String description =
+                    toCodeJavaDoc(syntax.getValue().getDescription().replaceAll(" Syntax$", "")
+                            + " Syntax");
+            out.println("    /**");
+            out.println("     * Returns a reference to the " + description);
+            out.println("     * which has the OID "
+                    + toCodeJavaDoc(syntax.getValue().getOID()) + ".");
+            out.println("     *");
+            out.println("     * @return A reference to the " + description + ".");
+
+            out.println("     */");
+            out.println("    public static Syntax get" + toJavaName(syntax.getKey()) + "() {");
+            out.println("        return " + syntax.getKey() + ";");
+            out.println("    }");
+        }
+
+        for (final Map.Entry<String, MatchingRule> matchingRule : matchingRules.entrySet()) {
+            out.println();
+
+            final String description = toCodeJavaDoc(matchingRule.getValue().getNameOrOID());
+            out.println("    /**");
+            out.println("     * Returns a reference to the " + description + " Matching Rule");
+            out.println("     * which has the OID "
+                    + toCodeJavaDoc(matchingRule.getValue().getOID()) + ".");
+            out.println("     *");
+            out.println("     * @return A reference to the " + description + " Matching Rule.");
+
+            out.println("     */");
+            out.println("    public static MatchingRule get" + toJavaName(matchingRule.getKey()) + "() {");
+            out.println("        return " + matchingRule.getKey() + ";");
+            out.println("    }");
+        }
+
+        for (final Map.Entry<String, AttributeType> attributeType : attributeTypes.entrySet()) {
+            out.println();
+
+            final String description = toCodeJavaDoc(attributeType.getValue().getNameOrOID());
+            out.println("    /**");
+            out.println("     * Returns a reference to the " + description + " Attribute Type");
+            out.println("     * which has the OID "
+                    + toCodeJavaDoc(attributeType.getValue().getOID()) + ".");
+            out.println("     *");
+            out.println("     * @return A reference to the " + description + " Attribute Type.");
+
+            out.println("     */");
+            out.println("    public static AttributeType get"
+                    + toJavaName(attributeType.getKey()) + "() {");
+            out.println("        return " + attributeType.getKey() + ";");
+            out.println("    }");
+        }
+
+        for (final Map.Entry<String, ObjectClass> objectClass : objectClasses.entrySet()) {
+            out.println();
+
+            final String description = toCodeJavaDoc(objectClass.getValue().getNameOrOID());
+            out.println("    /**");
+            out.println("     * Returns a reference to the " + description + " Object Class");
+            out.println("     * which has the OID "
+                    + toCodeJavaDoc(objectClass.getValue().getOID()) + ".");
+            out.println("     *");
+            out.println("     * @return A reference to the " + description + " Object Class.");
+
+            out.println("     */");
+            out.println("    public static ObjectClass get" + toJavaName(objectClass.getKey())
+                    + "() {");
+            out.println("        return " + objectClass.getKey() + ";");
+            out.println("    }");
+        }
+
+        out.println("}");
+    }
+
+    private static boolean isOpenDSOID(final String oid) {
+        return oid.startsWith(SchemaConstants.OID_OPENDS_SERVER_BASE + ".");
+    }
+
+    private static boolean isCollationMatchingRule(final String oid) {
+        return oid.startsWith("1.3.6.1.4.1.42.2.27.9.4.");
+    }
+
+    private static String splitNameIntoWords(final String name) {
+        String splitName = name.replaceAll("([A-Z][a-z])", "_$1");
+        splitName = splitName.replaceAll("([a-z])([A-Z])", "$1_$2");
+        splitName = splitName.replaceAll("[-.]", "");
+
+        return splitName.toUpperCase(Locale.ENGLISH);
+    }
+
+    private static void testSplitNameIntoWords() {
+        final String[][] values =
+                new String[][] { { "oneTwoThree", "ONE_TWO_THREE" },
+                    { "oneTWOThree", "ONE_TWO_THREE" }, { "oneX500Three", "ONE_X500_THREE" },
+                    { "oneTwoX500", "ONE_TWO_X500" }, { "oneTwoX500", "ONE_TWO_X500" },
+                    { "x500TwoThree", "X500_TWO_THREE" }, };
+
+        for (final String[] test : values) {
+            final String actual = splitNameIntoWords(test[0]);
+            final String expected = test[1];
+            if (!actual.equals(expected)) {
+                System.out.println("Test Split Failure: " + test[0] + " -> " + actual + " != "
+                        + expected);
+            }
+        }
+    }
+
+    private static String toCodeJavaDoc(final String text) {
+        return String.format("{@code %s}", text);
+    }
+
+    private static String toJavaName(final String splitName) {
+        final StringBuilder builder = new StringBuilder();
+        for (final String word : splitName.split("_")) {
+            if (ABBREVIATIONS.contains(word)) {
+                builder.append(word);
+            } else {
+                builder.append(word.charAt(0));
+                if (word.length() > 1) {
+                    builder.append(word.substring(1).toLowerCase(Locale.ENGLISH));
+                }
+            }
+        }
+        return builder.toString();
+    }
+
+    private GenerateCoreSchema() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GuideSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GuideSyntaxImpl.java
new file mode 100644
index 0000000..2856fe8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GuideSyntaxImpl.java
@@ -0,0 +1,314 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GUIDE_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements the guide attribute syntax, which may be used to
+ * provide criteria for generating search filters for entries, optionally tied
+ * to a specified objectclass.
+ */
+final class GuideSyntaxImpl extends AbstractSyntaxImpl {
+    /**
+     * Determines whether the provided string represents a valid criteria
+     * according to the guide syntax.
+     *
+     * @param schema
+     *            The schema in which this syntax is defined.
+     * @param criteria
+     *            The portion of the criteria for which to make the
+     *            determination.
+     * @param valueStr
+     *            The complete guide value provided by the client.
+     * @param invalidReason
+     *            The buffer to which to append the reason that the criteria is
+     *            invalid if a problem is found.
+     * @return <CODE>true</CODE> if the provided string does contain a valid
+     *         criteria, or <CODE>false</CODE> if not.
+     */
+    static boolean criteriaIsValid(final Schema schema, final String criteria,
+            final String valueStr, final LocalizableMessageBuilder invalidReason) {
+        // See if the criteria starts with a '!'. If so, then just evaluate
+        // everything after that as a criteria.
+        char c = criteria.charAt(0);
+        if (c == '!') {
+            return criteriaIsValid(schema, criteria.substring(1), valueStr, invalidReason);
+        }
+
+        // See if the criteria starts with a '('. If so, then find the
+        // corresponding ')' and parse what's in between as a criteria.
+        if (c == '(') {
+            final int length = criteria.length();
+            int depth = 1;
+
+            for (int i = 1; i < length; i++) {
+                c = criteria.charAt(i);
+                if (c == ')') {
+                    depth--;
+                    if (depth == 0) {
+                        final String subCriteria = criteria.substring(1, i);
+                        if (!criteriaIsValid(schema, subCriteria, valueStr, invalidReason)) {
+                            return false;
+                        }
+
+                        // If we are at the end of the value, then it was valid.
+                        // Otherwise, the next character must be a pipe or an
+                        // ampersand followed by another set of criteria.
+                        if (i == length - 1) {
+                            return true;
+                        } else {
+                            c = criteria.charAt(i + 1);
+                            if (c == '|' || c == '&') {
+                                return criteriaIsValid(schema, criteria.substring(i + 2), valueStr,
+                                        invalidReason);
+                            } else {
+                                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(
+                                        valueStr, criteria, c, i + 1));
+                                return false;
+                            }
+                        }
+                    }
+                } else if (c == '(') {
+                    depth++;
+                }
+            }
+
+            // If we've gotten here, then we went through the entire value
+            // without finding the appropriate closing parenthesis.
+
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN.get(valueStr, criteria));
+            return false;
+        }
+
+        // See if the criteria starts with a '?'. If so, then it must be
+        // either "?true" or "?false".
+        if (c == '?') {
+            if (criteria.startsWith("?true")) {
+                if (criteria.length() == 5) {
+                    return true;
+                } else {
+                    // The only characters allowed next are a pipe or an
+                    // ampersand.
+                    c = criteria.charAt(5);
+                    if (c == '|' || c == '&') {
+                        return criteriaIsValid(schema, criteria.substring(6), valueStr,
+                                invalidReason);
+                    } else {
+                        invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(valueStr,
+                                criteria, c, 5));
+                        return false;
+                    }
+                }
+            } else if (criteria.startsWith("?false")) {
+                if (criteria.length() == 6) {
+                    return true;
+                } else {
+                    // The only characters allowed next are a pipe or an
+                    // ampersand.
+                    c = criteria.charAt(6);
+                    if (c == '|' || c == '&') {
+                        return criteriaIsValid(schema, criteria.substring(7), valueStr,
+                                invalidReason);
+                    } else {
+                        invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(valueStr,
+                                criteria, c, 6));
+                        return false;
+                    }
+                }
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK.get(valueStr,
+                        criteria));
+                return false;
+            }
+        }
+
+        // See if the criteria is either "true" or "false". If so, then it
+        // is valid.
+        if ("true".equals(criteria) || "false".equals(criteria)) {
+            return true;
+        }
+
+        // The only thing that will be allowed is an attribute type name or
+        // OID followed by a dollar sign and a match type. Find the dollar
+        // sign and verify whether the value before it is a valid attribute
+        // type name or OID.
+        final int dollarPos = criteria.indexOf('$');
+        if (dollarPos < 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR.get(valueStr, criteria));
+            return false;
+        } else if (dollarPos == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_ATTR.get(valueStr, criteria));
+            return false;
+        } else if (dollarPos == criteria.length() - 1) {
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE.get(valueStr, criteria));
+            return false;
+        } else {
+            try {
+                readOID(new SubstringReader(criteria.substring(0, dollarPos)),
+                    schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+            } catch (final DecodeException de) {
+                invalidReason.append(de.getMessageObject());
+                return false;
+            }
+        }
+
+        // The substring immediately after the dollar sign must be one of
+        // "eq", "substr", "ge", "le", or "approx". It may be followed by
+        // the end of the value, a pipe, or an ampersand.
+        int endPos;
+        c = criteria.charAt(dollarPos + 1);
+        switch (c) {
+        case 'e':
+            if (criteria.startsWith("eq", dollarPos + 1)) {
+                endPos = dollarPos + 3;
+                break;
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr,
+                        criteria, dollarPos + 1));
+                return false;
+            }
+
+        case 's':
+            if (criteria.startsWith("substr", dollarPos + 1)) {
+                endPos = dollarPos + 7;
+                break;
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr,
+                        criteria, dollarPos + 1));
+                return false;
+            }
+
+        case 'g':
+            if (criteria.startsWith("ge", dollarPos + 1)) {
+                endPos = dollarPos + 3;
+                break;
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr,
+                        criteria, dollarPos + 1));
+                return false;
+            }
+
+        case 'l':
+            if (criteria.startsWith("le", dollarPos + 1)) {
+                endPos = dollarPos + 3;
+                break;
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr,
+                        criteria, dollarPos + 1));
+                return false;
+            }
+
+        case 'a':
+            if (criteria.startsWith("approx", dollarPos + 1)) {
+                endPos = dollarPos + 7;
+                break;
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr,
+                        criteria, dollarPos + 1));
+                return false;
+            }
+
+        default:
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(valueStr, criteria,
+                    dollarPos + 1));
+            return false;
+        }
+
+        // See if we are at the end of the value. If so, then it is valid.
+        // Otherwise, the next character must be a pipe or an ampersand.
+        if (endPos >= criteria.length()) {
+            return true;
+        } else {
+            c = criteria.charAt(endPos);
+            if (c == '|' || c == '&') {
+                return criteriaIsValid(schema, criteria.substring(endPos + 1), valueStr,
+                        invalidReason);
+            } else {
+                invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(valueStr, criteria, c,
+                        endPos));
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_GUIDE_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get a lowercase string version of the provided value.
+        final String valueStr = toLowerCase(value.toString());
+
+        // Find the position of the octothorpe. If there isn't one, then the
+        // entire value should be the criteria.
+        final int sharpPos = valueStr.indexOf('#');
+        if (sharpPos < 0) {
+            return criteriaIsValid(schema, valueStr, valueStr, invalidReason);
+        }
+
+        // Get the objectclass and see if it is a valid name or OID.
+        final String ocName = valueStr.substring(0, sharpPos).trim();
+        final int ocLength = ocName.length();
+        if (ocLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_OC1.get(valueStr));
+            return false;
+        }
+
+        try {
+            readOID(new SubstringReader(ocName.substring(0, ocLength)),
+                    schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+        } catch (final DecodeException de) {
+            invalidReason.append(de.getMessageObject());
+            return false;
+        }
+
+        // The rest of the value must be the criteria.
+        return criteriaIsValid(schema, valueStr.substring(sharpPos + 1), valueStr, invalidReason);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxImpl.java
new file mode 100644
index 0000000..1d3e472
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxImpl.java
@@ -0,0 +1,84 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_IA5_ILLEGAL_CHARACTER;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the IA5 string attribute syntax, which is simply a set
+ * of ASCII characters. By default, they will be treated in a case-insensitive
+ * manner, and equality, ordering, substring, and approximate matching will be
+ * allowed.
+ */
+final class IA5StringSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_IA5_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We will allow any value that does not contain any non-ASCII
+        // characters. Empty values are acceptable as well.
+        byte b;
+        for (int i = 0; i < value.length(); i++) {
+            b = value.byteAt(i);
+            if ((b & 0x7F) != b) {
+
+                final LocalizableMessage message =
+                        WARN_ATTR_SYNTAX_IA5_ILLEGAL_CHARACTER.get(value.toString(), String
+                                .valueOf(b));
+                invalidReason.append(message);
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..c6cf7b4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleImpl.java
@@ -0,0 +1,43 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.IntegerOrderingMatchingRuleImpl.normalizeValueAndEncode;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the integerMatch matching rule defined in X.520 and
+ * referenced in RFC 2252. The implementation of this matching rule is
+ * intentionally aligned with the ordering matching rule so that they could
+ * potentially share the same index.
+ */
+final class IntegerEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    IntegerEqualityMatchingRuleImpl() {
+        super(EMR_INTEGER_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalizeValueAndEncode(value);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerFirstComponentEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerFirstComponentEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..3bb099b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerFirstComponentEqualityMatchingRuleImpl.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements the integerFirstComponentMatch matching rule defined in
+ * X.520 and referenced in RFC 2252. This rule is intended for use with
+ * attributes whose values contain a set of parentheses enclosing a
+ * space-delimited set of names and/or name-value pairs (like attribute type or
+ * objectclass descriptions) in which the "first component" is the first item
+ * after the opening parenthesis.
+ */
+final class IntegerFirstComponentEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    IntegerFirstComponentEqualityMatchingRuleImpl() {
+        super(EMR_INTEGER_FIRST_COMPONENT_NAME);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
+        try {
+            final String definition = assertionValue.toString();
+            return defaultAssertion(normalizeRuleID(new SubstringReader(definition)));
+        } catch (final Exception e) {
+            logger.debug(LocalizableMessage.raw("%s", e));
+            throw DecodeException.error(ERR_EMR_INTFIRSTCOMP_FIRST_COMPONENT_NOT_INT.get(assertionValue));
+        }
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        final String definition = value.toString();
+        final SubstringReader reader = new SubstringReader(definition);
+
+        // We'll do this a character at a time. First, skip over any leading whitespace.
+        reader.skipWhitespaces();
+
+        if (reader.remaining() <= 0) {
+            // This means that the value was empty or contained only whitespace.
+            // That is illegal.
+            throw DecodeException.error(ERR_ATTR_SYNTAX_EMPTY_VALUE.get());
+        }
+
+        // The next character must be an open parenthesis.
+        // If it is not, then that is an error.
+        final char c = reader.read();
+        if (c != '(') {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, c));
+        }
+
+        // Skip over any spaces immediately following the opening parenthesis.
+        reader.skipWhitespaces();
+
+        // The next set of characters must be the OID.
+        return normalizeRuleID(reader);
+    }
+
+    private ByteString normalizeRuleID(final SubstringReader reader) throws DecodeException {
+        return ByteString.valueOfInt(SchemaUtils.readRuleID(reader));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..0a14a37
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleImpl.java
@@ -0,0 +1,140 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_ILLEGAL_INTEGER;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.math.BigInteger;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the integerOrderingMatch matching rule defined in X.520
+ * and referenced in RFC 4519. The implementation of this matching rule is
+ * intentionally aligned with the equality matching rule so that they could
+ * potentially share the same index.
+ */
+final class IntegerOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+    /** Sign mask to be used when encoding zero or positive integers. */
+    static final byte SIGN_MASK_POSITIVE = (byte) 0x00;
+
+    /** Sign mask to be used when encoding negative integers. */
+    static final byte SIGN_MASK_NEGATIVE = (byte) 0xff;
+
+    /**
+     * Encodes an integer using a format which is suitable for comparisons using
+     * {@link ByteSequence#compareTo(ByteSequence)}. The integer is encoded as
+     * follows:
+     * <ul>
+     * <li>bit 0: sign bit, 0 = negative, 1 = positive
+     * <li>bits 1-3: length of the encoded length in bytes (0 when length is <
+     *     2^4, 4 when length is < 2^32)
+     * <li>bits 4-7: encoded length when length is < 2^4
+     * <li>bits 4-15: encoded length when length is < 2^12
+     * <li>bits 4-23: encoded length when length is < 2^20
+     * <li>bits 4-31: encoded length when length is < 2^28
+     * <li>bits 4-35: encoded length when length is < 2^31 (bits 35-39 are
+     *     always zero, because an int is 32 bits)
+     * <li>remaining: byte encoding of the absolute value of the integer.
+     * </ul>
+     * When the value is negative all bits from bit 1 onwards are inverted.
+     */
+    static ByteString normalizeValueAndEncode(final ByteSequence value) throws DecodeException {
+        final BigInteger bi;
+        try {
+            bi = new BigInteger(value.toString());
+        } catch (final Exception e) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_ILLEGAL_INTEGER.get(value));
+        }
+
+        /*
+         * BigInteger.toByteArray() always includes a sign bit, which means that
+         * we gain an extra zero byte for numbers that require a multiple of 8
+         * bits, e.g. 128-255, 32768-65535, because the sign bit overflows into
+         * an additional byte. We'll strip it out in that case because we encode
+         * the sign bit in the header.
+         */
+        final byte[] absBytes = bi.abs().toByteArray();
+        final int length = absBytes.length;
+        final boolean removeLeadingByte = length > 1 && absBytes[0] == 0;
+        final int trimmedLength = removeLeadingByte ? length - 1 : length;
+        final int startIndex = removeLeadingByte ? 1 : 0;
+        final byte signMask = bi.signum() < 0 ? SIGN_MASK_NEGATIVE : SIGN_MASK_POSITIVE;
+
+        // Encode the sign, length of the length, and the length.
+        final ByteStringBuilder builder = new ByteStringBuilder(trimmedLength + 5);
+        encodeHeader(builder, trimmedLength, signMask);
+
+        // Encode the absolute value of the integer..
+        for (int i = startIndex; i < length; i++) {
+            builder.appendByte(absBytes[i] ^ signMask);
+        }
+
+        return builder.toByteString();
+    }
+
+    // Package private for unit testing.
+    static void encodeHeader(final ByteStringBuilder builder, final int length,
+            final byte signMask) {
+        if ((length & 0x0000000F) == length) {
+            // 0000xxxx
+            final byte b0 = (byte) (0x80 | length & 0x0F);
+            builder.appendByte(b0 ^ signMask);
+        } else if ((length & 0x00000FFF) == length) {
+            // 0001xxxx xxxxxxxx
+            final byte b0 = (byte) (0x90 | length >> 8 & 0x0F);
+            builder.appendByte(b0 ^ signMask);
+            builder.appendByte(length & 0xFF ^ signMask);
+        } else if ((length & 0x000FFFFF) == length) {
+            // 0010xxxx xxxxxxxx xxxxxxxx
+            final byte b0 = (byte) (0xA0 | length >> 16 & 0x0F);
+            builder.appendByte(b0 ^ signMask);
+            builder.appendByte(length >> 8 & 0xFF ^ signMask);
+            builder.appendByte(length & 0xFF ^ signMask);
+        } else if ((length & 0x0FFFFFFF) == length) {
+            // 0011xxxx xxxxxxxx xxxxxxxx xxxxxxxx
+            final byte b0 = (byte) (0xB0 | length >> 24 & 0x0F);
+            builder.appendByte(b0 ^ signMask);
+            builder.appendByte(length >> 16 & 0xFF ^ signMask);
+            builder.appendByte(length >> 8 & 0xFF ^ signMask);
+            builder.appendByte(length & 0xFF ^ signMask);
+        } else {
+            // 0100xxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxx0000
+            final byte b0 = (byte) (0xC0 | length >> 28 & 0x0F);
+            builder.appendByte(b0 ^ signMask);
+            builder.appendByte(length >> 20 & 0xFF ^ signMask);
+            builder.appendByte(length >> 12 & 0xFF ^ signMask);
+            builder.appendByte(length >> 4 & 0xFF ^ signMask);
+            builder.appendByte(length << 4 & 0xFF ^ signMask);
+        }
+    }
+
+    public IntegerOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_INTEGER_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        return normalizeValueAndEncode(value);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxImpl.java
new file mode 100644
index 0000000..ada5055
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxImpl.java
@@ -0,0 +1,170 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_INTEGER_DASH_NEEDS_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_INTEGER_EMPTY_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_INTEGER_INITIAL_ZERO;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_INTEGER_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_INTEGER_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_INTEGER_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class defines the integer attribute syntax, which holds an
+ * arbitrarily-long integer value. Equality, ordering, and substring matching
+ * will be allowed by default.
+ */
+final class IntegerSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_INTEGER_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_INTEGER_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_INTEGER_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_EXACT_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String valueString = value.toString();
+        final int length = valueString.length();
+
+        if (length == 0) {
+            invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_EMPTY_VALUE.get(valueString));
+            return false;
+        } else if (length == 1) {
+            switch (valueString.charAt(0)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                return true;
+            case '-':
+                invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_DASH_NEEDS_VALUE.get(valueString));
+                return false;
+            default:
+                invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER.get(valueString,
+                        valueString.charAt(0), 0));
+                return false;
+            }
+        } else {
+            boolean negative = false;
+
+            switch (valueString.charAt(0)) {
+            case '0':
+                invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INITIAL_ZERO.get(valueString));
+                return false;
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            case '-':
+                // This is fine too.
+                negative = true;
+                break;
+            default:
+                invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER.get(valueString,
+                        valueString.charAt(0), 0));
+                return false;
+            }
+
+            switch (valueString.charAt(1)) {
+            case '0':
+                // This is fine as long as the value isn't negative.
+                if (negative) {
+                    invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INITIAL_ZERO.get(valueString));
+                    return false;
+                }
+                break;
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER.get(valueString,
+                        valueString.charAt(0), 0));
+                return false;
+            }
+
+            for (int i = 2; i < length; i++) {
+                switch (valueString.charAt(i)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    // These are all fine.
+                    break;
+                default:
+                    invalidReason.append(WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER.get(
+                            valueString, valueString.charAt(0), 0));
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxImpl.java
new file mode 100644
index 0000000..35a0da1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxImpl.java
@@ -0,0 +1,89 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+/**
+ * This class implements the JPEG attribute syntax. This is actually
+ * two specifications - JPEG and JFIF. As an extension we allow JPEG
+ * and Exif, which is what most digital cameras use. We only check for
+ * valid JFIF and Exif headers.
+ */
+final class JPEGSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_JPEG_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        return schema.getOption(ALLOW_MALFORMED_JPEG_PHOTOS) || isValidJfif(value) || isValidExif(value);
+    }
+
+    /**
+     * JFIF files start:
+     * <pre>
+     * 0xff 0xd8 0xff 0xe0 LH LL 0x4a 0x46 0x49 0x46 ...
+     * SOI       APP0      len   "JFIF"
+     * </pre>
+     * So all legal values must be at least 10 bytes long
+     */
+    private boolean isValidJfif(final ByteSequence value) {
+        return value.length() >= 10
+                && value.byteAt(0) == (byte) 0xff && value.byteAt(1) == (byte) 0xd8
+                && value.byteAt(2) == (byte) 0xff && value.byteAt(3) == (byte) 0xe0
+                && value.byteAt(6) == 'J' && value.byteAt(7) == 'F'
+                && value.byteAt(8) == 'I' && value.byteAt(9) == 'F';
+    }
+
+    /**
+     * Exif files (from most digital cameras) start:
+     * <pre>
+     * 0xff 0xd8 0xff 0xe1 LH LL 0x45 0x78 0x69 0x66 ...
+     * SOI       APP1      len   "Exif"
+     * </pre>
+     * So all legal values must be at least 10 bytes long
+     */
+    private boolean isValidExif(final ByteSequence value) {
+        return value.length() >= 10
+                && value.byteAt(0) == (byte) 0xff && value.byteAt(1) == (byte) 0xd8
+                && value.byteAt(2) == (byte) 0xff && value.byteAt(3) == (byte) 0xe1
+                && value.byteAt(6) == 'E' && value.byteAt(7) == 'x'
+                && value.byteAt(8) == 'i' && value.byteAt(9) == 'f';
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/KeywordEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/KeywordEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..9043a96
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/KeywordEqualityMatchingRuleImpl.java
@@ -0,0 +1,134 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StringPrepProfile.CASE_FOLD;
+import static com.forgerock.opendj.util.StringPrepProfile.TRIM;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class implements the keywordMatch matching rule defined in X.520. That
+ * document defines "keyword" as implementation-specific, but in this case we
+ * will consider it a match if the assertion value is contained within the
+ * attribute value and is bounded by the start or the end of the attribute value
+ * or any of the following characters: <BR>
+ * <UL>
+ * <LI>A space</LI>
+ * <LI>A period</LI>
+ * <LI>A comma</LI>
+ * <LI>A slash</LI>
+ * <LI>A dollar sign</LI>
+ * <LI>A plus sign</LI>
+ * <LI>A dash</LI>
+ * <LI>An underscore</LI>
+ * <LI>An octothorpe</LI>
+ * <LI>An equal sign</LI>
+ * </UL>
+ */
+final class KeywordEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    KeywordEqualityMatchingRuleImpl() {
+        super(EMR_KEYWORD_NAME);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue)
+            throws DecodeException {
+        final String normalStr = normalize(assertionValue);
+
+        return new Assertion() {
+            @Override
+            public ConditionResult matches(final ByteSequence attributeValue) {
+                // See if the assertion value is contained in the attribute
+                // value. If not, then it isn't a match.
+                final String valueStr1 = attributeValue.toString();
+
+                final int pos = valueStr1.indexOf(normalStr);
+                if (pos < 0) {
+                    return ConditionResult.FALSE;
+                }
+
+                if (pos > 0) {
+                    final char c = valueStr1.charAt(pos - 1);
+                    if (!isAcceptable(c)) {
+                        return ConditionResult.FALSE;
+                    }
+                }
+
+                if (valueStr1.length() > pos + normalStr.length()) {
+                    final char c = valueStr1.charAt(pos + normalStr.length());
+                    if (!isAcceptable(c)) {
+                        return ConditionResult.FALSE;
+                    }
+                }
+
+                // If we've gotten here, then we can assume it is a match.
+                return ConditionResult.TRUE;
+            }
+
+            private boolean isAcceptable(final char c) {
+                switch (c) {
+                case ' ':
+                case '.':
+                case ',':
+                case '/':
+                case '$':
+                case '+':
+                case '-':
+                case '_':
+                case '#':
+                case '=':
+                    return true;
+
+                default:
+                    return false;
+                }
+            }
+
+            @Override
+            public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                return factory.createMatchAllQuery();
+            }
+        };
+    }
+
+    @Override
+    public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return ByteString.valueOfUtf8(normalize(value));
+    }
+
+    private String normalize(final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD).toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxDescriptionSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxDescriptionSyntaxImpl.java
new file mode 100644
index 0000000..9a70761
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxDescriptionSyntaxImpl.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class defines the LDAP syntax description syntax, which is used to hold
+ * attribute syntax definitions in the schema. The format of this syntax is
+ * defined in RFC 2252.
+ */
+final class LDAPSyntaxDescriptionSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_LDAP_SYNTAX_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeNameForm method to determine if the value is
+        // acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final String oid = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+
+            Map<String, List<String>> extraProperties = Collections.emptyMap();
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = SchemaUtils.readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the syntax. It is an
+                    // arbitrary string of characters enclosed in single quotes.
+                    SchemaUtils.readQuotedString(reader);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    if (extraProperties.isEmpty()) {
+                        extraProperties = new HashMap<>();
+                    }
+                    extraProperties.put(tokenName, SchemaUtils.readExtensions(reader));
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            for (final Map.Entry<String, List<String>> property : extraProperties.entrySet()) {
+                if ("x-pattern".equalsIgnoreCase(property.getKey())) {
+                    final Iterator<String> values = property.getValue().iterator();
+                    if (values.hasNext()) {
+                        final String pattern = values.next();
+                        try {
+                            Pattern.compile(pattern);
+                        } catch (final Exception e) {
+                            throwDecodeException(logger,
+                                WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_PATTERN.get(oid, pattern));
+                        }
+                        break;
+                    }
+                } else if ("x-enum".equalsIgnoreCase(property.getKey())) {
+                    final List<String> values = property.getValue();
+                    for (int i = 0; i < values.size() - 1; i++) {
+                        final String entry = values.get(i);
+                        for (int j = i + 1; j < values.size(); j++) {
+                            if (entry.equals(values.get(j))) {
+                                throwDecodeException(logger,
+                                    WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_DUPLICATE_VALUE.get(oid, entry, j));
+                            }
+                        }
+                    }
+                }
+            }
+
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRule.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRule.java
new file mode 100644
index 0000000..aa5f7de
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRule.java
@@ -0,0 +1,558 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class defines a data structure for storing and interacting with matching
+ * rules, which are used by servers to compare attribute values against
+ * assertion values when performing Search and Compare operations. They are also
+ * used to identify the value to be added or deleted when modifying entries, and
+ * are used when comparing a purported distinguished name with the name of an
+ * entry.
+ * <p>
+ * Matching rule implementations must extend the
+ * <code>MatchingRuleImplementation</code> class so they can be used by OpenDJ.
+ * <p>
+ * Where ordered sets of names, or extra properties are provided, the ordering
+ * will be preserved when the associated fields are accessed via their getters
+ * or via the {@link #toString()} methods.
+ */
+public final class MatchingRule extends SchemaElement {
+
+    /** A fluent API for incrementally constructing matching rules. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private String oid;
+        private final List<String> names = new LinkedList<>();
+        private boolean isObsolete;
+        private String syntaxOID;
+        private MatchingRuleImpl impl;
+
+        Builder(final MatchingRule mr, final SchemaBuilder builder) {
+            super(builder, mr);
+            this.oid = mr.oid;
+            this.names.addAll(mr.names);
+            this.isObsolete = mr.isObsolete;
+            this.syntaxOID = mr.syntaxOID;
+            this.impl = mr.impl;
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            oid(oid);
+        }
+
+        /**
+         * Adds this matching rule to the schema, throwing a
+         * {@code ConflictingSchemaElementException} if there is an existing
+         * matching rule with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing matching rule with the same
+         *             numeric OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addMatchingRule(new MatchingRule(this), false);
+        }
+
+        /**
+         * Adds this matching rule to the schema overwriting any existing matching rule with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addMatchingRule(new MatchingRule(this), true);
+        }
+
+        /**
+         * Adds this matching rule to the schema, overwriting any existing matching rule
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any matching rule with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(Arrays.asList(names));
+        }
+
+        /**
+         * Specifies whether or not this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete (default is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this matching rule.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        /**
+         * Sets the syntax OID of this matching rule.
+         *
+         * @param syntax
+         *            The syntax OID.
+         * @return This builder.
+         */
+        public Builder syntaxOID(final String syntax) {
+            this.syntaxOID = syntax;
+            return this;
+        }
+
+        /**
+         * Sets the matching rule implementation.
+         *
+         * @param implementation
+         *            The matching rule implementation.
+         * @return This builder.
+         */
+        public Builder implementation(final MatchingRuleImpl implementation) {
+            this.impl = implementation;
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user friendly names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user friendly name.
+         *
+         * @param name
+         *            The user friendly name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(final String name) {
+            names.remove(name);
+            return this;
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+    }
+
+    private final String oid;
+    private final List<String> names;
+    private final boolean isObsolete;
+    private final String syntaxOID;
+    private MatchingRuleImpl impl;
+    private Syntax syntax;
+    private Schema schema;
+
+    private MatchingRule(final Builder builder) {
+        super(builder);
+
+        // Checks for required attributes.
+        if (builder.oid == null || builder.oid.isEmpty()) {
+            throw new IllegalArgumentException("An OID must be specified.");
+        }
+        if (builder.syntaxOID == null || builder.syntaxOID.isEmpty()) {
+            throw new IllegalArgumentException("Required syntax OID must be specified.");
+        }
+
+        oid = builder.oid;
+        names = SchemaUtils.unmodifiableCopyOfList(builder.names);
+        isObsolete = builder.isObsolete;
+        syntaxOID = builder.syntaxOID;
+        impl = builder.impl;
+    }
+
+    /**
+     * Returns {@code true} if the provided object is a matching rule having the
+     * same numeric OID as this matching rule.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a matching rule having the
+     *         same numeric OID as this matching rule.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof MatchingRule) {
+            final MatchingRule other = (MatchingRule) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing matching operations on that value.
+     * The assertion value is guaranteed to be valid against this matching
+     * rule's assertion syntax.
+     *
+     * @param value
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if the syntax of the value is not valid.
+     */
+    public Assertion getAssertion(final ByteSequence value) throws DecodeException {
+        return impl.getAssertion(schema, value);
+    }
+
+    /**
+     * Returns the normalized form of the provided assertion substring values,
+     * which is best suited for efficiently performing matching operations on
+     * that value.
+     *
+     * @param subInitial
+     *            The normalized substring value fragment that should appear at
+     *            the beginning of the target value.
+     * @param subAnyElements
+     *            The normalized substring value fragments that should appear in
+     *            the middle of the target value.
+     * @param subFinal
+     *            The normalized substring value fragment that should appear at
+     *            the end of the target value.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if the syntax of the value is not valid.
+     */
+    public Assertion getSubstringAssertion(final ByteSequence subInitial,
+            final List<? extends ByteSequence> subAnyElements, final ByteSequence subFinal)
+            throws DecodeException {
+        return impl.getSubstringAssertion(schema, subInitial, subAnyElements, subFinal);
+    }
+
+    /**
+     * Returns the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing greater than or equal ordering
+     * matching operations on that value. The assertion value is guaranteed to
+     * be valid against this matching rule's assertion syntax.
+     *
+     * @param value
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if the syntax of the value is not valid.
+     */
+    public Assertion getGreaterOrEqualAssertion(final ByteSequence value) throws DecodeException {
+        return impl.getGreaterOrEqualAssertion(schema, value);
+    }
+
+    /**
+     * Returns the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing greater than or equal ordering
+     * matching operations on that value. The assertion value is guaranteed to
+     * be valid against this matching rule's assertion syntax.
+     *
+     * @param value
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if the syntax of the value is not valid.
+     */
+    public Assertion getLessOrEqualAssertion(final ByteSequence value) throws DecodeException {
+        return impl.getLessOrEqualAssertion(schema, value);
+    }
+
+    /**
+     * Returns the indexers for this matching rule configured using the provided indexing options.
+     *
+     * @param options
+     *            The indexing options
+     * @return the collection of indexers for this matching rule.
+     */
+    public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+        return impl.createIndexers(options);
+    }
+
+    /**
+     * Returns the name or OID for this schema definition. If it has one or more
+     * names, then the primary name will be returned. If it does not have any
+     * names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return oid;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Returns the OID for this schema definition.
+     *
+     * @return The OID for this schema definition.
+     */
+    public String getOID() {
+        return oid;
+    }
+
+    /**
+     * Returns the OID of the assertion value syntax with which this matching
+     * rule is associated.
+     *
+     * @return The OID of the assertion value syntax with which this matching
+     *         rule is associated.
+     */
+    public Syntax getSyntax() {
+        return syntax;
+    }
+
+    /**
+     * Returns the hash code for this matching rule. It will be calculated as
+     * the hash code of the numeric OID.
+     *
+     * @return The hash code for this matching rule.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return <code>true</code> if the specified name is assigned to this
+     *         schema definition, or <code>false</code> if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name or OID.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <code>true</code> if the provided value matches the OID or one of
+     *         the names assigned to this schema definition, or
+     *         <code>false</code> if not.
+     */
+    public boolean hasNameOrOID(final String value) {
+        return hasName(value) || getOID().equals(value);
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return <code>true</code> if this schema definition is declared
+     *         "obsolete", or <code>false</code> if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    /**
+     * Returns the normalized form of the provided attribute value, which is
+     * best suited for efficiently performing matching operations on that value.
+     * The returned normalized representation can be compared for equality with
+     * other values normalized with this matching rule using
+     * {@link ByteSequence#equals(Object)}. In addition, normalized values can
+     * be compared using {@link ByteSequence#compareTo(ByteSequence)}, although
+     * the sort order is only defined for ordering matching rules.
+     *
+     * @param value
+     *            The attribute value to be normalized.
+     * @return The normalized version of the provided attribute value.
+     * @throws DecodeException
+     *             If the syntax of the value is not valid.
+     */
+    public ByteString normalizeAttributeValue(final ByteSequence value) throws DecodeException {
+        return impl.normalizeAttributeValue(schema, value);
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        buffer.append(" SYNTAX ");
+        buffer.append(syntaxOID);
+    }
+
+    void validate(final Schema schema, final List<LocalizableMessage> warnings)
+            throws SchemaException {
+        // Try finding an implementation in the core schema
+        if (impl == null && Schema.getDefaultSchema().hasMatchingRule(oid)) {
+            impl = Schema.getDefaultSchema().getMatchingRule(oid).impl;
+        }
+        if (impl == null && Schema.getCoreSchema().hasMatchingRule(oid)) {
+            impl = Schema.getCoreSchema().getMatchingRule(oid).impl;
+        }
+
+        if (impl == null) {
+            final MatchingRule defaultMatchingRule = schema.getDefaultMatchingRule();
+            if (defaultMatchingRule.impl == null) {
+                // The default matching rule was never validated.
+                defaultMatchingRule.validate(schema, warnings);
+            }
+            impl = defaultMatchingRule.impl;
+            final LocalizableMessage message =
+                    WARN_MATCHING_RULE_NOT_IMPLEMENTED1.get(getNameOrOID(), schema
+                            .getDefaultMatchingRule().getOID());
+            warnings.add(message);
+        }
+
+        try {
+            // Make sure the specific syntax is defined in this schema.
+            syntax = schema.getSyntax(syntaxOID);
+        } catch (final UnknownSchemaElementException e) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_MR_UNKNOWN_SYNTAX1.get(getNameOrOID(), syntaxOID);
+            throw new SchemaException(message, e);
+        }
+
+        this.schema = schema;
+    }
+
+    /**
+     * Indicates if the matching rule has been validated, which means it has a
+     * non-null schema.
+     *
+     * @return {@code true} if and only if this matching rule has been validated
+     */
+    boolean isValidated() {
+        return schema != null;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleImpl.java
new file mode 100644
index 0000000..fe17c06
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleImpl.java
@@ -0,0 +1,132 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This interface defines the set of methods that must be implemented to define
+ * a new matching rule.
+ */
+public interface MatchingRuleImpl {
+
+    /**
+     * Retrieves the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing less than matching operations on
+     * that value. The assertion value is guaranteed to be valid against this
+     * matching rule's assertion syntax.
+     *
+     * @param schema
+     *            The schema in which this matching rule is defined.
+     * @param assertionValue
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if an syntax error occurred while parsing the value.
+     */
+    Assertion getAssertion(Schema schema, ByteSequence assertionValue) throws DecodeException;
+
+    /**
+     * Retrieves the normalized form of the provided assertion substring values,
+     * which is best suited for efficiently performing matching operations on
+     * that value.
+     *
+     * @param schema
+     *            The schema in which this matching rule is defined.
+     * @param subInitial
+     *            The normalized substring value fragment that should appear at
+     *            the beginning of the target value.
+     * @param subAnyElements
+     *            The normalized substring value fragments that should appear in
+     *            the middle of the target value.
+     * @param subFinal
+     *            The normalized substring value fragment that should appear at
+     *            the end of the target value.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if an syntax error occurred while parsing the value.
+     */
+    Assertion getSubstringAssertion(Schema schema, ByteSequence subInitial,
+            List<? extends ByteSequence> subAnyElements, ByteSequence subFinal)
+            throws DecodeException;
+
+    /**
+     * Retrieves the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing greater than or equal matching
+     * operations on that value. The assertion value is guaranteed to be valid
+     * against this matching rule's assertion syntax.
+     *
+     * @param schema
+     *            The schema in which this matching rule is defined.
+     * @param value
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if an syntax error occurred while parsing the value.
+     */
+    Assertion getGreaterOrEqualAssertion(Schema schema, ByteSequence value) throws DecodeException;
+
+    /**
+     * Retrieves the normalized form of the provided assertion value, which is
+     * best suited for efficiently performing less than or equal matching
+     * operations on that value. The assertion value is guaranteed to be valid
+     * against this matching rule's assertion syntax.
+     *
+     * @param schema
+     *            The schema in which this matching rule is defined.
+     * @param value
+     *            The syntax checked assertion value to be normalized.
+     * @return The normalized version of the provided assertion value.
+     * @throws DecodeException
+     *             if an syntax error occurred while parsing the value.
+     */
+    Assertion getLessOrEqualAssertion(Schema schema, ByteSequence value) throws DecodeException;
+
+    /**
+     * Retrieves the normalized form of the provided attribute value, which is
+     * best suited for efficiently performing matching operations on that value.
+     * Equality and ordering matching rules should return a normalized
+     * representation which can be compared with other normalized values using
+     * {@link ByteSequence#equals(Object)} and
+     * {@link ByteSequence#compareTo(ByteSequence)}.
+     *
+     * @param schema
+     *            The schema in which this matching rule is defined.
+     * @param value
+     *            The attribute value to be normalized.
+     * @return The normalized version of the provided attribute value.
+     * @throws DecodeException
+     *             If an syntax error occurred while parsing the value.
+     */
+    ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException;
+
+    /**
+     * Returns the indexers for this matching rule.
+     * @param options
+     *          The indexing options
+     * @return a non null collection of indexers for this matching rule.
+     */
+    Collection<? extends Indexer> createIndexers(IndexingOptions options);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxImpl.java
new file mode 100644
index 0000000..6901fe6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxImpl.java
@@ -0,0 +1,141 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the matching rule description syntax, which is used to
+ * hold matching rule definitions in the server schema. The format of this
+ * syntax is defined in RFC 2252.
+ */
+final class MatchingRuleSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_MATCHING_RULE_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeMatchingRule method to determine if the value
+        // is acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_MR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+            readOID(reader, allowMalformedNamesAndOptions);
+            String syntax = null;
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = SchemaUtils.readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the matching rule. It
+                    // is
+                    // an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the matching rule should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("syntax".equalsIgnoreCase(tokenName)) {
+                    syntax = readOID(reader, allowMalformedNamesAndOptions);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_MR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that a syntax was specified.
+            if (syntax == null) {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_MR_NO_SYNTAX.get(definition));
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_MR_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUse.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUse.java
new file mode 100644
index 0000000..24bb8cd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUse.java
@@ -0,0 +1,490 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Arrays.*;
+
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines a data structure for storing and interacting with a
+ * matching rule use definition, which may be used to restrict the set of
+ * attribute types that may be used for a given matching rule.
+ */
+public final class MatchingRuleUse extends SchemaElement {
+    /** A fluent API for incrementally constructing matching rule uses. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private String oid;
+        private final List<String> names = new LinkedList<>();
+        private boolean isObsolete;
+        private final Set<String> attributeOIDs = new LinkedHashSet<>();
+
+        Builder(MatchingRuleUse mru, SchemaBuilder builder) {
+            super(builder, mru);
+            this.oid = mru.oid;
+            this.names.addAll(mru.names);
+            this.isObsolete = mru.isObsolete;
+            this.attributeOIDs.addAll(mru.attributeOIDs);
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            this.oid = oid;
+        }
+
+        /**
+         * Adds this matching rule use definition to the schema, throwing a
+         * {@code  ConflictingSchemaElementException} if there is an existing
+         * matching rule definition with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing matching rule use definition with
+         *             the same numeric OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addMatchingRuleUse(new MatchingRuleUse(this), false);
+        }
+
+        /**
+         * Adds this matching rule use definition to the schema overwriting any
+         * existing matching rule use definition with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addMatchingRuleUse(new MatchingRuleUse(this), true);
+        }
+
+        /**
+         * Adds this matching rule use to the schema, overwriting any existing matching rule use
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any matching rule use with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        /**
+         * Adds the provided list of attribute types to the list of attribute
+         * type the matching rule applies to.
+         *
+         * @param attributeOIDs
+         *            The list of attribute type numeric OIDs.
+         * @return This builder.
+         */
+        public Builder attributes(Collection<String> attributeOIDs) {
+            this.attributeOIDs.addAll(attributeOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided list of attribute types to the list of attribute
+         * type the matching rule applies to.
+         *
+         * @param attributeOIDs
+         *            The list of attribute type numeric OIDs.
+         * @return This builder.
+         */
+        public Builder attributes(String... attributeOIDs) {
+            this.attributeOIDs.addAll(asList(attributeOIDs));
+            return this;
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(asList(names));
+        }
+
+        /**
+         * Specifies whether this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete
+         *            (default is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this matching rule use
+         * definition.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        /**
+         * Removes all attribute types the matching rule applies to.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllAttributes() {
+            this.attributeOIDs.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user defined names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        /**
+         * Removes the provided attribute type.
+         *
+         * @param attributeOID
+         *            The attribute type OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeAttribute(String attributeOID) {
+            this.attributeOIDs.remove(attributeOID);
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(String extensionName, String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user defined name.
+         *
+         * @param name
+         *            The user defined name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(String name) {
+            this.names.remove(name);
+            return this;
+        }
+    }
+
+    /** The OID of the matching rule associated with this matching rule use definition. */
+    private final String oid;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /** The set of attribute types with which this matching rule use is associated. */
+    private final Set<String> attributeOIDs;
+
+    private MatchingRule matchingRule;
+    private Set<AttributeType> attributes = Collections.emptySet();
+
+    private MatchingRuleUse(final Builder builder) {
+        super(builder);
+        Reject.ifNull(builder.oid);
+
+        this.oid = builder.oid;
+        this.names = unmodifiableCopyOfList(builder.names);
+        this.isObsolete = builder.isObsolete;
+        this.attributeOIDs = unmodifiableCopyOfSet(builder.attributeOIDs);
+    }
+
+    /**
+     * Returns {@code true} if the provided object is a matching rule use having
+     * the same numeric OID as this matching rule use.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a matching rule use having
+     *         the same numeric OID as this matching rule use.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof MatchingRuleUse) {
+            final MatchingRuleUse other = (MatchingRuleUse) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an unmodifiable set containing the attributes associated with
+     * this matching rule use.
+     *
+     * @return An unmodifiable set containing the attributes associated with
+     *         this matching rule use.
+     */
+    public Set<AttributeType> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Returns the matching rule for this matching rule use.
+     *
+     * @return The matching rule for this matching rule use.
+     */
+    public MatchingRule getMatchingRule() {
+        return matchingRule;
+    }
+
+    /**
+     * Returns the matching rule OID for this schema definition.
+     *
+     * @return The OID for this schema definition.
+     */
+    public String getMatchingRuleOID() {
+        return oid;
+    }
+
+    /**
+     * Returns the name or matching rule OID for this schema definition. If it
+     * has one or more names, then the primary name will be returned. If it does
+     * not have any names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return oid;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Indicates whether the provided attribute type is referenced by this
+     * matching rule use.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return {@code true} if the provided attribute type is referenced by this
+     *         matching rule use, or {@code false} if it is not.
+     */
+    public boolean hasAttribute(final AttributeType attributeType) {
+        return attributes.contains(attributeType);
+    }
+
+    /**
+     * Returns the hash code for this matching rule use. It will be calculated
+     * as the hash code of the numeric OID.
+     *
+     * @return The hash code for this matching rule use.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return <code>true</code> if the specified name is assigned to this
+     *         schema definition, or <code>false</code> if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name or
+     * matching rule OID.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <code>true</code> if the provided value matches the OID or one of
+     *         the names assigned to this schema definition, or
+     *         <code>false</code> if not.
+     */
+    public boolean hasNameOrOID(final String value) {
+        return hasName(value) || oid.equals(value);
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return <code>true</code> if this schema definition is declared
+     *         "obsolete", or <code>false</code> if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        if (!attributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = attributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" APPLIES ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" APPLIES ");
+                buffer.append(firstName);
+            }
+        }
+    }
+
+    void validate(final Schema schema) throws SchemaException {
+        try {
+            matchingRule = schema.getMatchingRule(oid);
+        } catch (final UnknownSchemaElementException e) {
+            // This is bad because the matching rule use is associated with a
+            // matching rule that we don't know anything about.
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_MATCHING_RULE1.get(getNameOrOID(), oid);
+            throw new SchemaException(message, e);
+        }
+
+        attributes = new HashSet<>(attributeOIDs.size());
+        for (final String attribute : attributeOIDs) {
+            try {
+                attributes.add(schema.getAttributeType(attribute));
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_ATTR1.get(getNameOrOID(), attribute);
+                throw new SchemaException(message, e);
+            }
+        }
+        attributes = Collections.unmodifiableSet(attributes);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxImpl.java
new file mode 100644
index 0000000..52ba9f3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxImpl.java
@@ -0,0 +1,142 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the matching rule use description syntax, which is used
+ * to hold matching rule use definitions in the server schema. The format of
+ * this syntax is defined in RFC 2252.
+ */
+final class MatchingRuleUseSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_MATCHING_RULE_USE_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeAttributeType method to determine if the
+        // value is acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+            readOID(reader, allowMalformedNamesAndOptions);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            Set<String> attributes = null;
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("applies".equalsIgnoreCase(tokenName)) {
+                    attributes = readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that the set of attributes was defined.
+            if (attributes == null || attributes.size() == 0) {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_MRUSE_NO_ATTR.get(definition));
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_MRUSE_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameAndOptionalUIDSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameAndOptionalUIDSyntaxImpl.java
new file mode 100644
index 0000000..9dfc619
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameAndOptionalUIDSyntaxImpl.java
@@ -0,0 +1,103 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_UNIQUE_MEMBER_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_NAME_AND_OPTIONAL_UID_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DN;
+
+/**
+ * This class implements the name and optional UID attribute syntax, which holds
+ * values consisting of a DN, optionally followed by an octothorpe (#) and a bit
+ * string value.
+ */
+final class NameAndOptionalUIDSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_UNIQUE_MEMBER_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_NAME_AND_OPTIONAL_UID_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String valueString = value.toString().trim();
+        final int valueLength = valueString.length();
+
+        // See if the value contains the "optional uid" portion. If we think
+        // it does, then mark its location.
+        int dnEndPos = valueLength;
+        int sharpPos = -1;
+        if (valueString.endsWith("'B") || valueString.endsWith("'b")) {
+            sharpPos = valueString.lastIndexOf("#'");
+            if (sharpPos > 0) {
+                dnEndPos = sharpPos;
+            }
+        }
+
+        // Take the DN portion of the string and try to normalize it.
+        try {
+            DN.valueOf(valueString.substring(0, dnEndPos), schema);
+        } catch (final LocalizedIllegalArgumentException e) {
+            // We couldn't normalize the DN for some reason. The value cannot
+            // be acceptable.
+            invalidReason.append(ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN.get(valueString, e
+                    .getMessageObject()));
+            return false;
+        }
+
+        // If there is an "optional uid", then normalize it and make sure it
+        // only contains valid binary digits.
+        if (sharpPos > 0) {
+            final int endPos = valueLength - 2;
+            for (int i = sharpPos + 2; i < endPos; i++) {
+                final char c = valueString.charAt(i);
+                if (c != '0' && c != '1') {
+                    invalidReason.append(ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT.get(
+                            valueString, String.valueOf(c), i));
+                    return false;
+                }
+            }
+        }
+
+        // If we've gotten here, then the value is acceptable.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameForm.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameForm.java
new file mode 100644
index 0000000..5be8d50
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameForm.java
@@ -0,0 +1,637 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAME_FORM_STRUCTURAL_CLASS_NOT_STRUCTURAL1;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_OPTIONAL_ATTR1;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_REQUIRED_ATTR1;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_STRUCTURAL_CLASS1;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
+
+/**
+ * This class defines a data structure for storing and interacting with a name
+ * form, which defines the attribute type(s) that must and/or may be used in the
+ * RDN of an entry with a given structural objectclass.
+ */
+public final class NameForm extends SchemaElement {
+
+    /** A fluent API for incrementally constructing name forms. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private boolean isObsolete;
+        private final List<String> names = new LinkedList<>();
+        private String oid;
+        private final Set<String> optionalAttributes = new LinkedHashSet<>();
+        private final Set<String> requiredAttributes = new LinkedHashSet<>();
+        private String structuralObjectClassOID;
+
+        Builder(final NameForm nf, final SchemaBuilder builder) {
+            super(builder, nf);
+            this.oid = nf.oid;
+            this.structuralObjectClassOID = nf.structuralClassOID;
+            this.isObsolete = nf.isObsolete;
+            this.names.addAll(nf.names);
+            this.requiredAttributes.addAll(nf.requiredAttributeOIDs);
+            this.optionalAttributes.addAll(nf.optionalAttributeOIDs);
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            oid(oid);
+        }
+
+        /**
+         * Adds this name form to the schema, throwing a
+         * {@code ConflictingSchemaElementException} if there is an existing
+         * name form with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing name form with the same numeric
+         *             OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addNameForm(new NameForm(this), false);
+        }
+
+        /**
+         * Adds this name form to the schema overwriting any existing name form
+         * with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addNameForm(new NameForm(this), true);
+        }
+
+        /**
+         * Adds this name form to the schema, overwriting any existing name form
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any name form with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(Arrays.asList(names));
+        }
+
+        /**
+         * Specifies whether or not this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete (default
+         *            is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this name form.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes.
+         *
+         * @param nameOrOIDs
+         *            The list of optional attributes.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final Collection<String> nameOrOIDs) {
+            this.optionalAttributes.addAll(nameOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes.
+         *
+         * @param nameOrOIDs
+         *            The list of optional attributes.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final String... nameOrOIDs) {
+            return optionalAttributes(Arrays.asList(nameOrOIDs));
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        /**
+         * Removes all user friendly names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        /**
+         * Removes all optional attributes.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllOptionalAttributes() {
+            this.optionalAttributes.clear();
+            return this;
+        }
+
+        /**
+         * Removes all required attributes.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllRequiredAttributes() {
+            this.requiredAttributes.clear();
+            return this;
+        }
+
+        @Override
+        public Builder removeExtraProperty(final String extensionName,
+                final String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes the provided user friendly name.
+         *
+         * @param name
+         *            The user friendly name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(final String name) {
+            names.remove(name);
+            return this;
+        }
+
+        /**
+         * Removes the specified optional attribute.
+         *
+         * @param nameOrOID
+         *            The optional attribute to be removed.
+         * @return This builder.
+         */
+        public Builder removeOptionalAttribute(final String nameOrOID) {
+            this.optionalAttributes.remove(nameOrOID);
+            return this;
+        }
+
+        /**
+         * Removes the specified required attribute.
+         *
+         * @param nameOrOID
+         *            The required attribute to be removed.
+         * @return This builder.
+         */
+        public Builder removeRequiredAttribute(final String nameOrOID) {
+            this.requiredAttributes.remove(nameOrOID);
+            return this;
+        }
+
+        /**
+         * Adds the provided required attributes.
+         *
+         * @param nameOrOIDs
+         *            The list of required attributes.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final Collection<String> nameOrOIDs) {
+            this.requiredAttributes.addAll(nameOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided required attributes.
+         *
+         * @param nameOrOIDs
+         *            The list of required attributes.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final String... nameOrOIDs) {
+            return requiredAttributes(Arrays.asList(nameOrOIDs));
+        }
+
+        /**
+         * Sets the structural object class.
+         *
+         * @param nameOrOID
+         *            The structural object class.
+         * @return This builder.
+         */
+        public Builder structuralObjectClassOID(final String nameOrOID) {
+            this.structuralObjectClassOID = nameOrOID;
+            return this;
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+    }
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** The OID that may be used to reference this definition. */
+    private final String oid;
+
+    /** The set of optional attribute types for this name form. */
+    private final Set<String> optionalAttributeOIDs;
+    private Set<AttributeType> optionalAttributes = Collections.emptySet();
+
+    /** The set of required attribute types for this name form. */
+    private final Set<String> requiredAttributeOIDs;
+    private Set<AttributeType> requiredAttributes = Collections.emptySet();
+
+    /** The reference to the structural objectclass for this name form. */
+    private ObjectClass structuralClass;
+    private final String structuralClassOID;
+
+    private NameForm(final Builder builder) {
+        super(builder);
+
+        // Checks for required attributes.
+        if (builder.oid == null || builder.oid.isEmpty()) {
+            throw new IllegalArgumentException("An OID must be specified.");
+        }
+        if (builder.structuralObjectClassOID == null || builder.structuralObjectClassOID.isEmpty()) {
+            throw new IllegalArgumentException("A structural class OID must be specified.");
+        }
+        if (builder.requiredAttributes == null || builder.requiredAttributes.isEmpty()) {
+            throw new IllegalArgumentException("Required attribute must be specified.");
+        }
+
+        oid = builder.oid;
+        structuralClassOID = builder.structuralObjectClassOID;
+        names = SchemaUtils.unmodifiableCopyOfList(builder.names);
+        requiredAttributeOIDs = SchemaUtils.unmodifiableCopyOfSet(builder.requiredAttributes);
+        optionalAttributeOIDs = SchemaUtils.unmodifiableCopyOfSet(builder.optionalAttributes);
+        isObsolete = builder.isObsolete;
+    }
+
+    /**
+     * Returns {@code true} if the provided object is a name form having the
+     * same numeric OID as this name form.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a name form having the
+     *         same numeric OID as this name form.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof NameForm) {
+            final NameForm other = (NameForm) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the name or numeric OID of this name form. If it has one or more
+     * names, then the primary name will be returned. If it does not have any
+     * names, then the numeric OID will be returned.
+     *
+     * @return The name or numeric OID of this name form.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return oid;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-friendly names that may
+     * be used to reference this name form.
+     *
+     * @return An unmodifiable list containing the user-friendly names that may
+     *         be used to reference this name form.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Returns the numeric OID of this name form.
+     *
+     * @return The numeric OID of this name form.
+     */
+    public String getOID() {
+        return oid;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the optional attributes of this
+     * name form.
+     *
+     * @return An unmodifiable set containing the optional attributes of this
+     *         name form.
+     */
+    public Set<AttributeType> getOptionalAttributes() {
+        return optionalAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the required attributes of this
+     * name form.
+     *
+     * @return An unmodifiable set containing the required attributes of this
+     *         name form.
+     */
+    public Set<AttributeType> getRequiredAttributes() {
+        return requiredAttributes;
+    }
+
+    /**
+     * Returns the structural objectclass of this name form.
+     *
+     * @return The structural objectclass of this name form.
+     */
+    public ObjectClass getStructuralClass() {
+        return structuralClass;
+    }
+
+    /**
+     * Returns the hash code for this name form. It will be calculated as the
+     * hash code of the numeric OID.
+     *
+     * @return The hash code for this name form.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Returns {@code true} if this name form has the specified user-friendly
+     * name.
+     *
+     * @param name
+     *            The name.
+     * @return {@code true} if this name form has the specified user-friendly
+     *         name.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if this name form has the specified user-friendly
+     * name or numeric OID.
+     *
+     * @param nameOrOID
+     *            The name or numeric OID.
+     * @return {@code true} if this name form has the specified user-friendly
+     *         name or numeric OID.
+     */
+    public boolean hasNameOrOID(final String nameOrOID) {
+        return hasName(nameOrOID) || getOID().equals(nameOrOID);
+    }
+
+    /**
+     * Returns {@code true} if this name form is "obsolete".
+     *
+     * @return {@code true} if this name form is "obsolete".
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    /**
+     * Returns {@code true} if the provided attribute type is included in the
+     * list of optional attributes for this name form.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @return {@code true} if the provided attribute type is included in the
+     *         list of optional attributes for this name form.
+     */
+    public boolean isOptional(final AttributeType attributeType) {
+        return optionalAttributes.contains(attributeType);
+    }
+
+    /**
+     * Returns {@code true} if the provided attribute type is included in the
+     * list of required attributes for this name form.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @return {@code true} if the provided attribute type is included in the
+     *         list of required attributes for this name form.
+     */
+    public boolean isRequired(final AttributeType attributeType) {
+        return requiredAttributes.contains(attributeType);
+    }
+
+    /**
+     * Returns {@code true} if the provided attribute type is included in the
+     * list of optional or required attributes for this name form.
+     *
+     * @param attributeType
+     *            The attribute type.
+     * @return {@code true} if the provided attribute type is included in the
+     *         list of optional or required attributes for this name form.
+     */
+    public boolean isRequiredOrOptional(final AttributeType attributeType) {
+        return isRequired(attributeType) || isOptional(attributeType);
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        buffer.append(" OC ");
+        buffer.append(structuralClassOID);
+
+        if (!requiredAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = requiredAttributeOIDs.iterator();
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MUST ( ");
+                buffer.append(firstName);
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+                buffer.append(" )");
+            } else {
+                buffer.append(" MUST ");
+                buffer.append(firstName);
+            }
+        }
+
+        if (!optionalAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = optionalAttributeOIDs.iterator();
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MAY ( ");
+                buffer.append(firstName);
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+                buffer.append(" )");
+            } else {
+                buffer.append(" MAY ");
+                buffer.append(firstName);
+            }
+        }
+    }
+
+    void validate(final Schema schema) throws SchemaException {
+        try {
+            structuralClass = schema.getObjectClass(structuralClassOID);
+        } catch (final UnknownSchemaElementException e) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_STRUCTURAL_CLASS1.get(getNameOrOID(),
+                            structuralClassOID);
+            throw new SchemaException(message, e);
+        }
+        if (structuralClass.getObjectClassType() != ObjectClassType.STRUCTURAL) {
+            // This is bad because the associated structural class type is not structural.
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_NAME_FORM_STRUCTURAL_CLASS_NOT_STRUCTURAL1.get(getNameOrOID(),
+                            structuralClass.getNameOrOID(), structuralClass.getObjectClassType());
+            throw new SchemaException(message);
+        }
+
+        requiredAttributes =
+              getAttributeTypes(schema, requiredAttributeOIDs, ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_REQUIRED_ATTR1);
+
+        if (!optionalAttributeOIDs.isEmpty()) {
+            optionalAttributes =
+                    getAttributeTypes(schema, optionalAttributeOIDs, ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_OPTIONAL_ATTR1);
+        }
+
+        optionalAttributes = Collections.unmodifiableSet(optionalAttributes);
+        requiredAttributes = Collections.unmodifiableSet(requiredAttributes);
+    }
+
+    private Set<AttributeType> getAttributeTypes(final Schema schema, Set<String> oids, Arg2<Object, Object> errorMsg)
+            throws SchemaException {
+        Set<AttributeType> attrTypes = new HashSet<>(oids.size());
+        for (final String oid : oids) {
+            try {
+                attrTypes.add(schema.getAttributeType(oid));
+            } catch (final UnknownSchemaElementException e) {
+                // This isn't good because it means that the name form requires
+                // an attribute type that we don't know anything about.
+                throw new SchemaException(errorMsg.get(getNameOrOID(), oid), e);
+            }
+        }
+        return attrTypes;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameFormSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameFormSyntaxImpl.java
new file mode 100644
index 0000000..8abe06b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NameFormSyntaxImpl.java
@@ -0,0 +1,154 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the name form description syntax, which is used to hold
+ * name form definitions in the server schema. The format of this syntax is
+ * defined in RFC 2252.
+ */
+final class NameFormSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_NAME_FORM_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeNameForm method to determine if the value is
+        // acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_NAME_FORM_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+            readOID(reader, allowMalformedNamesAndOptions);
+
+            String structuralClass = null;
+            Set<String> requiredAttributes = null;
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("oc".equalsIgnoreCase(tokenName)) {
+                    structuralClass = readOID(reader, allowMalformedNamesAndOptions);
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    requiredAttributes = readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_NAME_FORM_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that a structural class was specified. If not, then
+            // it cannot be valid.
+            if (structuralClass == null) {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_NAME_FORM_NO_STRUCTURAL_CLASS1.get(definition));
+            }
+
+            if (requiredAttributes == null || requiredAttributes.size() == 0) {
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_NAME_FORM_NO_REQUIRED_ATTR.get(definition));
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_NAME_FORM_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..ad47b29
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class implements the numericStringMatch matching rule defined in X.520
+ * and referenced in RFC 2252. It allows for values with numeric digits and
+ * spaces, but ignores spaces when performing matching.
+ */
+final class NumericStringEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    NumericStringEqualityMatchingRuleImpl() {
+        super(EMR_NUMERIC_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeNumericStringAttributeValue(value);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..18bca41
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This implements defines the numericStringOrderingMatch matching rule defined
+ * in X.520 and referenced in RFC 2252.
+ */
+final class NumericStringOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+
+    NumericStringOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_NUMERIC_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeNumericStringAttributeValue(value);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..3e38b5a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleImpl.java
@@ -0,0 +1,38 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class implements the numericStringSubstringsMatch matching rule defined
+ * in X.520 and referenced in RFC 2252.
+ */
+final class NumericStringSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    NumericStringSubstringMatchingRuleImpl() {
+        super(SMR_NUMERIC_STRING_NAME, EMR_NUMERIC_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeNumericStringAttributeValue(value);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSyntaxImpl.java
new file mode 100644
index 0000000..1a8843c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/NumericStringSyntaxImpl.java
@@ -0,0 +1,88 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.isDigit;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_NUMERIC_STRING_ILLEGAL_CHAR;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_NUMERIC_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_NUMERIC_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_NUMERIC_STRING_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the numeric string attribute syntax, which may be hold
+ * one or more numeric digits and/or spaces. Equality, ordering, and substring
+ * matching will be allowed by default.
+ */
+final class NumericStringSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_NUMERIC_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_NUMERIC_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_NUMERIC_STRING_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_EXACT_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String valueString = value.toString();
+        final int length = valueString.length();
+
+        // It must have at least one digit or space.
+        if (length == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE.get());
+            return false;
+        }
+
+        // Iterate through the characters and make sure they are all digits
+        // or spaces.
+        for (int i = 0; i < length; i++) {
+            final char c = valueString.charAt(i);
+            if (!isDigit(c) && c != ' ') {
+                invalidReason.append(WARN_ATTR_SYNTAX_NUMERIC_STRING_ILLEGAL_CHAR.get(valueString,
+                        String.valueOf(c), i));
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OIDSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OIDSyntaxImpl.java
new file mode 100644
index 0000000..6b3aa04
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OIDSyntaxImpl.java
@@ -0,0 +1,68 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OID_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_OID_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class defines the OID syntax, which holds either an identifier name or a
+ * numeric OID. Equality and substring matching will be allowed by default.
+ */
+final class OIDSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_OID_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        try {
+            SchemaUtils.readOID(new SubstringReader(value.toString()),
+                    schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(de.getMessageObject());
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClass.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClass.java
new file mode 100644
index 0000000..a6b276c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClass.java
@@ -0,0 +1,961 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2015-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+import static java.util.Arrays.*;
+import static java.util.Collections.*;
+
+import static org.forgerock.opendj.ldap.schema.ObjectClassType.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class defines a data structure for storing and interacting with an
+ * objectclass, which contains a collection of attributes that must and/or may
+ * be present in an entry with that objectclass.
+ * <p>
+ * Where ordered sets of names, attribute types, or extra properties are
+ * provided, the ordering will be preserved when the associated fields are
+ * accessed via their getters or via the {@link #toString()} methods.
+ */
+public final class ObjectClass extends SchemaElement {
+
+    /** A fluent API for incrementally constructing object classes. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+        private boolean isObsolete;
+        private final List<String> names = new LinkedList<>();
+        private String oid;
+        private final Set<String> optionalAttributes = new LinkedHashSet<>();
+        private final Set<String> requiredAttributes = new LinkedHashSet<>();
+        private final Set<String> superiorClasses = new LinkedHashSet<>();
+        private ObjectClassType type;
+
+        Builder(final ObjectClass oc, final SchemaBuilder builder) {
+            super(builder, oc);
+            this.oid = oc.oid;
+            this.names.addAll(oc.names);
+            this.isObsolete = oc.isObsolete;
+            this.type = oc.objectClassType;
+            this.superiorClasses.addAll(oc.superiorClassOIDs);
+            this.requiredAttributes.addAll(oc.requiredAttributeOIDs);
+            this.optionalAttributes.addAll(optionalAttributes);
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            this.oid = oid;
+        }
+
+        /**
+         * Adds this object class to the schema, throwing a
+         * {@code ConflictingSchemaElementException} if there is an existing
+         * object class with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing object class with the same numeric
+         *             OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return getSchemaBuilder().addObjectClass(new ObjectClass(this), false);
+        }
+
+        /**
+         * Adds this object class to the schema overwriting any existing object class
+         * with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return getSchemaBuilder().addObjectClass(new ObjectClass(this), true);
+        }
+
+        /**
+         * Adds this object class to the schema, overwriting any existing object class
+         * with the same numeric OID if the overwrite parameter is set to {@code true}.
+         *
+         * @param overwrite
+         *            {@code true} if any object class with the same OID should be overwritten.
+         * @return The parent schema builder.
+         */
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            return overwrite ? addToSchemaOverwrite() : addToSchema();
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final Collection<String> names) {
+            this.names.addAll(names);
+            return this;
+        }
+
+        /**
+         * Adds the provided user friendly names.
+         *
+         * @param names
+         *            The user friendly names.
+         * @return This builder.
+         */
+        public Builder names(final String... names) {
+            return names(asList(names));
+        }
+
+        /**
+         * Specifies whether this schema element is obsolete.
+         *
+         * @param isObsolete
+         *            {@code true} if this schema element is obsolete
+         *            (default is {@code false}).
+         * @return This builder.
+         */
+        public Builder obsolete(final boolean isObsolete) {
+            this.isObsolete = isObsolete;
+            return this;
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this object class.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes.
+         *
+         * @param attributeNamesOrOIDs
+         *      The list of optional attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final Collection<String> attributeNamesOrOIDs) {
+            this.optionalAttributes.addAll(attributeNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided optional attributes.
+         *
+         * @param attributeNamesOrOIDs
+         *      The list of optional attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder optionalAttributes(final String... attributeNamesOrOIDs) {
+            this.optionalAttributes.addAll(asList(attributeNamesOrOIDs));
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        @Override
+        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Removes all user defined names.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllNames() {
+            this.names.clear();
+            return this;
+        }
+
+        /**
+         * Removes all optional attributes.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllOptionalAttributes() {
+            this.optionalAttributes.clear();
+            return this;
+        }
+
+        /**
+         * Removes all required attributes.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllRequiredAttributes() {
+            this.requiredAttributes.clear();
+            return this;
+        }
+
+        /**
+         * Removes all superior object class.
+         *
+         * @return This builder.
+         */
+        public Builder removeAllSuperiorObjectClass() {
+            this.superiorClasses.clear();
+            return this;
+        }
+
+        /**
+         * Removes the provided user defined name.
+         *
+         * @param name
+         *            The user defined name to be removed.
+         * @return This builder.
+         */
+        public Builder removeName(String name) {
+            this.names.remove(name);
+            return this;
+        }
+
+        /**
+         * Removes the provided optional attribute.
+         *
+         * @param attributeNameOrOID
+         *            The optional attribute name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeOptionalAttribute(String attributeNameOrOID) {
+            this.optionalAttributes.remove(attributeNameOrOID);
+            return this;
+        }
+
+        /**
+         * Removes the provided required attribute.
+         *
+         * @param attributeNameOrOID
+         *            The provided required attribute name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeRequiredAttribute(String attributeNameOrOID) {
+            this.requiredAttributes.remove(attributeNameOrOID);
+            return this;
+        }
+
+        /**
+         * Removes the provided superior object class.
+         *
+         * @param objectClassNameOrOID
+         *            The superior object class name or OID to be removed.
+         * @return This builder.
+         */
+        public Builder removeSuperiorObjectClass(String objectClassNameOrOID) {
+            this.superiorClasses.remove(objectClassNameOrOID);
+            return this;
+        }
+
+        /**
+         * Adds the provided required attributes.
+         *
+         * @param attributeNamesOrOIDs
+         *      The list of required attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final Collection<String> attributeNamesOrOIDs) {
+            this.requiredAttributes.addAll(attributeNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided required attributes.
+         *
+         * @param attributeNamesOrOIDs
+         *      The list of required attribute names or OIDs.
+         * @return This builder.
+         */
+        public Builder requiredAttributes(final String... attributeNamesOrOIDs) {
+            this.requiredAttributes.addAll(asList(attributeNamesOrOIDs));
+            return this;
+        }
+
+        /**
+         * Adds the provided superior object classes.
+         *
+         * @param objectClassNamesOrOIDs
+         *      The list of superior object classes names or OIDs.
+         * @return This builder.
+         */
+        public Builder superiorObjectClasses(final Collection<String> objectClassNamesOrOIDs) {
+            this.superiorClasses.addAll(objectClassNamesOrOIDs);
+            return this;
+        }
+
+        /**
+         * Adds the provided superior object classes.
+         *
+         * @param objectClassNamesOrOIDs
+         *      The list of superior object classes names or OIDs.
+         * @return This builder.
+         */
+        public Builder superiorObjectClasses(final String... objectClassNamesOrOIDs) {
+            this.superiorClasses.addAll(asList(objectClassNamesOrOIDs));
+            return this;
+        }
+
+        /**
+         * Sets the type of this object class.
+         *
+         * @param type
+         *      The object class type.
+         * @return This builder.
+         */
+        public Builder type(final ObjectClassType type) {
+            this.type = type;
+            return this;
+        }
+    }
+
+    /** The OID that may be used to reference this definition. */
+    private final String oid;
+
+    /** The set of user defined names for this definition. */
+    private final List<String> names;
+
+    /** Indicates whether this definition is declared "obsolete". */
+    private final boolean isObsolete;
+
+    /** The reference to the superior objectclasses. */
+    private final Set<String> superiorClassOIDs;
+
+    /** The objectclass type for this objectclass. */
+    private final ObjectClassType objectClassType;
+
+    /** The set of required attribute types for this objectclass. */
+    private final Set<String> requiredAttributeOIDs;
+
+    /** The set of optional attribute types for this objectclass. */
+    private final Set<String> optionalAttributeOIDs;
+
+    private Set<ObjectClass> superiorClasses = emptySet();
+    private Set<AttributeType> declaredRequiredAttributes = emptySet();
+    private Set<AttributeType> requiredAttributes = emptySet();
+    private Set<AttributeType> declaredOptionalAttributes = emptySet();
+    private Set<AttributeType> optionalAttributes = emptySet();
+
+    /** Indicates whether or not validation has been performed. */
+    private boolean needsValidating = true;
+
+    /** The indicates whether or not validation failed. */
+    private boolean isValid;
+
+    /**
+     * Construct a extensibleObject object class where the set of allowed
+     * attribute types of this object class is implicitly the set of all
+     * attribute types of userApplications usage.
+     *
+     * @param description
+     *            The description for this schema definition
+     * @param extraProperties
+     *            The map of "extra" properties for this schema definition
+     */
+    static ObjectClass newExtensibleObjectObjectClass(final String description,
+        final Map<String, List<String>> extraProperties, final SchemaBuilder builder) {
+        return new ObjectClass(new Builder(EXTENSIBLE_OBJECT_OBJECTCLASS_OID, builder)
+               .description(description)
+               .extraProperties(extraProperties)
+               .names(EXTENSIBLE_OBJECT_OBJECTCLASS_NAME)
+               .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+               .type(AUXILIARY));
+    }
+
+
+    private ObjectClass(final Builder builder) {
+        super(builder);
+
+        if (builder.oid == null || builder.oid.isEmpty()) {
+            throw new IllegalArgumentException("An OID must be specified.");
+        }
+
+        this.oid = builder.oid;
+        this.names = unmodifiableCopyOfList(builder.names);
+        this.isObsolete = builder.isObsolete;
+        this.superiorClassOIDs = unmodifiableCopyOfSet(builder.superiorClasses);
+        this.objectClassType = builder.type;
+        this.requiredAttributeOIDs = unmodifiableCopyOfSet(builder.requiredAttributes);
+        this.optionalAttributeOIDs = unmodifiableCopyOfSet(builder.optionalAttributes);
+    }
+
+    /**
+     * Returns {@code true} if the provided object is an object class having the
+     * same numeric OID as this object class.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is a object class having the
+     *         same numeric OID as this object class.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof ObjectClass) {
+            final ObjectClass other = (ObjectClass) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an unmodifiable set containing the optional attributes for this
+     * object class. Note that this set will not automatically include any
+     * optional attributes for superior object classes.
+     *
+     * @return An unmodifiable set containing the optional attributes for this
+     *         object class.
+     */
+    public Set<AttributeType> getDeclaredOptionalAttributes() {
+        return declaredOptionalAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the required attributes for this
+     * object class. Note that this set will not automatically include any
+     * required attributes for superior object classes.
+     *
+     * @return An unmodifiable set containing the required attributes for this
+     *         object class.
+     */
+    public Set<AttributeType> getDeclaredRequiredAttributes() {
+        return declaredRequiredAttributes;
+    }
+
+    /**
+     * Returns the name or OID for this schema definition. If it has one or more
+     * names, then the primary name will be returned. If it does not have any
+     * names, then the OID will be returned.
+     *
+     * @return The name or OID for this schema definition.
+     */
+    public String getNameOrOID() {
+        if (names.isEmpty()) {
+            return oid;
+        }
+        return names.get(0);
+    }
+
+    /**
+     * Returns an unmodifiable list containing the user-defined names that may
+     * be used to reference this schema definition.
+     *
+     * @return Returns an unmodifiable list containing the user-defined names
+     *         that may be used to reference this schema definition.
+     */
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Returns the objectclass type for this objectclass.
+     *
+     * @return The objectclass type for this objectclass.
+     */
+    public ObjectClassType getObjectClassType() {
+        return objectClassType != null ? objectClassType : STRUCTURAL;
+    }
+
+    /**
+     * Returns the OID for this schema definition.
+     *
+     * @return The OID for this schema definition.
+     */
+    public String getOID() {
+        return oid;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the optional attributes for this
+     * object class and any superior object classes that it might have.
+     *
+     * @return An unmodifiable set containing the optional attributes for this
+     *         object class and any superior object classes that it might have.
+     */
+    public Set<AttributeType> getOptionalAttributes() {
+        return optionalAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the required attributes for this
+     * object class and any superior object classes that it might have.
+     *
+     * @return An unmodifiable set containing the required attributes for this
+     *         object class and any superior object classes that it might have.
+     */
+    public Set<AttributeType> getRequiredAttributes() {
+        return requiredAttributes;
+    }
+
+    /**
+     * Returns an unmodifiable set containing the superior classes for this
+     * object class.
+     *
+     * @return An unmodifiable set containing the superior classes for this
+     *         object class.
+     */
+    public Set<ObjectClass> getSuperiorClasses() {
+        return superiorClasses;
+    }
+
+    /**
+     * Returns the hash code for this object class. It will be calculated as the
+     * hash code of the numeric OID.
+     *
+     * @return The hash code for this object class.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name.
+     *
+     * @param name
+     *            The name for which to make the determination.
+     * @return <code>true</code> if the specified name is assigned to this
+     *         schema definition, or <code>false</code> if not.
+     */
+    public boolean hasName(final String name) {
+        for (final String n : names) {
+            if (n.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition has the specified name or OID.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <code>true</code> if the provided value matches the OID or one of
+     *         the names assigned to this schema definition, or
+     *         <code>false</code> if not.
+     */
+    public boolean hasNameOrOID(final String value) {
+        return hasName(value) || getOID().equals(value);
+    }
+
+    /**
+     * Indicates whether this objectclass is a descendant of the provided class.
+     *
+     * @param objectClass
+     *            The objectClass for which to make the determination.
+     * @return <code>true</code> if this objectclass is a descendant of the
+     *         provided class, or <code>false</code> if not.
+     */
+    public boolean isDescendantOf(final ObjectClass objectClass) {
+        for (final ObjectClass sup : superiorClasses) {
+            if (sup.equals(objectClass) || sup.isDescendantOf(objectClass)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this schema definition is declared "obsolete".
+     *
+     * @return <code>true</code> if this schema definition is declared
+     *         "obsolete", or <code>false</code> if not.
+     */
+    public boolean isObsolete() {
+        return isObsolete;
+    }
+
+    /**
+     * Indicates whether the provided attribute type is included in the optional
+     * attribute list for this or any of its superior objectclasses.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is optional for
+     *         this objectclass or any of its superior classes, or
+     *         <code>false</code> if not.
+     */
+    public boolean isOptional(final AttributeType attributeType) {
+        return optionalAttributes.contains(attributeType);
+    }
+
+    /**
+     * Indicates whether the provided attribute type is included in the required
+     * attribute list for this or any of its superior objectclasses.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is required by
+     *         this objectclass or any of its superior classes, or
+     *         <code>false</code> if not.
+     */
+    public boolean isRequired(final AttributeType attributeType) {
+        return requiredAttributes.contains(attributeType);
+    }
+
+    /**
+     * Indicates whether the provided attribute type is in the list of required
+     * or optional attributes for this objectclass or any of its superior
+     * classes.
+     *
+     * @param attributeType
+     *            The attribute type for which to make the determination.
+     * @return <code>true</code> if the provided attribute type is required or
+     *         allowed for this objectclass or any of its superior classes, or
+     *         <code>false</code> if it is not.
+     */
+    public boolean isRequiredOrOptional(final AttributeType attributeType) {
+        return isRequired(attributeType) || isOptional(attributeType);
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+
+        if (!names.isEmpty()) {
+            final Iterator<String> iterator = names.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" NAME ( '");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append("' '");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append("' )");
+            } else {
+                buffer.append(" NAME '");
+                buffer.append(firstName);
+                buffer.append("'");
+            }
+        }
+
+        appendDescription(buffer);
+
+        if (isObsolete) {
+            buffer.append(" OBSOLETE");
+        }
+
+        if (!superiorClassOIDs.isEmpty()) {
+            final Iterator<String> iterator = superiorClassOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" SUP ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" SUP ");
+                buffer.append(firstName);
+            }
+        }
+
+        if (objectClassType != null) {
+            buffer.append(" ");
+            buffer.append(objectClassType);
+        }
+
+        if (!requiredAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = requiredAttributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MUST ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" MUST ");
+                buffer.append(firstName);
+            }
+        }
+
+        if (!optionalAttributeOIDs.isEmpty()) {
+            final Iterator<String> iterator = optionalAttributeOIDs.iterator();
+
+            final String firstName = iterator.next();
+            if (iterator.hasNext()) {
+                buffer.append(" MAY ( ");
+                buffer.append(firstName);
+
+                while (iterator.hasNext()) {
+                    buffer.append(" $ ");
+                    buffer.append(iterator.next());
+                }
+
+                buffer.append(" )");
+            } else {
+                buffer.append(" MAY ");
+                buffer.append(firstName);
+            }
+        }
+    }
+
+    boolean validate(final Schema schema, final List<ObjectClass> invalidSchemaElements,
+            final List<LocalizableMessage> warnings) {
+        // Avoid validating this schema element more than once.
+        // This may occur if multiple object classes specify the same superior.
+        if (!needsValidating) {
+            return isValid;
+        }
+
+        // Prevent re-validation.
+        needsValidating = false;
+
+        // Init a flag to check to inheritance from top (only needed for
+        // structural object classes) per RFC 4512
+        boolean derivesTop = getObjectClassType() != ObjectClassType.STRUCTURAL;
+
+        if (!superiorClassOIDs.isEmpty()) {
+            superiorClasses = new HashSet<>(superiorClassOIDs.size());
+            ObjectClass superiorClass;
+            for (final String superClassOid : superiorClassOIDs) {
+                try {
+                    superiorClass = schema.getObjectClass(superClassOid);
+                } catch (final UnknownSchemaElementException e) {
+                    final LocalizableMessage message =
+                            WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_SUPERIOR_CLASS1.get(
+                                    getNameOrOID(), superClassOid);
+                    failValidation(invalidSchemaElements, warnings, message);
+                    return false;
+                }
+
+                // Make sure that the inheritance configuration is acceptable.
+                final ObjectClassType superiorType = superiorClass.getObjectClassType();
+                final ObjectClassType type = getObjectClassType();
+                switch (type) {
+                case ABSTRACT:
+                    // Abstract classes may only inherit from other abstract classes.
+                    if (superiorType != ObjectClassType.ABSTRACT) {
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_TYPE1.get(
+                                        getNameOrOID(), type.toString(), superiorType
+                                                .toString(), superiorClass.getNameOrOID());
+                        failValidation(invalidSchemaElements, warnings, message);
+                        return false;
+                    }
+                    break;
+
+                case AUXILIARY:
+                    // Auxiliary classes may only inherit from abstract classes
+                    // or other auxiliary classes.
+                    if (superiorType != ObjectClassType.ABSTRACT
+                            && superiorType != ObjectClassType.AUXILIARY) {
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_TYPE1.get(
+                                        getNameOrOID(), type.toString(), superiorType
+                                                .toString(), superiorClass.getNameOrOID());
+                        failValidation(invalidSchemaElements, warnings, message);
+                        return false;
+                    }
+                    break;
+
+                case STRUCTURAL:
+                    // Structural classes may only inherit from abstract classes
+                    // or other structural classes.
+                    if (superiorType != ObjectClassType.ABSTRACT
+                            && superiorType != ObjectClassType.STRUCTURAL) {
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_TYPE1.get(
+                                        getNameOrOID(), type.toString(), superiorType
+                                                .toString(), superiorClass.getNameOrOID());
+                        failValidation(invalidSchemaElements, warnings, message);
+                        return false;
+                    }
+                    break;
+                }
+
+                // All existing structural object classes defined in this schema
+                // are implicitly guaranteed to inherit from top.
+                if (!derivesTop && superiorType == ObjectClassType.STRUCTURAL) {
+                    derivesTop = true;
+                }
+
+                // First ensure that the superior has been validated and fail if
+                // it is invalid.
+                if (!superiorClass.validate(schema, invalidSchemaElements, warnings)) {
+                    final LocalizableMessage message =
+                            WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_CLASS.get(getNameOrOID(),
+                                    superClassOid);
+                    failValidation(invalidSchemaElements, warnings, message);
+                    return false;
+                }
+
+                // Inherit all required attributes from superior class.
+                Iterator<AttributeType> i = superiorClass.getRequiredAttributes().iterator();
+                if (i.hasNext() && requiredAttributes == Collections.EMPTY_SET) {
+                    requiredAttributes = new HashSet<>();
+                }
+                while (i.hasNext()) {
+                    requiredAttributes.add(i.next());
+                }
+
+                // Inherit all optional attributes from superior class.
+                i = superiorClass.getRequiredAttributes().iterator();
+                if (i.hasNext() && requiredAttributes == Collections.EMPTY_SET) {
+                    requiredAttributes = new HashSet<>();
+                }
+                while (i.hasNext()) {
+                    requiredAttributes.add(i.next());
+                }
+
+                superiorClasses.add(superiorClass);
+            }
+        }
+
+        if (!derivesTop) {
+            derivesTop = isDescendantOf(schema.getObjectClass("2.5.6.0"));
+        }
+
+        // Structural classes must have the "top" objectclass somewhere
+        // in the superior chain.
+        if (!derivesTop) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_OBJECTCLASS_STRUCTURAL_SUPERIOR_NOT_TOP1.get(getNameOrOID());
+            failValidation(invalidSchemaElements, warnings, message);
+            return false;
+        }
+
+        if (oid.equals(EXTENSIBLE_OBJECT_OBJECTCLASS_OID)) {
+            declaredOptionalAttributes = new HashSet<>(requiredAttributeOIDs.size());
+            for (final AttributeType attributeType : schema.getAttributeTypes()) {
+                if (attributeType.getUsage() == AttributeUsage.USER_APPLICATIONS) {
+                    declaredOptionalAttributes.add(attributeType);
+                }
+            }
+            optionalAttributes = declaredRequiredAttributes;
+        } else {
+            if (!requiredAttributeOIDs.isEmpty()) {
+                declaredRequiredAttributes = new HashSet<>(requiredAttributeOIDs.size());
+                AttributeType attributeType;
+                for (final String requiredAttribute : requiredAttributeOIDs) {
+                    try {
+                        attributeType = schema.getAttributeType(requiredAttribute);
+                    } catch (final UnknownSchemaElementException e) {
+                        // This isn't good because it means that the objectclass
+                        // requires an attribute type that we don't know anything about.
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_REQUIRED_ATTR1.get(
+                                        getNameOrOID(), requiredAttribute);
+                        failValidation(invalidSchemaElements, warnings, message);
+                        return false;
+                    }
+                    declaredRequiredAttributes.add(attributeType);
+                }
+                if (requiredAttributes == Collections.EMPTY_SET) {
+                    requiredAttributes = declaredRequiredAttributes;
+                } else {
+                    requiredAttributes.addAll(declaredRequiredAttributes);
+                }
+            }
+
+            if (!optionalAttributeOIDs.isEmpty()) {
+                declaredOptionalAttributes = new HashSet<>(optionalAttributeOIDs.size());
+                AttributeType attributeType;
+                for (final String optionalAttribute : optionalAttributeOIDs) {
+                    try {
+                        attributeType = schema.getAttributeType(optionalAttribute);
+                    } catch (final UnknownSchemaElementException e) {
+                        // This isn't good because it means that the objectclass
+                        // requires an attribute type that we don't know anything about.
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_OPTIONAL_ATTR1.get(
+                                        getNameOrOID(), optionalAttribute);
+                        failValidation(invalidSchemaElements, warnings, message);
+                        return false;
+                    }
+                    declaredOptionalAttributes.add(attributeType);
+                }
+                if (optionalAttributes == Collections.EMPTY_SET) {
+                    optionalAttributes = declaredOptionalAttributes;
+                } else {
+                    optionalAttributes.addAll(declaredOptionalAttributes);
+                }
+            }
+        }
+
+        declaredOptionalAttributes = Collections.unmodifiableSet(declaredOptionalAttributes);
+        declaredRequiredAttributes = Collections.unmodifiableSet(declaredRequiredAttributes);
+        optionalAttributes = Collections.unmodifiableSet(optionalAttributes);
+        requiredAttributes = Collections.unmodifiableSet(requiredAttributes);
+        superiorClasses = Collections.unmodifiableSet(superiorClasses);
+
+        return isValid = true;
+    }
+
+    private void failValidation(final List<ObjectClass> invalidSchemaElements,
+            final List<LocalizableMessage> warnings, final LocalizableMessage message) {
+        invalidSchemaElements.add(this);
+        warnings.add(ERR_OC_VALIDATION_FAIL.get(toString(), message));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassSyntaxImpl.java
new file mode 100644
index 0000000..ba34ea9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassSyntaxImpl.java
@@ -0,0 +1,149 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class implements the object class description syntax, which is used to
+ * hold objectclass definitions in the server schema. The format of this syntax
+ * is defined in RFC 2252.
+ */
+final class ObjectClassSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OID_FIRST_COMPONENT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_OBJECTCLASS_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll use the decodeObjectClass method to determine if the value
+        // is acceptable.
+        final String definition = value.toString();
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // Value was empty or contained only whitespace. This is illegal.
+                throwDecodeException(logger, ERR_ATTR_SYNTAX_OBJECTCLASS_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                throwDecodeException(logger,
+                    ERR_ATTR_SYNTAX_OBJECTCLASS_EXPECTED_OPEN_PARENTHESIS1.get(definition, reader.pos() - 1, c));
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+            readOID(reader, allowMalformedNamesAndOptions);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    readNameDescriptors(reader, allowMalformedNamesAndOptions);
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    readQuotedString(reader);
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("abstract".equalsIgnoreCase(tokenName)) {
+                    // This indicates that entries must not include this
+                    // objectclass unless they also include a non-abstract
+                    // objectclass that inherits from this class. We do not need
+                    // any more parsing for this token.
+                } else if ("structural".equalsIgnoreCase(tokenName)) {
+                    // This indicates that this is a structural objectclass.
+                    // We do not need any more parsing for this token.
+                } else if ("auxiliary".equalsIgnoreCase(tokenName)) {
+                    // This indicates that this is an auxiliary objectclass.
+                    // We do not need any more parsing for this token.
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    readOIDs(reader, allowMalformedNamesAndOptions);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    readExtensions(reader);
+                } else {
+                    throwDecodeException(logger, ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+            return true;
+        } catch (final DecodeException de) {
+            invalidReason.append(ERR_ATTR_SYNTAX_OBJECTCLASS_INVALID1.get(definition, de.getMessageObject()));
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassType.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassType.java
new file mode 100644
index 0000000..45410ea
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectClassType.java
@@ -0,0 +1,64 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+/**
+ * This enumeration defines the set of possible objectclass types that may be
+ * used, as defined in RFC 2252.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2252">RFC 2252 - Lightweight
+ *      Directory Access Protocol (v3): Attribute Syntax Definitions</a>
+ */
+public enum ObjectClassType {
+    /**
+     * The objectclass type that to use for classes declared "abstract".
+     */
+    ABSTRACT("ABSTRACT"),
+
+    /**
+     * The objectclass type that to use for classes declared "structural".
+     */
+    STRUCTURAL("STRUCTURAL"),
+
+    /**
+     * The objectclass type that to use for classes declared "auxiliary".
+     */
+    AUXILIARY("AUXILIARY");
+
+    /** The string representation of this objectclass type. */
+    private final String typeString;
+
+    /**
+     * Creates a new objectclass type with the provided string representation.
+     *
+     * @param typeString
+     *            The string representation for this objectclass type.
+     */
+    private ObjectClassType(final String typeString) {
+        this.typeString = typeString;
+    }
+
+    /**
+     * Retrieves a string representation of this objectclass type.
+     *
+     * @return A string representation of this objectclass type.
+     */
+    @Override
+    public String toString() {
+        return typeString;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..8d37d45
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java
@@ -0,0 +1,76 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.StaticUtils;
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class defines the objectIdentifierMatch matching rule defined in X.520
+ * and referenced in RFC 2252. This expects to work on OIDs and will match
+ * either an attribute/objectclass name or a numeric OID. NOTE: This matching
+ * rule requires a schema to lookup object identifiers in the descriptor form.
+ */
+final class ObjectIdentifierEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    ObjectIdentifierEqualityMatchingRuleImpl() {
+        super(EMR_OID_NAME);
+    }
+
+    static String resolveNames(final Schema schema, final String oidOrName) throws DecodeException {
+        if (StaticUtils.isDigit(oidOrName.charAt(0))) {
+            return oidOrName;
+        }
+        // Do a best effort attempt to normalize names to OIDs.
+        final String lowerCaseName = StaticUtils.toLowerCase(oidOrName);
+        try {
+            final String oid = schema.getOIDForName(lowerCaseName);
+            if (oid != null) {
+                return oid;
+            }
+        } catch (UnknownSchemaElementException e) {
+            throw DecodeException.error(e.getMessageObject(), e);
+        }
+        return lowerCaseName;
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        return normalizeAttributeValuePrivate(schema, value);
+    }
+
+    static ByteString normalizeAttributeValuePrivate(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        final String definition = value.toString();
+        final SubstringReader reader = new SubstringReader(definition);
+        final String oid = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+        return ByteString.valueOfUtf8(resolveNames(schema, oid));
+    }
+
+    @Override
+    String keyToHumanReadableString(ByteSequence key) {
+        return key.toByteString().toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..4c10cd1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java
@@ -0,0 +1,82 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static org.forgerock.opendj.ldap.schema.ObjectIdentifierEqualityMatchingRuleImpl.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * This class implements the objectIdentifierFirstComponentMatch matching rule
+ * defined in X.520 and referenced in RFC 2252. This rule is intended for use
+ * with attributes whose values contain a set of parentheses enclosing a
+ * space-delimited set of names and/or name-value pairs (like attribute type or
+ * objectclass descriptions) in which the "first component" is the first item
+ * after the opening parenthesis.
+ */
+final class ObjectIdentifierFirstComponentEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    ObjectIdentifierFirstComponentEqualityMatchingRuleImpl() {
+        super(EMR_OID_FIRST_COMPONENT_NAME);
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
+        return defaultAssertion(normalizeAttributeValuePrivate(schema, assertionValue));
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        final String definition = value.toString();
+        final SubstringReader reader = new SubstringReader(definition);
+
+        // We'll do this a character at a time. First, skip over any leading whitespace.
+        reader.skipWhitespaces();
+
+        if (reader.remaining() <= 0) {
+            // Value was empty or contained only whitespace. This is illegal.
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_EMPTY_VALUE.get();
+            throw DecodeException.error(message);
+        }
+
+        // The next character must be an open parenthesis.
+        // If it is not, then that is an error.
+        final char c = reader.read();
+        if (c != '(') {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, c));
+        }
+
+        // Skip over any spaces immediately following the opening parenthesis.
+        reader.skipWhitespaces();
+
+        // The next set of characters must be the OID.
+        final String oid = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+        return ByteString.valueOfUtf8(resolveNames(schema, oid));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..63b8943
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringEqualityMatchingRuleImpl.java
@@ -0,0 +1,39 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines the octetStringMatch matching rule defined in X.520. It
+ * will be used as the default equality matching rule for the binary and octet
+ * string syntaxes.
+ */
+final class OctetStringEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    OctetStringEqualityMatchingRuleImpl() {
+        super(EMR_OCTET_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return value.toByteString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..48fefc3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringOrderingMatchingRuleImpl.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines the octetStringOrderingMatch matching rule defined in
+ * X.520. This will be the default ordering matching rule for the binary and
+ * octet string syntaxes.
+ */
+final class OctetStringOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+
+    OctetStringOrderingMatchingRuleImpl() {
+        // Reusing equality index since OPENDJ-1864
+        super(EMR_OCTET_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return value.toByteString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..16798e6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSubstringMatchingRuleImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines the octetStringSubstringsMatch matching rule defined in
+ * X.520. It will be used as the default substring matching rule for the binary
+ * and octet string syntaxes.
+ */
+final class OctetStringSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    OctetStringSubstringMatchingRuleImpl() {
+        super(SMR_OCTET_STRING_NAME, EMR_OCTET_STRING_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return value.toByteString();
+    }
+
+    @Override
+    String keyToHumanReadableString(ByteSequence key) {
+        return key.toByteString().toHexString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSyntaxImpl.java
new file mode 100644
index 0000000..854b267
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OctetStringSyntaxImpl.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_OCTET_STRING_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the octet string attribute syntax, which is equivalent
+ * to the binary syntax and should be considered a replacement for it. Equality,
+ * ordering, and substring matching will be allowed by default.
+ */
+final class OctetStringSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_OCTET_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the octet string syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxImpl.java
new file mode 100644
index 0000000..7108177
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxImpl.java
@@ -0,0 +1,116 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_LIST_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_LIST_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_OTHER_MAILBOX_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the other mailbox attribute syntax, which consists of a
+ * printable string component (the mailbox type) followed by a dollar sign and
+ * an IA5 string component (the mailbox). Equality and substring matching will
+ * be allowed by default.
+ */
+final class OtherMailboxSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_LIST_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_OTHER_MAILBOX_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_LIST_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Check to see if the provided value was null. If so, then that's
+        // not acceptable.
+        if (value == null) {
+            invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE.get());
+            return false;
+        }
+
+        // Get the value as a string and determine its length. If it is
+        // empty, then that's not acceptable.
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+        if (valueLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE.get());
+            return false;
+        }
+
+        // Iterate through the characters in the vale until we find a dollar
+        // sign. Every character up to that point must be a printable string
+        // character.
+        int pos = 0;
+        for (; pos < valueLength; pos++) {
+            final char c = valueString.charAt(pos);
+            if (c == '$') {
+                if (pos == 0) {
+                    invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE.get(valueString));
+                    return false;
+                }
+
+                pos++;
+                break;
+            } else if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR.get(
+                        valueString, String.valueOf(c), pos));
+                return false;
+            }
+        }
+
+        // Make sure there is at least one character left for the mailbox.
+        if (pos >= valueLength) {
+            invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX.get(valueString));
+            return false;
+        }
+
+        // The remaining characters in the value must be IA5 (ASCII)
+        // characters.
+        for (; pos < valueLength; pos++) {
+            final char c = valueString.charAt(pos);
+            if (c != (c & 0x7F)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR.get(valueString,
+                        String.valueOf(c), pos));
+                return false;
+            }
+        }
+
+        // If we've gotten here, then the value is OK.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PostalAddressSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PostalAddressSyntaxImpl.java
new file mode 100644
index 0000000..6bb5bd6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PostalAddressSyntaxImpl.java
@@ -0,0 +1,62 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_POSTAL_ADDRESS_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the postal address attribute syntax, which is a list of
+ * UCS (Universal Character Set, as defined in the ISO 10646 specification and
+ * includes UTF-8 and UTF-16) strings separated by dollar signs. By default,
+ * they will be treated in a case-insensitive manner, and equality and substring
+ * matching will be allowed.
+ */
+final class PostalAddressSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_POSTAL_ADDRESS_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We'll allow any value.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..91b108b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleImpl.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the presentationAddressMatch matching rule defined in
+ * X.520 and referenced in RFC 2252. However, since this matching rule and the
+ * associated syntax have been deprecated, this matching rule behaves exactly
+ * like the caseIgnoreMatch rule.
+ */
+final class PresentationAddressEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    PresentationAddressEqualityMatchingRuleImpl() {
+        super(EMR_PRESENTATION_ADDRESS_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressSyntaxImpl.java
new file mode 100644
index 0000000..ffa1802
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PresentationAddressSyntaxImpl.java
@@ -0,0 +1,67 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the presentation address attribute syntax, which is
+ * defined in RFC 1278. However, because this LDAP syntax is being deprecated,
+ * this implementation behaves exactly like the directory string syntax.
+ */
+final class PresentationAddressSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_PRESENTATION_ADDRESS_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We will accept any value for this syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PrintableStringSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PrintableStringSyntaxImpl.java
new file mode 100644
index 0000000..f3092b8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/PrintableStringSyntaxImpl.java
@@ -0,0 +1,189 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_PRINTABLE_STRING_EMPTY_VALUE;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_PRINTABLE_STRING_ILLEGAL_CHARACTER;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the printable string attribute syntax, which is simply
+ * a string of characters from a limited ASCII character set (uppercase and
+ * lowercase letters, numeric digits, the space, and a set of various symbols).
+ * By default, they will be treated in a case-insensitive manner, and equality,
+ * ordering, substring, and approximate matching will be allowed.
+ */
+final class PrintableStringSyntaxImpl extends AbstractSyntaxImpl {
+
+    /**
+     * Indicates whether the provided character is a valid printable character.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided character is a printable
+     *         character, or <CODE>false</CODE> if not.
+     */
+    static boolean isPrintableCharacter(final char c) {
+        switch (c) {
+        case 'a':
+        case 'b':
+        case 'c':
+        case 'd':
+        case 'e':
+        case 'f':
+        case 'g':
+        case 'h':
+        case 'i':
+        case 'j':
+        case 'k':
+        case 'l':
+        case 'm':
+        case 'n':
+        case 'o':
+        case 'p':
+        case 'q':
+        case 'r':
+        case 's':
+        case 't':
+        case 'u':
+        case 'v':
+        case 'w':
+        case 'x':
+        case 'y':
+        case 'z':
+        case 'A':
+        case 'B':
+        case 'C':
+        case 'D':
+        case 'E':
+        case 'F':
+        case 'G':
+        case 'H':
+        case 'I':
+        case 'J':
+        case 'K':
+        case 'L':
+        case 'M':
+        case 'N':
+        case 'O':
+        case 'P':
+        case 'Q':
+        case 'R':
+        case 'S':
+        case 'T':
+        case 'U':
+        case 'V':
+        case 'W':
+        case 'X':
+        case 'Y':
+        case 'Z':
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case '\'':
+        case '(':
+        case ')':
+        case '+':
+        case ',':
+        case '-':
+        case '.':
+        case '=':
+        case '/':
+        case ':':
+        case '?':
+        case ' ':
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_PRINTABLE_STRING_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Check to see if the provided value was null. If so, then that's
+        // not acceptable.
+        if (value == null) {
+            invalidReason.append(WARN_ATTR_SYNTAX_PRINTABLE_STRING_EMPTY_VALUE.get());
+            return false;
+        }
+
+        // Get the value as a string and determine its length. If it is
+        // empty, then that's not acceptable.
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+        if (valueLength == 0) {
+            invalidReason.append(WARN_ATTR_SYNTAX_PRINTABLE_STRING_EMPTY_VALUE.get());
+            return false;
+        }
+
+        // Iterate through all the characters and see if they are
+        // acceptable.
+        for (int i = 0; i < valueLength; i++) {
+            final char c = valueString.charAt(i);
+            if (!isPrintableCharacter(c)) {
+                invalidReason.append(WARN_ATTR_SYNTAX_PRINTABLE_STRING_ILLEGAL_CHARACTER.get(
+                        valueString, String.valueOf(c), i));
+                return false;
+            }
+        }
+
+        // If we've gotten here, then the value is OK.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..f01de90
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleImpl.java
@@ -0,0 +1,41 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * This class implements the protocolInformationMatch matching rule defined in
+ * X.520 and referenced in RFC 2252. However, since this matching rule and the
+ * associated syntax have been deprecated, this matching rule behaves exactly
+ * like the caseIgnoreMatch rule.
+ */
+final class ProtocolInformationEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    ProtocolInformationEqualityMatchingRuleImpl() {
+        super(EMR_PROTOCOL_INFORMATION_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        return SchemaUtils.normalizeStringAttributeValue(value, TRIM, CASE_FOLD);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationSyntaxImpl.java
new file mode 100644
index 0000000..2297976
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ProtocolInformationSyntaxImpl.java
@@ -0,0 +1,68 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the protocol information attribute syntax, which is
+ * being deprecated. As such, this implementation behaves exactly like the
+ * directory string syntax.
+ */
+final class ProtocolInformationSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_PROTOCOL_INFORMATION_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We will accept any value for this syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/RegexSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/RegexSyntaxImpl.java
new file mode 100644
index 0000000..fdec7c7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/RegexSyntaxImpl.java
@@ -0,0 +1,91 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_VALUE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.AMR_DOUBLE_METAPHONE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+import org.forgerock.util.Reject;
+
+/**
+ * This class provides a regex mechanism where a new syntax and its
+ * corresponding matching rules can be created on-the-fly. A regex syntax is an
+ * LDAPSyntaxDescriptionSyntax with X-PATTERN extension.
+ */
+final class RegexSyntaxImpl extends AbstractSyntaxImpl {
+    /** The Pattern associated with the regex. */
+    private final Pattern pattern;
+
+    RegexSyntaxImpl(final Pattern pattern) {
+        Reject.ifNull(pattern);
+        this.pattern = pattern;
+    }
+
+    @Override
+    public String getApproximateMatchingRule() {
+        return AMR_DOUBLE_METAPHONE_OID;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return "Regex(" + pattern + ")";
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        final String strValue = value.toString();
+        final boolean matches = pattern.matcher(strValue).matches();
+        if (!matches) {
+            final LocalizableMessage message =
+                    WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_VALUE
+                            .get(strValue, pattern.pattern());
+            invalidReason.append(message);
+        }
+        return matches;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
new file mode 100644
index 0000000..e9db683
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
@@ -0,0 +1,2211 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.AVA;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.util.Function;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+import static org.forgerock.opendj.ldap.AttributeDescription.*;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+/**
+ * This class defines a data structure that holds information about the
+ * components of the LDAP schema. It includes the following kinds of elements:
+ * <UL>
+ * <LI>Attribute type definitions</LI>
+ * <LI>Object class definitions</LI>
+ * <LI>Attribute syntax definitions</LI>
+ * <LI>Matching rule definitions</LI>
+ * <LI>Matching rule use definitions</LI>
+ * <LI>DIT content rule definitions</LI>
+ * <LI>DIT structure rule definitions</LI>
+ * <LI>Name form definitions</LI>
+ * </UL>
+ */
+public final class Schema {
+    private static interface Impl {
+        Schema asNonStrictSchema();
+
+        Schema asStrictSchema();
+
+        Options getOptions();
+
+        MatchingRule getDefaultMatchingRule();
+
+        Syntax getDefaultSyntax();
+
+        String getOIDForName(String lowerCaseName);
+
+        AttributeType getAttributeType(Schema schema, String nameOrOid);
+
+        AttributeType getAttributeType(String nameOrOid, Syntax syntax);
+
+        Collection<AttributeType> getAttributeTypes();
+
+        List<AttributeType> getAttributeTypesWithName(String name);
+
+        DITContentRule getDITContentRule(ObjectClass structuralClass);
+
+        DITContentRule getDITContentRule(String nameOrOid);
+
+        Collection<DITContentRule> getDITContentRules();
+
+        Collection<DITContentRule> getDITContentRulesWithName(String name);
+
+        DITStructureRule getDITStructureRule(int ruleID);
+
+        Collection<DITStructureRule> getDITStructureRules(NameForm nameForm);
+
+        Collection<DITStructureRule> getDITStructureRulesWithName(String name);
+
+        Collection<DITStructureRule> getDITStuctureRules();
+
+        MatchingRule getMatchingRule(String nameOrOid);
+
+        Collection<MatchingRule> getMatchingRules();
+
+        Collection<MatchingRule> getMatchingRulesWithName(String name);
+
+        MatchingRuleUse getMatchingRuleUse(MatchingRule matchingRule);
+
+        MatchingRuleUse getMatchingRuleUse(String nameOrOid);
+
+        Collection<MatchingRuleUse> getMatchingRuleUses();
+
+        Collection<MatchingRuleUse> getMatchingRuleUsesWithName(String name);
+
+        NameForm getNameForm(String nameOrOid);
+
+        Collection<NameForm> getNameForms();
+
+        Collection<NameForm> getNameForms(ObjectClass structuralClass);
+
+        Collection<NameForm> getNameFormsWithName(String name);
+
+        ObjectClass getObjectClass(String nameOrOid);
+
+        Collection<ObjectClass> getObjectClasses();
+
+        Collection<ObjectClass> getObjectClassesWithName(String name);
+
+        String getSchemaName();
+
+        Syntax getSyntax(Schema schema, String numericOID);
+
+        Collection<Syntax> getSyntaxes();
+
+        Collection<LocalizableMessage> getWarnings();
+
+        boolean hasAttributeType(String nameOrOid);
+
+        boolean hasDITContentRule(String nameOrOid);
+
+        boolean hasDITStructureRule(int ruleID);
+
+        boolean hasMatchingRule(String nameOrOid);
+
+        boolean hasMatchingRuleUse(String nameOrOid);
+
+        boolean hasNameForm(String nameOrOid);
+
+        boolean hasObjectClass(String nameOrOid);
+
+        boolean hasSyntax(String numericOID);
+
+        boolean isStrict();
+    }
+
+    private static final class NonStrictImpl implements Impl {
+        private final StrictImpl strictImpl;
+
+        private NonStrictImpl(final StrictImpl strictImpl) {
+            this.strictImpl = strictImpl;
+        }
+
+        @Override
+        public Schema asNonStrictSchema() {
+            return strictImpl.asNonStrictSchema();
+        }
+
+        @Override
+        public Schema asStrictSchema() {
+            return strictImpl.asStrictSchema();
+        }
+
+        @Override
+        public Options getOptions() {
+            return strictImpl.getOptions();
+        }
+
+        @Override
+        public Syntax getDefaultSyntax() {
+            return strictImpl.getDefaultSyntax();
+        }
+
+        @Override
+        public MatchingRule getDefaultMatchingRule() {
+            return strictImpl.getDefaultMatchingRule();
+        }
+
+        @Override
+        public String getOIDForName(final String lowerCaseName) {
+            return strictImpl.getOIDForName(lowerCaseName);
+        }
+
+        @Override
+        public AttributeType getAttributeType(final Schema schema, final String nameOrOid) {
+            return getAttributeType0(nameOrOid, schema.getDefaultSyntax(), schema.getDefaultMatchingRule());
+        }
+
+        @Override
+        public AttributeType getAttributeType(final String nameOrOid, final Syntax syntax) {
+            return getAttributeType0(nameOrOid, syntax, syntax.getEqualityMatchingRule());
+        }
+
+        private AttributeType getAttributeType0(String nameOrOid, Syntax syntax, MatchingRule equalityMatchingRule) {
+            final AttributeType type = strictImpl.getAttributeType0(nameOrOid);
+            return type != null ? type : new AttributeType(nameOrOid, syntax, equalityMatchingRule);
+        }
+
+        @Override
+        public Collection<AttributeType> getAttributeTypes() {
+            return strictImpl.getAttributeTypes();
+        }
+
+        @Override
+        public List<AttributeType> getAttributeTypesWithName(final String name) {
+            return strictImpl.getAttributeTypesWithName(name);
+        }
+
+        @Override
+        public DITContentRule getDITContentRule(final ObjectClass structuralClass) {
+            return strictImpl.getDITContentRule(structuralClass);
+        }
+
+        @Override
+        public DITContentRule getDITContentRule(final String nameOrOid) {
+            return strictImpl.getDITContentRule(nameOrOid);
+        }
+
+        @Override
+        public Collection<DITContentRule> getDITContentRules() {
+            return strictImpl.getDITContentRules();
+        }
+
+        @Override
+        public Collection<DITContentRule> getDITContentRulesWithName(final String name) {
+            return strictImpl.getDITContentRulesWithName(name);
+        }
+
+        @Override
+        public DITStructureRule getDITStructureRule(final int ruleID) {
+            return strictImpl.getDITStructureRule(ruleID);
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStructureRules(final NameForm nameForm) {
+            return strictImpl.getDITStructureRules(nameForm);
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStructureRulesWithName(final String name) {
+            return strictImpl.getDITStructureRulesWithName(name);
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStuctureRules() {
+            return strictImpl.getDITStuctureRules();
+        }
+
+        @Override
+        public MatchingRule getMatchingRule(final String nameOrOid) {
+            return strictImpl.getMatchingRule(nameOrOid);
+        }
+
+        @Override
+        public Collection<MatchingRule> getMatchingRules() {
+            return strictImpl.getMatchingRules();
+        }
+
+        @Override
+        public Collection<MatchingRule> getMatchingRulesWithName(final String name) {
+            return strictImpl.getMatchingRulesWithName(name);
+        }
+
+        @Override
+        public MatchingRuleUse getMatchingRuleUse(final MatchingRule matchingRule) {
+            return strictImpl.getMatchingRuleUse(matchingRule);
+        }
+
+        @Override
+        public MatchingRuleUse getMatchingRuleUse(final String nameOrOid) {
+            return strictImpl.getMatchingRuleUse(nameOrOid);
+        }
+
+        @Override
+        public Collection<MatchingRuleUse> getMatchingRuleUses() {
+            return strictImpl.getMatchingRuleUses();
+        }
+
+        @Override
+        public Collection<MatchingRuleUse> getMatchingRuleUsesWithName(final String name) {
+            return strictImpl.getMatchingRuleUsesWithName(name);
+        }
+
+        @Override
+        public NameForm getNameForm(final String nameOrOid) {
+            return strictImpl.getNameForm(nameOrOid);
+        }
+
+        @Override
+        public Collection<NameForm> getNameForms() {
+            return strictImpl.getNameForms();
+        }
+
+        @Override
+        public Collection<NameForm> getNameForms(final ObjectClass structuralClass) {
+            return strictImpl.getNameForms(structuralClass);
+        }
+
+        @Override
+        public Collection<NameForm> getNameFormsWithName(final String name) {
+            return strictImpl.getNameFormsWithName(name);
+        }
+
+        @Override
+        public ObjectClass getObjectClass(final String nameOrOid) {
+            return strictImpl.getObjectClass(nameOrOid);
+        }
+
+        @Override
+        public Collection<ObjectClass> getObjectClasses() {
+            return strictImpl.getObjectClasses();
+        }
+
+        @Override
+        public Collection<ObjectClass> getObjectClassesWithName(final String name) {
+            return strictImpl.getObjectClassesWithName(name);
+        }
+
+        @Override
+        public String getSchemaName() {
+            return strictImpl.getSchemaName();
+        }
+
+        @Override
+        public Syntax getSyntax(final Schema schema, final String numericOID) {
+            if (!strictImpl.hasSyntax(numericOID)) {
+                return new Syntax(schema, numericOID);
+            }
+            return strictImpl.getSyntax(schema, numericOID);
+        }
+
+        @Override
+        public Collection<Syntax> getSyntaxes() {
+            return strictImpl.getSyntaxes();
+        }
+
+        @Override
+        public Collection<LocalizableMessage> getWarnings() {
+            return strictImpl.getWarnings();
+        }
+
+        @Override
+        public boolean hasAttributeType(final String nameOrOid) {
+            // In theory a non-strict schema always contains the requested
+            // attribute type, so we could always return true. However, we
+            // should provide a way for callers to differentiate between a
+            // real attribute type and a faked up attribute type.
+            return strictImpl.hasAttributeType(nameOrOid);
+        }
+
+        @Override
+        public boolean hasDITContentRule(final String nameOrOid) {
+            return strictImpl.hasDITContentRule(nameOrOid);
+        }
+
+        @Override
+        public boolean hasDITStructureRule(final int ruleID) {
+            return strictImpl.hasDITStructureRule(ruleID);
+        }
+
+        @Override
+        public boolean hasMatchingRule(final String nameOrOid) {
+            return strictImpl.hasMatchingRule(nameOrOid);
+        }
+
+        @Override
+        public boolean hasMatchingRuleUse(final String nameOrOid) {
+            return strictImpl.hasMatchingRuleUse(nameOrOid);
+        }
+
+        @Override
+        public boolean hasNameForm(final String nameOrOid) {
+            return strictImpl.hasNameForm(nameOrOid);
+        }
+
+        @Override
+        public boolean hasObjectClass(final String nameOrOid) {
+            return strictImpl.hasObjectClass(nameOrOid);
+        }
+
+        @Override
+        public boolean hasSyntax(final String numericOID) {
+            return strictImpl.hasSyntax(numericOID);
+        }
+
+        @Override
+        public boolean isStrict() {
+            return false;
+        }
+    }
+
+    static final class StrictImpl implements Impl {
+        private final Map<Integer, DITStructureRule> id2StructureRules;
+        private final Map<String, List<AttributeType>> name2AttributeTypes;
+        private final Map<String, List<DITContentRule>> name2ContentRules;
+        private final Map<String, List<MatchingRule>> name2MatchingRules;
+        private final Map<String, List<MatchingRuleUse>> name2MatchingRuleUses;
+        private final Map<String, List<NameForm>> name2NameForms;
+        private final Map<String, List<ObjectClass>> name2ObjectClasses;
+        private final Map<String, List<DITStructureRule>> name2StructureRules;
+        private final Map<String, List<DITStructureRule>> nameForm2StructureRules;
+        private final Map<String, AttributeType> numericOID2AttributeTypes;
+        private final Map<String, DITContentRule> numericOID2ContentRules;
+        private final Map<String, MatchingRule> numericOID2MatchingRules;
+        private final Map<String, MatchingRuleUse> numericOID2MatchingRuleUses;
+        private final Map<String, NameForm> numericOID2NameForms;
+        private final Map<String, ObjectClass> numericOID2ObjectClasses;
+        private final Map<String, Syntax> numericOID2Syntaxes;
+        private final Map<String, List<NameForm>> objectClass2NameForms;
+        private final Map<String, String> name2OIDs;
+        private final List<LocalizableMessage> warnings;
+        private final String schemaName;
+        private final Options options;
+        private final Syntax defaultSyntax;
+        private final MatchingRule defaultMatchingRule;
+        private final Schema strictSchema;
+        private final Schema nonStrictSchema;
+
+        StrictImpl(final String schemaName,
+                final Options options,
+                final Syntax defaultSyntax,
+                final MatchingRule defaultMatchingRule,
+                final Map<String, Syntax> numericOID2Syntaxes,
+                final Map<String, MatchingRule> numericOID2MatchingRules,
+                final Map<String, MatchingRuleUse> numericOID2MatchingRuleUses,
+                final Map<String, AttributeType> numericOID2AttributeTypes,
+                final Map<String, ObjectClass> numericOID2ObjectClasses,
+                final Map<String, NameForm> numericOID2NameForms,
+                final Map<String, DITContentRule> numericOID2ContentRules,
+                final Map<Integer, DITStructureRule> id2StructureRules,
+                final Map<String, List<MatchingRule>> name2MatchingRules,
+                final Map<String, List<MatchingRuleUse>> name2MatchingRuleUses,
+                final Map<String, List<AttributeType>> name2AttributeTypes,
+                final Map<String, List<ObjectClass>> name2ObjectClasses,
+                final Map<String, List<NameForm>> name2NameForms,
+                final Map<String, List<DITContentRule>> name2ContentRules,
+                final Map<String, List<DITStructureRule>> name2StructureRules,
+                final Map<String, List<NameForm>> objectClass2NameForms,
+                final Map<String, List<DITStructureRule>> nameForm2StructureRules,
+                final Map<String, String> name2OIDs,
+                final List<LocalizableMessage> warnings) {
+            this.schemaName = schemaName;
+            this.options = options;
+            this.defaultSyntax = defaultSyntax;
+            this.defaultMatchingRule = defaultMatchingRule;
+            this.numericOID2Syntaxes = Collections.unmodifiableMap(numericOID2Syntaxes);
+            this.numericOID2MatchingRules = Collections.unmodifiableMap(numericOID2MatchingRules);
+            this.numericOID2MatchingRuleUses = Collections.unmodifiableMap(numericOID2MatchingRuleUses);
+            this.numericOID2AttributeTypes = Collections.unmodifiableMap(numericOID2AttributeTypes);
+            this.numericOID2ObjectClasses = Collections.unmodifiableMap(numericOID2ObjectClasses);
+            this.numericOID2NameForms = Collections.unmodifiableMap(numericOID2NameForms);
+            this.numericOID2ContentRules = Collections.unmodifiableMap(numericOID2ContentRules);
+            this.id2StructureRules = Collections.unmodifiableMap(id2StructureRules);
+            this.name2MatchingRules = Collections.unmodifiableMap(name2MatchingRules);
+            this.name2MatchingRuleUses = Collections.unmodifiableMap(name2MatchingRuleUses);
+            this.name2AttributeTypes = Collections.unmodifiableMap(name2AttributeTypes);
+            this.name2ObjectClasses = Collections.unmodifiableMap(name2ObjectClasses);
+            this.name2NameForms = Collections.unmodifiableMap(name2NameForms);
+            this.name2ContentRules = Collections.unmodifiableMap(name2ContentRules);
+            this.name2StructureRules = Collections.unmodifiableMap(name2StructureRules);
+            this.objectClass2NameForms = Collections.unmodifiableMap(objectClass2NameForms);
+            this.nameForm2StructureRules = Collections.unmodifiableMap(nameForm2StructureRules);
+            this.name2OIDs = Collections.unmodifiableMap(name2OIDs);
+            this.warnings = Collections.unmodifiableList(warnings);
+            this.strictSchema = new Schema(this);
+            this.nonStrictSchema = new Schema(new NonStrictImpl(this));
+        }
+
+        @Override
+        public Schema asNonStrictSchema() {
+            return nonStrictSchema;
+        }
+
+        @Override
+        public Schema asStrictSchema() {
+            return strictSchema;
+        }
+
+        @Override
+        public Options getOptions() {
+            return options;
+        }
+
+        @Override
+        public Syntax getDefaultSyntax() {
+            return defaultSyntax;
+        }
+
+        @Override
+        public MatchingRule getDefaultMatchingRule() {
+            return defaultMatchingRule;
+        }
+
+        @Override
+        public String getOIDForName(String lowerCaseName) {
+            final String oid = name2OIDs.get(lowerCaseName);
+            if (SchemaBuilder.AMBIGUOUS_OID.equals(oid)) {
+                throw new UnknownSchemaElementException(WARN_NAME_AMBIGUOUS.get(lowerCaseName));
+            }
+            return oid;
+        }
+
+        @Override
+        public AttributeType getAttributeType(String nameOrOid, Syntax syntax) {
+            return getAttributeType(null, nameOrOid);
+        }
+
+        @Override
+        public AttributeType getAttributeType(final Schema schema, final String nameOrOid) {
+            final AttributeType type = getAttributeType0(nameOrOid);
+            if (type != null) {
+                return type;
+            }
+            throw new UnknownSchemaElementException(WARN_ATTR_TYPE_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<AttributeType> getAttributeTypes() {
+            return numericOID2AttributeTypes.values();
+        }
+
+        @Override
+        public List<AttributeType> getAttributeTypesWithName(final String name) {
+            final List<AttributeType> attributes =
+                    name2AttributeTypes.get(StaticUtils.toLowerCase(name));
+            if (attributes != null) {
+                return attributes;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public DITContentRule getDITContentRule(final ObjectClass structuralClass) {
+            return numericOID2ContentRules.get(structuralClass.getOID());
+        }
+
+        @Override
+        public DITContentRule getDITContentRule(final String nameOrOid) {
+            final DITContentRule rule = numericOID2ContentRules.get(nameOrOid);
+            if (rule != null) {
+                return rule;
+            }
+            final List<DITContentRule> rules = name2ContentRules.get(StaticUtils.toLowerCase(nameOrOid));
+            if (rules != null) {
+                if (rules.size() == 1) {
+                    return rules.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_DCR_AMBIGUOUS.get(nameOrOid));
+            }
+            throw new UnknownSchemaElementException(WARN_DCR_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<DITContentRule> getDITContentRules() {
+            return numericOID2ContentRules.values();
+        }
+
+        @Override
+        public Collection<DITContentRule> getDITContentRulesWithName(final String name) {
+            final List<DITContentRule> rules = name2ContentRules.get(StaticUtils.toLowerCase(name));
+            if (rules != null) {
+                return rules;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public DITStructureRule getDITStructureRule(final int ruleID) {
+            final DITStructureRule rule = id2StructureRules.get(ruleID);
+            if (rule == null) {
+                throw new UnknownSchemaElementException(WARN_DSR_UNKNOWN
+                        .get(String.valueOf(ruleID)));
+            }
+            return rule;
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStructureRules(final NameForm nameForm) {
+            final List<DITStructureRule> rules = nameForm2StructureRules.get(nameForm.getOID());
+            if (rules != null) {
+                return rules;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStructureRulesWithName(final String name) {
+            final List<DITStructureRule> rules =
+                    name2StructureRules.get(StaticUtils.toLowerCase(name));
+            if (rules != null) {
+                return rules;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public Collection<DITStructureRule> getDITStuctureRules() {
+            return id2StructureRules.values();
+        }
+
+        @Override
+        public MatchingRule getMatchingRule(final String nameOrOid) {
+            final MatchingRule rule = numericOID2MatchingRules.get(nameOrOid);
+            if (rule != null) {
+                return rule;
+            }
+            final List<MatchingRule> rules = name2MatchingRules.get(StaticUtils.toLowerCase(nameOrOid));
+            if (rules != null) {
+                if (rules.size() == 1) {
+                    return rules.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_MR_AMBIGUOUS.get(nameOrOid));
+            }
+            throw new UnknownSchemaElementException(WARN_MR_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<MatchingRule> getMatchingRules() {
+            return numericOID2MatchingRules.values();
+        }
+
+        @Override
+        public Collection<MatchingRule> getMatchingRulesWithName(final String name) {
+            final List<MatchingRule> rules = name2MatchingRules.get(StaticUtils.toLowerCase(name));
+            if (rules != null) {
+                return rules;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public MatchingRuleUse getMatchingRuleUse(final MatchingRule matchingRule) {
+            return numericOID2MatchingRuleUses.get(matchingRule.getOID());
+        }
+
+        @Override
+        public MatchingRuleUse getMatchingRuleUse(final String nameOrOid) {
+            final MatchingRuleUse rule = numericOID2MatchingRuleUses.get(nameOrOid);
+            if (rule != null) {
+                return rule;
+            }
+            final List<MatchingRuleUse> uses =
+                    name2MatchingRuleUses.get(StaticUtils.toLowerCase(nameOrOid));
+            if (uses != null) {
+                if (uses.size() == 1) {
+                    return uses.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_MRU_AMBIGUOUS.get(nameOrOid));
+            }
+            throw new UnknownSchemaElementException(WARN_MRU_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<MatchingRuleUse> getMatchingRuleUses() {
+            return numericOID2MatchingRuleUses.values();
+        }
+
+        @Override
+        public Collection<MatchingRuleUse> getMatchingRuleUsesWithName(final String name) {
+            final List<MatchingRuleUse> rules =
+                    name2MatchingRuleUses.get(StaticUtils.toLowerCase(name));
+            if (rules != null) {
+                return rules;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public NameForm getNameForm(final String nameOrOid) {
+            final NameForm form = numericOID2NameForms.get(nameOrOid);
+            if (form != null) {
+                return form;
+            }
+            final List<NameForm> forms = name2NameForms.get(StaticUtils.toLowerCase(nameOrOid));
+            if (forms != null) {
+                if (forms.size() == 1) {
+                    return forms.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_NAMEFORM_AMBIGUOUS.get(nameOrOid));
+            }
+            throw new UnknownSchemaElementException(WARN_NAMEFORM_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<NameForm> getNameForms() {
+            return numericOID2NameForms.values();
+        }
+
+        @Override
+        public Collection<NameForm> getNameForms(final ObjectClass structuralClass) {
+            final List<NameForm> forms = objectClass2NameForms.get(structuralClass.getOID());
+            if (forms != null) {
+                return forms;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public Collection<NameForm> getNameFormsWithName(final String name) {
+            final List<NameForm> forms = name2NameForms.get(StaticUtils.toLowerCase(name));
+            if (forms != null) {
+                return forms;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public ObjectClass getObjectClass(final String nameOrOid) {
+            final ObjectClass oc = numericOID2ObjectClasses.get(nameOrOid);
+            if (oc != null) {
+                return oc;
+            }
+            final List<ObjectClass> classes = name2ObjectClasses.get(StaticUtils.toLowerCase(nameOrOid));
+            if (classes != null) {
+                if (classes.size() == 1) {
+                    return classes.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_OBJECTCLASS_AMBIGUOUS.get(nameOrOid));
+            }
+            throw new UnknownSchemaElementException(WARN_OBJECTCLASS_UNKNOWN.get(nameOrOid));
+        }
+
+        @Override
+        public Collection<ObjectClass> getObjectClasses() {
+            return numericOID2ObjectClasses.values();
+        }
+
+        @Override
+        public Collection<ObjectClass> getObjectClassesWithName(final String name) {
+            final List<ObjectClass> classes = name2ObjectClasses.get(StaticUtils.toLowerCase(name));
+            if (classes != null) {
+                return classes;
+            }
+            return Collections.emptyList();
+        }
+
+        @Override
+        public String getSchemaName() {
+            return schemaName;
+        }
+
+        @Override
+        public Syntax getSyntax(final Schema schema, final String numericOID) {
+            final Syntax syntax = numericOID2Syntaxes.get(numericOID);
+            if (syntax == null) {
+                throw new UnknownSchemaElementException(WARN_SYNTAX_UNKNOWN.get(numericOID));
+            }
+            return syntax;
+        }
+
+        @Override
+        public Collection<Syntax> getSyntaxes() {
+            return numericOID2Syntaxes.values();
+        }
+
+        @Override
+        public Collection<LocalizableMessage> getWarnings() {
+            return warnings;
+        }
+
+        @Override
+        public boolean hasAttributeType(final String nameOrOid) {
+            if (numericOID2AttributeTypes.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<AttributeType> attributes =
+                    name2AttributeTypes.get(StaticUtils.toLowerCase(nameOrOid));
+            return attributes != null && attributes.size() == 1;
+        }
+
+        @Override
+        public boolean hasDITContentRule(final String nameOrOid) {
+            if (numericOID2ContentRules.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<DITContentRule> rules = name2ContentRules.get(StaticUtils.toLowerCase(nameOrOid));
+            return rules != null && rules.size() == 1;
+        }
+
+        @Override
+        public boolean hasDITStructureRule(final int ruleID) {
+            return id2StructureRules.containsKey(ruleID);
+        }
+
+        @Override
+        public boolean hasMatchingRule(final String nameOrOid) {
+            if (numericOID2MatchingRules.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<MatchingRule> rules = name2MatchingRules.get(StaticUtils.toLowerCase(nameOrOid));
+            return rules != null && rules.size() == 1;
+        }
+
+        @Override
+        public boolean hasMatchingRuleUse(final String nameOrOid) {
+            if (numericOID2MatchingRuleUses.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<MatchingRuleUse> uses =
+                    name2MatchingRuleUses.get(StaticUtils.toLowerCase(nameOrOid));
+            return uses != null && uses.size() == 1;
+        }
+
+        @Override
+        public boolean hasNameForm(final String nameOrOid) {
+            if (numericOID2NameForms.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<NameForm> forms = name2NameForms.get(StaticUtils.toLowerCase(nameOrOid));
+            return forms != null && forms.size() == 1;
+        }
+
+        @Override
+        public boolean hasObjectClass(final String nameOrOid) {
+            if (numericOID2ObjectClasses.containsKey(nameOrOid)) {
+                return true;
+            }
+            final List<ObjectClass> classes = name2ObjectClasses.get(StaticUtils.toLowerCase(nameOrOid));
+            return classes != null && classes.size() == 1;
+        }
+
+        @Override
+        public boolean hasSyntax(final String numericOID) {
+            return numericOID2Syntaxes.containsKey(numericOID);
+        }
+
+        @Override
+        public boolean isStrict() {
+            return true;
+        }
+
+        AttributeType getAttributeType0(final String nameOrOid) {
+            final AttributeType type = numericOID2AttributeTypes.get(nameOrOid);
+            if (type != null) {
+                return type;
+            }
+            final List<AttributeType> attributes =
+                    name2AttributeTypes.get(StaticUtils.toLowerCase(nameOrOid));
+            if (attributes != null) {
+                if (attributes.size() == 1) {
+                    return attributes.get(0);
+                }
+                throw new UnknownSchemaElementException(WARN_ATTR_TYPE_AMBIGUOUS.get(nameOrOid));
+            }
+            return null;
+        }
+    }
+
+    static final String ATTR_ATTRIBUTE_TYPES = "attributeTypes";
+    static final String ATTR_DIT_CONTENT_RULES = "dITContentRules";
+    static final String ATTR_DIT_STRUCTURE_RULES = "dITStructureRules";
+    static final String ATTR_LDAP_SYNTAXES = "ldapSyntaxes";
+    static final String ATTR_MATCHING_RULE_USE = "matchingRuleUse";
+    static final String ATTR_MATCHING_RULES = "matchingRules";
+    static final String ATTR_NAME_FORMS = "nameForms";
+    static final String ATTR_OBJECT_CLASSES = "objectClasses";
+
+    /**
+     * Returns the core schema. The core schema is non-strict and contains the
+     * following standard LDAP schema elements:
+     * <ul>
+     * <li><a href="http://tools.ietf.org/html/rfc4512">RFC 4512 - Lightweight
+     * Directory Access Protocol (LDAP): Directory Information Models </a>
+     * <li><a href="http://tools.ietf.org/html/rfc4517">RFC 4517 - Lightweight
+     * Directory Access Protocol (LDAP): Syntaxes and Matching Rules </a>
+     * <li><a href="http://tools.ietf.org/html/rfc4519">RFC 4519 - Lightweight
+     * Directory Access Protocol (LDAP): Schema for User Applications </a>
+     * <li><a href="http://tools.ietf.org/html/rfc4530">RFC 4530 - Lightweight
+     * Directory Access Protocol (LDAP): entryUUID Operational Attribute </a>
+     * <li><a href="http://tools.ietf.org/html/rfc3045">RFC 3045 - Storing
+     * Vendor Information in the LDAP root DSE </a>
+     * <li><a href="http://tools.ietf.org/html/rfc3112">RFC 3112 - LDAP
+     * Authentication Password Schema </a>
+     * </ul>
+     *
+     * @return The core schema.
+     */
+    public static Schema getCoreSchema() {
+        return CoreSchemaImpl.getInstance();
+    }
+
+    /**
+     * Returns the default schema which should be used by this application. The
+     * default schema is initially set to the core schema.
+     *
+     * @return The default schema which should be used by this application.
+     */
+    public static Schema getDefaultSchema() {
+        return DelayedSchema.defaultSchema;
+    }
+
+    /**
+     * Returns the empty schema. The empty schema is non-strict and does not
+     * contain any schema elements.
+     *
+     * @return The empty schema.
+     */
+    public static Schema getEmptySchema() {
+        return DelayedSchema.EMPTY_SCHEMA;
+    }
+
+    /**
+     * Reads the schema contained in the named subschema sub-entry.
+     * <p>
+     * If the requested schema is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, this method will never return {@code null}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the subschema sub-entry.
+     * @return The schema from the Directory Server.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public static Schema readSchema(final Connection connection, final DN name) throws LdapException {
+        return new SchemaBuilder().addSchema(connection, name, true).toSchema();
+    }
+
+    /**
+     * Asynchronously reads the schema contained in the named subschema
+     * sub-entry.
+     * <p>
+     * If the requested schema is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}. More
+     * specifically, the returned promise will never return {@code null}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the subschema sub-entry.
+     *            the operation result when it is received, may be {@code null}.
+     * @return A promise representing the retrieved schema.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public static LdapPromise<Schema> readSchemaAsync(final Connection connection, final DN name) {
+        final SchemaBuilder builder = new SchemaBuilder();
+        return builder.addSchemaAsync(connection, name, true).then(
+                new Function<SchemaBuilder, Schema, LdapException>() {
+                    @Override
+                    public Schema apply(SchemaBuilder builder) throws LdapException {
+                        return builder.toSchema();
+                    }
+                });
+    }
+
+    /**
+     * Reads the schema contained in the subschema sub-entry which applies to
+     * the named entry.
+     * <p>
+     * If the requested entry or its associated schema are not returned by the
+     * Directory Server then the request will fail with an
+     * {@link EntryNotFoundException}. More specifically, this method will never
+     * return {@code null}.
+     * <p>
+     * This implementation first reads the {@code subschemaSubentry} attribute
+     * of the entry in order to identify the schema and then invokes
+     * {@link #readSchema(Connection, DN)} to read the schema.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the entry whose schema is to be
+     *            located.
+     * @return The schema from the Directory Server which applies to the named
+     *         entry.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public static Schema readSchemaForEntry(final Connection connection, final DN name)
+            throws LdapException {
+        return new SchemaBuilder().addSchemaForEntry(connection, name, true).toSchema();
+    }
+
+    /**
+     * Asynchronously reads the schema contained in the subschema sub-entry
+     * which applies to the named entry.
+     * <p>
+     * If the requested entry or its associated schema are not returned by the
+     * Directory Server then the request will fail with an
+     * {@link EntryNotFoundException}. More specifically, the returned promise
+     * will never return {@code null}.
+     * <p>
+     * This implementation first reads the {@code subschemaSubentry} attribute
+     * of the entry in order to identify the schema and then invokes
+     * {@link #readSchemaAsync(Connection, DN)} to read the schema.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the entry whose schema is to be
+     *            located.
+     * @return A promise representing the retrieved schema.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public static LdapPromise<Schema> readSchemaForEntryAsync(final Connection connection, final DN name) {
+        final SchemaBuilder builder = new SchemaBuilder();
+        return builder.addSchemaForEntryAsync(connection, name, true).then(
+            new Function<SchemaBuilder, Schema, LdapException>() {
+                @Override
+                public Schema apply(SchemaBuilder builder) throws LdapException {
+                    return builder.toSchema();
+                }
+            });
+    }
+
+    /**
+     * Sets the default schema which should be used by this application. The
+     * default schema is initially set to the core schema.
+     *
+     * @param schema
+     *            The default schema which should be used by this application.
+     */
+    public static void setDefaultSchema(final Schema schema) {
+        Reject.ifNull(schema);
+        DelayedSchema.defaultSchema = schema;
+    }
+
+    /**
+     * Parses the provided entry as a subschema subentry. Any problems
+     * encountered while parsing the entry can be retrieved using the returned
+     * schema's {@link #getWarnings()} method.
+     *
+     * @param entry
+     *            The subschema subentry to be parsed.
+     * @return The parsed schema.
+     */
+    public static Schema valueOf(final Entry entry) {
+        return new SchemaBuilder(entry).toSchema();
+    }
+
+    private final Impl impl;
+
+    Schema(final Impl impl) {
+        this.impl = impl;
+    }
+
+    /**
+     * Returns a non-strict view of this schema.
+     * <p>
+     * See the description of {@link #isStrict()} for more details.
+     *
+     * @return A non-strict view of this schema.
+     * @see Schema#isStrict()
+     */
+    public Schema asNonStrictSchema() {
+        return impl.asNonStrictSchema();
+    }
+
+    /**
+     * Returns a strict view of this schema.
+     * <p>
+     * See the description of {@link #isStrict()} for more details.
+     *
+     * @return A strict view of this schema.
+     * @see Schema#isStrict()
+     */
+    public Schema asStrictSchema() {
+        return impl.asStrictSchema();
+    }
+
+    MatchingRule getDefaultMatchingRule() {
+        return impl.getDefaultMatchingRule();
+    }
+
+    Syntax getDefaultSyntax() {
+        return impl.getDefaultSyntax();
+    }
+
+    /**
+     * Return the numerical OID matching the lowerCaseName.
+     * @param lowerCaseName The lower case name
+     * @return OID matching the name or null if name doesn't match to an OID
+     * @throws UnknownSchemaElementException if multiple OID are matching
+     * lowerCaseName
+     */
+    String getOIDForName(String lowerCaseName) {
+        return impl.getOIDForName(lowerCaseName);
+    }
+
+    /**
+     * Returns the attribute type for the specified name or numeric OID.
+     * <p>
+     * If the requested attribute type is not registered in this schema and this
+     * schema is non-strict then a temporary "place-holder" attribute type will
+     * be created and returned. Place holder attribute types have an OID which
+     * is the normalized attribute name with the string {@code -oid} appended.
+     * In addition, they will use the directory string syntax and case ignore
+     * matching rule.
+     *
+     * @param nameOrOid
+     *            The name or OID of the attribute type to retrieve.
+     * @return The requested attribute type.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested attribute type
+     *             was not found or if the provided name is ambiguous.
+     * @see AttributeType#isPlaceHolder()
+     */
+    public AttributeType getAttributeType(final String nameOrOid) {
+        return impl.getAttributeType(this, nameOrOid);
+    }
+
+    /**
+     * Returns the attribute type for the specified name or numeric OID.
+     * <p>
+     * If the requested attribute type is not registered in this schema and this
+     * schema is non-strict then a temporary "place-holder" attribute type will
+     * be created and returned. Place holder attribute types have an OID which
+     * is the normalized attribute name with the string {@code -oid} appended.
+     * In addition, they will use the provided syntax and the default matching
+     * rule associated with the syntax.
+     *
+     * @param nameOrOid
+     *            The name or OID of the attribute type to retrieve.
+     * @param syntax
+     *            The syntax to use when creating the temporary "place-holder" attribute type.
+     * @return The requested attribute type.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested attribute type
+     *             was not found or if the provided name is ambiguous.
+     * @see AttributeType#isPlaceHolder()
+     */
+    public AttributeType getAttributeType(final String nameOrOid, final Syntax syntax) {
+        return impl.getAttributeType(nameOrOid, syntax);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the attribute types
+     * contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the attribute types
+     *         contained in this schema.
+     */
+    public Collection<AttributeType> getAttributeTypes() {
+        return impl.getAttributeTypes();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the attribute types
+     * having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the attribute types to retrieve.
+     * @return An unmodifiable collection containing all of the attribute types
+     *         having the specified name or numeric OID.
+     */
+    public List<AttributeType> getAttributeTypesWithName(final String name) {
+        return impl.getAttributeTypesWithName(name);
+    }
+
+    /**
+     * Returns the DIT content rule associated with the provided structural
+     * object class, or {@code null} if no rule is defined.
+     *
+     * @param structuralClass
+     *            The structural object class .
+     * @return The DIT content rule associated with the provided structural
+     *         object class, or {@code null} if no rule is defined.
+     */
+    public DITContentRule getDITContentRule(final ObjectClass structuralClass) {
+        return impl.getDITContentRule(structuralClass);
+    }
+
+    /**
+     * Returns the DIT content rule with the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the DIT content rule to retrieve.
+     * @return The requested DIT content rule.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested DIT content rule
+     *             was not found or if the provided name is ambiguous.
+     */
+    public DITContentRule getDITContentRule(final String nameOrOid) {
+        return impl.getDITContentRule(nameOrOid);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the DIT content
+     * rules contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the DIT content
+     *         rules contained in this schema.
+     */
+    public Collection<DITContentRule> getDITContentRules() {
+        return impl.getDITContentRules();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the DIT content
+     * rules having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the DIT content rules to retrieve.
+     * @return An unmodifiable collection containing all of the DIT content
+     *         rules having the specified name or numeric OID.
+     */
+    public Collection<DITContentRule> getDITContentRulesWithName(final String name) {
+        return impl.getDITContentRulesWithName(name);
+    }
+
+    /**
+     * Returns the DIT structure rule with the specified name or numeric OID.
+     *
+     * @param ruleID
+     *            The ID of the DIT structure rule to retrieve.
+     * @return The requested DIT structure rule.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested DIT structure
+     *             rule was not found.
+     */
+    public DITStructureRule getDITStructureRule(final int ruleID) {
+        return impl.getDITStructureRule(ruleID);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the DIT structure
+     * rules associated with the provided name form.
+     *
+     * @param nameForm
+     *            The name form.
+     * @return An unmodifiable collection containing all of the DIT structure
+     *         rules associated with the provided name form.
+     */
+    public Collection<DITStructureRule> getDITStructureRules(final NameForm nameForm) {
+        return impl.getDITStructureRules(nameForm);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the DIT structure
+     * rules having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the DIT structure rules to retrieve.
+     * @return An unmodifiable collection containing all of the DIT structure
+     *         rules having the specified name or numeric OID.
+     */
+    public Collection<DITStructureRule> getDITStructureRulesWithName(final String name) {
+        return impl.getDITStructureRulesWithName(name);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the DIT structure
+     * rules contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the DIT structure
+     *         rules contained in this schema.
+     */
+    public Collection<DITStructureRule> getDITStuctureRules() {
+        return impl.getDITStuctureRules();
+    }
+
+    /**
+     * Returns the matching rule with the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule to retrieve.
+     * @return The requested matching rule.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested matching rule
+     *             was not found or if the provided name is ambiguous.
+     */
+    public MatchingRule getMatchingRule(final String nameOrOid) {
+        return impl.getMatchingRule(nameOrOid);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the matching rules
+     * contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the matching rules
+     *         contained in this schema.
+     */
+    public Collection<MatchingRule> getMatchingRules() {
+        return impl.getMatchingRules();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the matching rules
+     * having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the matching rules to retrieve.
+     * @return An unmodifiable collection containing all of the matching rules
+     *         having the specified name or numeric OID.
+     */
+    public Collection<MatchingRule> getMatchingRulesWithName(final String name) {
+        return impl.getMatchingRulesWithName(name);
+    }
+
+    /**
+     * Returns the matching rule use associated with the provided matching rule,
+     * or {@code null} if no use is defined.
+     *
+     * @param matchingRule
+     *            The matching rule whose matching rule use is to be retrieved.
+     * @return The matching rule use associated with the provided matching rule,
+     *         or {@code null} if no use is defined.
+     */
+    public MatchingRuleUse getMatchingRuleUse(final MatchingRule matchingRule) {
+        return getMatchingRuleUse(matchingRule.getOID());
+    }
+
+    /**
+     * Returns the matching rule use with the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule use to retrieve.
+     * @return The requested matching rule use.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested matching rule
+     *             use was not found or if the provided name is ambiguous.
+     */
+    public MatchingRuleUse getMatchingRuleUse(final String nameOrOid) {
+        return impl.getMatchingRuleUse(nameOrOid);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the matching rule
+     * uses contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the matching rule
+     *         uses contained in this schema.
+     */
+    public Collection<MatchingRuleUse> getMatchingRuleUses() {
+        return impl.getMatchingRuleUses();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the matching rule
+     * uses having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the matching rule uses to retrieve.
+     * @return An unmodifiable collection containing all of the matching rule
+     *         uses having the specified name or numeric OID.
+     */
+    public Collection<MatchingRuleUse> getMatchingRuleUsesWithName(final String name) {
+        return impl.getMatchingRuleUsesWithName(name);
+    }
+
+    /**
+     * Returns the name form with the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the name form to retrieve.
+     * @return The requested name form.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested name form was
+     *             not found or if the provided name is ambiguous.
+     */
+    public NameForm getNameForm(final String nameOrOid) {
+        return impl.getNameForm(nameOrOid);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the name forms
+     * contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the name forms
+     *         contained in this schema.
+     */
+    public Collection<NameForm> getNameForms() {
+        return impl.getNameForms();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the name forms
+     * associated with the provided structural object class.
+     *
+     * @param structuralClass
+     *            The structural object class whose name forms are to be
+     *            retrieved.
+     * @return An unmodifiable collection containing all of the name forms
+     *         associated with the provided structural object class.
+     */
+    public Collection<NameForm> getNameForms(final ObjectClass structuralClass) {
+        return impl.getNameForms(structuralClass);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the name forms
+     * having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the name forms to retrieve.
+     * @return An unmodifiable collection containing all of the name forms
+     *         having the specified name or numeric OID.
+     */
+    public Collection<NameForm> getNameFormsWithName(final String name) {
+        return impl.getNameFormsWithName(name);
+    }
+
+    /**
+     * Returns the object class with the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the object class to retrieve.
+     * @return The requested object class.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested object class was
+     *             not found or if the provided name is ambiguous.
+     */
+    public ObjectClass getObjectClass(final String nameOrOid) {
+        return impl.getObjectClass(nameOrOid);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the object classes
+     * contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the object classes
+     *         contained in this schema.
+     */
+    public Collection<ObjectClass> getObjectClasses() {
+        return impl.getObjectClasses();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the object classes
+     * having the specified name or numeric OID.
+     *
+     * @param name
+     *            The name of the object classes to retrieve.
+     * @return An unmodifiable collection containing all of the object classes
+     *         having the specified name or numeric OID.
+     */
+    public Collection<ObjectClass> getObjectClassesWithName(final String name) {
+        return impl.getObjectClassesWithName(name);
+    }
+
+    /**
+     * Returns the value associated to the provided {@link Option} or the option
+     * default value, if there is no such option in this schema.
+     *
+     * @param <T>
+     *            The option type.
+     * @param option
+     *            The option whose associated value should to be retrieve.
+     * @return The value associated to the provided {@link Option} or the option
+     *         default value, if there is no such option in this schema.
+     */
+    public <T> T getOption(Option<T> option) {
+        return getOptions().get(option);
+    }
+
+    Options getOptions() {
+        return impl.getOptions();
+    }
+
+    /**
+     * Returns the user-friendly name of this schema which may be used for
+     * debugging purposes. The format of the schema name is not defined but
+     * should contain the distinguished name of the subschema sub-entry for
+     * those schemas retrieved from a Directory Server.
+     *
+     * @return The user-friendly name of this schema which may be used for
+     *         debugging purposes.
+     */
+    public String getSchemaName() {
+        return impl.getSchemaName();
+    }
+
+    /**
+     * Returns the syntax with the specified numeric OID.
+     *
+     * @param numericOID
+     *            The OID of the syntax to retrieve.
+     * @return The requested syntax.
+     * @throws UnknownSchemaElementException
+     *             If this is a strict schema and the requested syntax was not
+     *             found or if the provided name is ambiguous.
+     */
+    public Syntax getSyntax(final String numericOID) {
+        return impl.getSyntax(this, numericOID);
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the syntaxes
+     * contained in this schema.
+     *
+     * @return An unmodifiable collection containing all of the syntaxes
+     *         contained in this schema.
+     */
+    public Collection<Syntax> getSyntaxes() {
+        return impl.getSyntaxes();
+    }
+
+    /**
+     * Returns an unmodifiable collection containing all of the warnings that
+     * were detected when this schema was constructed.
+     *
+     * @return An unmodifiable collection containing all of the warnings that
+     *         were detected when this schema was constructed.
+     */
+    public Collection<LocalizableMessage> getWarnings() {
+        return impl.getWarnings();
+    }
+
+    /**
+     * Indicates whether or not this schema contains an attribute type with the
+     * specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the attribute type.
+     * @return {@code true} if this schema contains an attribute type with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasAttributeType(final String nameOrOid) {
+        return impl.hasAttributeType(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a DIT content rule with the
+     * specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the DIT content rule.
+     * @return {@code true} if this schema contains a DIT content rule with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasDITContentRule(final String nameOrOid) {
+        return impl.hasDITContentRule(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a DIT structure rule with
+     * the specified rule ID.
+     *
+     * @param ruleID
+     *            The ID of the DIT structure rule.
+     * @return {@code true} if this schema contains a DIT structure rule with
+     *         the specified rule ID, otherwise {@code false}.
+     */
+    public boolean hasDITStructureRule(final int ruleID) {
+        return impl.hasDITStructureRule(ruleID);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a matching rule with the
+     * specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule.
+     * @return {@code true} if this schema contains a matching rule with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasMatchingRule(final String nameOrOid) {
+        return impl.hasMatchingRule(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a matching rule use with
+     * the specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule use.
+     * @return {@code true} if this schema contains a matching rule use with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasMatchingRuleUse(final String nameOrOid) {
+        return impl.hasMatchingRuleUse(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a name form with the
+     * specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the name form.
+     * @return {@code true} if this schema contains a name form with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasNameForm(final String nameOrOid) {
+        return impl.hasNameForm(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains an object class with the
+     * specified name or numeric OID.
+     *
+     * @param nameOrOid
+     *            The name or OID of the object class.
+     * @return {@code true} if this schema contains an object class with the
+     *         specified name or numeric OID, otherwise {@code false}.
+     */
+    public boolean hasObjectClass(final String nameOrOid) {
+        return impl.hasObjectClass(nameOrOid);
+    }
+
+    /**
+     * Indicates whether or not this schema contains a syntax with the specified
+     * numeric OID.
+     *
+     * @param numericOID
+     *            The OID of the syntax.
+     * @return {@code true} if this schema contains a syntax with the specified
+     *         numeric OID, otherwise {@code false}.
+     */
+    public boolean hasSyntax(final String numericOID) {
+        return impl.hasSyntax(numericOID);
+    }
+
+    /**
+     * Indicates whether or not this schema is strict.
+     * <p>
+     * Attribute type queries against non-strict schema always succeed: if the
+     * requested attribute type is not found then a temporary attribute type is
+     * created automatically having the Octet String syntax and associated
+     * matching rules.
+     * <p>
+     * Strict schema, on the other hand, throw an
+     * {@link UnknownSchemaElementException} whenever an attempt is made to
+     * retrieve a non-existent attribute type.
+     *
+     * @return {@code true} if this schema is strict.
+     */
+    public boolean isStrict() {
+        return impl.isStrict();
+    }
+
+    /**
+     * Adds the definitions of all the schema elements contained in this schema
+     * to the provided subschema subentry. Any existing attributes (including
+     * schema definitions) contained in the provided entry will be preserved.
+     *
+     * @param entry
+     *            The subschema subentry to which all schema definitions should
+     *            be added.
+     * @return The updated subschema subentry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public Entry toEntry(final Entry entry) {
+        Attribute attr = new LinkedAttribute(Schema.ATTR_LDAP_SYNTAXES);
+        for (final Syntax syntax : getSyntaxes()) {
+            attr.add(syntax.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_ATTRIBUTE_TYPES);
+        for (final AttributeType attributeType : getAttributeTypes()) {
+            attr.add(attributeType.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_OBJECT_CLASSES);
+        for (final ObjectClass objectClass : getObjectClasses()) {
+            attr.add(objectClass.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_MATCHING_RULE_USE);
+        for (final MatchingRuleUse matchingRuleUse : getMatchingRuleUses()) {
+            attr.add(matchingRuleUse.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_MATCHING_RULES);
+        for (final MatchingRule matchingRule : getMatchingRules()) {
+            attr.add(matchingRule.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_DIT_CONTENT_RULES);
+        for (final DITContentRule ditContentRule : getDITContentRules()) {
+            attr.add(ditContentRule.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_DIT_STRUCTURE_RULES);
+        for (final DITStructureRule ditStructureRule : getDITStuctureRules()) {
+            attr.add(ditStructureRule.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        attr = new LinkedAttribute(Schema.ATTR_NAME_FORMS);
+        for (final NameForm nameForm : getNameForms()) {
+            attr.add(nameForm.toString());
+        }
+        if (!attr.isEmpty()) {
+            entry.addAttribute(attr);
+        }
+
+        return entry;
+    }
+
+    /**
+     * Returns {@code true} if the provided entry is valid according to this
+     * schema and the specified schema validation policy.
+     * <p>
+     * If attribute value validation is enabled then following checks will be
+     * performed:
+     * <ul>
+     * <li>checking that there is at least one value
+     * <li>checking that single-valued attributes contain only a single value
+     * </ul>
+     * In particular, attribute values will not be checked for conformance to
+     * their syntax since this is expected to have already been performed while
+     * adding the values to the entry.
+     *
+     * @param entry
+     *            The entry to be validated.
+     * @param policy
+     *            The schema validation policy.
+     * @param errorMessages
+     *            A collection into which any schema validation warnings or
+     *            error messages can be placed, or {@code null} if they should
+     *            not be saved.
+     * @return {@code true} if an entry conforms to this schema based on the
+     *         provided schema validation policy.
+     */
+    public boolean validateEntry(final Entry entry, final SchemaValidationPolicy policy,
+            final Collection<LocalizableMessage> errorMessages) {
+        // First check that the object classes are recognized and that there is
+        // one structural object class.
+        ObjectClass structuralObjectClass = null;
+        final Attribute objectClassAttribute = entry.getAttribute(objectClass());
+        final List<ObjectClass> objectClasses = new LinkedList<>();
+        if (objectClassAttribute != null) {
+            for (final ByteString v : objectClassAttribute) {
+                final String objectClassName = v.toString();
+                final ObjectClass objectClass;
+                try {
+                    objectClass = getObjectClass(objectClassName);
+                    objectClasses.add(objectClass);
+                } catch (final UnknownSchemaElementException e) {
+                    if (policy.checkAttributesAndObjectClasses().needsChecking()) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_UNKNOWN_OBJECT_CLASS.get(
+                                    entry.getName(), objectClassName));
+                        }
+                        if (policy.checkAttributesAndObjectClasses().isReject()) {
+                            return false;
+                        }
+                    }
+                    continue;
+                }
+
+                if (objectClass.getObjectClassType() == ObjectClassType.STRUCTURAL) {
+                    if (structuralObjectClass == null
+                            || objectClass.isDescendantOf(structuralObjectClass)) {
+                        structuralObjectClass = objectClass;
+                    } else if (!structuralObjectClass.isDescendantOf(objectClass)
+                            && policy.requireSingleStructuralObjectClass().needsChecking()) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_MULTIPLE_STRUCTURAL_CLASSES.get(
+                                    entry.getName(), structuralObjectClass.getNameOrOID(), objectClassName));
+                        }
+                        if (policy.requireSingleStructuralObjectClass().isReject()) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        Collection<DITStructureRule> ditStructureRules = Collections.emptyList();
+        DITContentRule ditContentRule = null;
+
+        if (structuralObjectClass == null) {
+            if (policy.requireSingleStructuralObjectClass().needsChecking()) {
+                if (errorMessages != null) {
+                    errorMessages.add(ERR_ENTRY_SCHEMA_NO_STRUCTURAL_CLASS.get(entry.getName()));
+                }
+                if (policy.requireSingleStructuralObjectClass().isReject()) {
+                    return false;
+                }
+            }
+        } else {
+            ditContentRule = getDITContentRule(structuralObjectClass);
+            if (ditContentRule != null && ditContentRule.isObsolete()) {
+                ditContentRule = null;
+            }
+        }
+
+        // Check entry conforms to object classes and optional content rule.
+        if (!checkAttributesAndObjectClasses(entry, policy, errorMessages, objectClasses,
+                ditContentRule)) {
+            return false;
+        }
+
+        // Check that the name of the entry conforms to at least one applicable
+        // name form.
+        if (policy.checkNameForms().needsChecking() && structuralObjectClass != null) {
+            /**
+             * There may be multiple name forms registered with this structural
+             * object class. However, we need to select only one of the name
+             * forms and its corresponding DIT structure rule(s). We will
+             * iterate over all the name forms and see if at least one is
+             * acceptable before rejecting the entry. DIT structure rules
+             * corresponding to other non-acceptable name forms are not applied.
+             */
+            boolean foundMatchingNameForms = false;
+            NameForm nameForm = null;
+            final List<LocalizableMessage> nameFormWarnings =
+                    (errorMessages != null) ? new LinkedList<LocalizableMessage>() : null;
+            for (final NameForm nf : getNameForms(structuralObjectClass)) {
+                if (nf.isObsolete()) {
+                    continue;
+                }
+
+                // If there are any candidate name forms then at least one
+                // should be valid.
+                foundMatchingNameForms = true;
+
+                if (checkNameForm(entry, nameFormWarnings, nf)) {
+                    nameForm = nf;
+                    break;
+                }
+            }
+
+            if (foundMatchingNameForms) {
+                if (nameForm != null) {
+                    ditStructureRules = getDITStructureRules(nameForm);
+                } else {
+                    // We couldn't match this entry against any of the name
+                    // forms, so append the reasons why they didn't match and
+                    // reject if required.
+                    if (errorMessages != null) {
+                        errorMessages.addAll(nameFormWarnings);
+                    }
+                    if (policy.checkNameForms().isReject()) {
+                        return false;
+                    }
+                }
+            }
+        }
+
+        // Check DIT structure rules - this needs the parent entry.
+        if (policy.checkDITStructureRules().needsChecking() && !entry.getName().isRootDN()) {
+            boolean foundMatchingRules = false;
+            boolean foundValidRule = false;
+            final List<LocalizableMessage> ruleWarnings =
+                    (errorMessages != null) ? new LinkedList<LocalizableMessage>() : null;
+            ObjectClass parentStructuralObjectClass = null;
+            boolean parentEntryHasBeenRead = false;
+            for (final DITStructureRule rule : ditStructureRules) {
+                if (rule.isObsolete()) {
+                    continue;
+                }
+
+                foundMatchingRules = true;
+
+                // A DIT structure rule with no superiors is automatically
+                // valid, so avoid reading the parent.
+                if (rule.getSuperiorRules().isEmpty()) {
+                    foundValidRule = true;
+                    break;
+                }
+
+                if (!parentEntryHasBeenRead) {
+                    // Don't drop out immediately on failure because there may
+                    // be some
+                    // applicable rules which do not require the parent entry.
+                    parentStructuralObjectClass =
+                            getParentStructuralObjectClass(entry, policy, ruleWarnings);
+                    parentEntryHasBeenRead = true;
+                }
+
+                if (parentStructuralObjectClass != null
+                      && checkDITStructureRule(entry, ruleWarnings, rule,
+                          structuralObjectClass, parentStructuralObjectClass)) {
+                    foundValidRule = true;
+                    break;
+                }
+            }
+
+            if (foundMatchingRules) {
+                if (!foundValidRule) {
+                    // We couldn't match this entry against any of the rules, so
+                    // append the reasons why they didn't match and reject if
+                    // required.
+                    if (errorMessages != null) {
+                        errorMessages.addAll(ruleWarnings);
+                    }
+                    if (policy.checkDITStructureRules().isReject()) {
+                        return false;
+                    }
+                }
+            } else {
+                // There is no DIT structure rule for this entry, but there may
+                // be one for the parent entry. If there is such a rule for the
+                // parent entry, then this entry will not be valid.
+
+                // The parent won't have been read yet.
+                parentStructuralObjectClass =
+                        getParentStructuralObjectClass(entry, policy, ruleWarnings);
+                if (parentStructuralObjectClass == null) {
+                    if (errorMessages != null) {
+                        errorMessages.addAll(ruleWarnings);
+                    }
+                    if (policy.checkDITStructureRules().isReject()) {
+                        return false;
+                    }
+                } else {
+                    for (final NameForm nf : getNameForms(parentStructuralObjectClass)) {
+                        if (!nf.isObsolete()) {
+                            for (final DITStructureRule rule : getDITStructureRules(nf)) {
+                                if (!rule.isObsolete()) {
+                                    if (errorMessages != null) {
+                                        errorMessages.add(ERR_ENTRY_SCHEMA_DSR_MISSING_DSR.get(
+                                                entry.getName(), rule.getNameOrRuleID()));
+                                    }
+                                    if (policy.checkDITStructureRules().isReject()) {
+                                        return false;
+                                    }
+
+                                    // We could break out of the loop here in
+                                    // warn mode but continuing allows us to
+                                    // collect all conflicts.
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // If we've gotten here, then the entry is acceptable.
+        return true;
+    }
+
+    private boolean checkAttributesAndObjectClasses(final Entry entry,
+            final SchemaValidationPolicy policy,
+            final Collection<LocalizableMessage> errorMessages,
+            final List<ObjectClass> objectClasses, final DITContentRule ditContentRule) {
+        // Check object classes.
+        final boolean checkDITContentRule =
+                policy.checkDITContentRules().needsChecking() && ditContentRule != null;
+        final boolean checkObjectClasses = policy.checkAttributesAndObjectClasses().needsChecking();
+        final boolean checkAttributeValues = policy.checkAttributeValues().needsChecking();
+
+        if (checkObjectClasses || checkDITContentRule) {
+            for (final ObjectClass objectClass : objectClasses) {
+                // Make sure that any auxiliary object classes are permitted by
+                // the content rule.
+                if (checkDITContentRule
+                        && objectClass.getObjectClassType() == ObjectClassType.AUXILIARY
+                        && !ditContentRule.getAuxiliaryClasses().contains(objectClass)) {
+                    if (errorMessages != null) {
+                        errorMessages.add(ERR_ENTRY_SCHEMA_DCR_PROHIBITED_AUXILIARY_OC.get(
+                                entry.getName(), objectClass.getNameOrOID(), ditContentRule.getNameOrOID()));
+                    }
+                    if (policy.checkDITContentRules().isReject()) {
+                        return false;
+                    }
+                }
+
+                // Make sure that all of the attributes required by the object
+                // class are present.
+                if (checkObjectClasses) {
+                    for (final AttributeType t : objectClass.getDeclaredRequiredAttributes()) {
+                        final Attribute a =
+                                Attributes.emptyAttribute(AttributeDescription.create(t));
+                        if (!entry.containsAttribute(a, null)) {
+                            if (errorMessages != null) {
+                                errorMessages.add(ERR_ENTRY_SCHEMA_OC_MISSING_MUST_ATTRIBUTES.get(
+                                        entry.getName(), t.getNameOrOID(), objectClass.getNameOrOID()));
+                            }
+                            if (policy.checkAttributesAndObjectClasses().isReject()) {
+                                return false;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Make sure that all of the attributes required by the content rule
+            // are present.
+            if (checkDITContentRule) {
+                for (final AttributeType t : ditContentRule.getRequiredAttributes()) {
+                    final Attribute a = Attributes.emptyAttribute(AttributeDescription.create(t));
+                    if (!entry.containsAttribute(a, null)) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_DCR_MISSING_MUST_ATTRIBUTES.get(
+                                    entry.getName(), t.getNameOrOID(), ditContentRule.getNameOrOID()));
+                        }
+                        if (policy.checkDITContentRules().isReject()) {
+                            return false;
+                        }
+                    }
+                }
+
+                // Make sure that attributes prohibited by the content rule are
+                // not present.
+                for (final AttributeType t : ditContentRule.getProhibitedAttributes()) {
+                    final Attribute a = Attributes.emptyAttribute(AttributeDescription.create(t));
+                    if (entry.containsAttribute(a, null)) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_DCR_PROHIBITED_ATTRIBUTES.get(
+                                    entry.getName(), t.getNameOrOID(), ditContentRule.getNameOrOID()));
+                        }
+                        if (policy.checkDITContentRules().isReject()) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Check attributes.
+        if (checkObjectClasses || checkDITContentRule || checkAttributeValues) {
+            for (final Attribute attribute : entry.getAllAttributes()) {
+                final AttributeType t = attribute.getAttributeDescription().getAttributeType();
+
+                if (!t.isOperational()
+                        && (checkObjectClasses || checkDITContentRule)) {
+                    boolean isAllowed = false;
+                    for (final ObjectClass objectClass : objectClasses) {
+                        if (objectClass.isRequiredOrOptional(t)) {
+                            isAllowed = true;
+                            break;
+                        }
+                    }
+                    if (!isAllowed && ditContentRule != null && ditContentRule.isRequiredOrOptional(t)) {
+                        isAllowed = true;
+                    }
+                    if (!isAllowed) {
+                        if (errorMessages != null) {
+                            final LocalizableMessage message;
+                            if (ditContentRule != null) {
+                                message = ERR_ENTRY_SCHEMA_DCR_DISALLOWED_ATTRIBUTES.get(
+                                        entry.getName(), t.getNameOrOID(), ditContentRule.getNameOrOID());
+                            } else {
+                                message = ERR_ENTRY_SCHEMA_OC_DISALLOWED_ATTRIBUTES.get(
+                                        entry.getName(), t.getNameOrOID());
+                            }
+                            errorMessages.add(message);
+                        }
+                        if (policy.checkAttributesAndObjectClasses().isReject()
+                                || policy.checkDITContentRules().isReject()) {
+                            return false;
+                        }
+                    }
+                }
+
+                // Check all attributes contain an appropriate number of values.
+                if (checkAttributeValues) {
+                    final int sz = attribute.size();
+
+                    if (sz == 0) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_AT_EMPTY_ATTRIBUTE.get(
+                                    entry.getName(), t.getNameOrOID()));
+                        }
+                        if (policy.checkAttributeValues().isReject()) {
+                            return false;
+                        }
+                    } else if (sz > 1 && t.isSingleValue()) {
+                        if (errorMessages != null) {
+                            errorMessages.add(ERR_ENTRY_SCHEMA_AT_SINGLE_VALUED_ATTRIBUTE.get(
+                                    entry.getName(), t.getNameOrOID()));
+                        }
+                        if (policy.checkAttributeValues().isReject()) {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        // If we've gotten here, then things are OK.
+        return true;
+    }
+
+    private boolean checkDITStructureRule(final Entry entry,
+            final List<LocalizableMessage> ruleWarnings, final DITStructureRule rule,
+            final ObjectClass structuralObjectClass, final ObjectClass parentStructuralObjectClass) {
+        boolean matchFound = false;
+        for (final DITStructureRule parentRule : rule.getSuperiorRules()) {
+            if (parentRule.getNameForm().getStructuralClass().equals(parentStructuralObjectClass)) {
+                matchFound = true;
+            }
+        }
+
+        if (!matchFound) {
+            if (ruleWarnings != null) {
+                ruleWarnings.add(ERR_ENTRY_SCHEMA_DSR_ILLEGAL_OC.get(
+                        entry.getName(), rule.getNameOrRuleID(), structuralObjectClass.getNameOrOID(),
+                        parentStructuralObjectClass.getNameOrOID()));
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean checkNameForm(final Entry entry, final List<LocalizableMessage> nameFormWarnings,
+            final NameForm nameForm) {
+        final RDN rdn = entry.getName().rdn();
+        if (rdn != null) {
+            // Make sure that all the required AVAs are present.
+            for (final AttributeType t : nameForm.getRequiredAttributes()) {
+                if (rdn.getAttributeValue(t) == null) {
+                    if (nameFormWarnings != null) {
+                        nameFormWarnings.add(ERR_ENTRY_SCHEMA_NF_MISSING_MUST_ATTRIBUTES.get(
+                                entry.getName(), t.getNameOrOID(), nameForm.getNameOrOID()));
+                    }
+                    return false;
+                }
+            }
+
+            // Make sure that all AVAs in the RDN are allowed.
+            for (final AVA ava : rdn) {
+                final AttributeType t = ava.getAttributeType();
+                if (!nameForm.isRequiredOrOptional(t)) {
+                    if (nameFormWarnings != null) {
+                        nameFormWarnings.add(ERR_ENTRY_SCHEMA_NF_DISALLOWED_ATTRIBUTES.get(
+                                entry.getName(), t.getNameOrOID(), nameForm.getNameOrOID()));
+                    }
+                    return false;
+                }
+            }
+        }
+
+        // If we've gotten here, then things are OK.
+        return true;
+    }
+
+    private ObjectClass getParentStructuralObjectClass(final Entry entry,
+            final SchemaValidationPolicy policy, final List<LocalizableMessage> ruleWarnings) {
+        final Entry parentEntry;
+        try {
+            parentEntry =
+                    policy.checkDITStructureRulesEntryResolver().getEntry(entry.getName().parent());
+        } catch (final LdapException e) {
+            if (ruleWarnings != null) {
+                ruleWarnings.add(ERR_ENTRY_SCHEMA_DSR_PARENT_NOT_FOUND.get(
+                        entry.getName(), e.getResult().getDiagnosticMessage()));
+            }
+            return null;
+        }
+
+        final ObjectClass parentStructuralObjectClass =
+                Entries.getStructuralObjectClass(parentEntry, this);
+        if (parentStructuralObjectClass == null) {
+            if (ruleWarnings != null) {
+                ruleWarnings.add(ERR_ENTRY_SCHEMA_DSR_NO_PARENT_OC.get(entry.getName()));
+            }
+            return null;
+        }
+        return parentStructuralObjectClass;
+    }
+
+    @Override
+    public String toString() {
+        return "Schema " + getSchemaName()
+                + " mr=" + getMatchingRules().size()
+                + " syntaxes=" + getSyntaxes().size()
+                + " at=" + getAttributeTypes().size();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java
new file mode 100644
index 0000000..f5d7301
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java
@@ -0,0 +1,2664 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2014 Manuel Gaupp
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.schema.ObjectClass.*;
+import static org.forgerock.opendj.ldap.schema.ObjectClassType.*;
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.DITContentRule.Builder;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.util.StaticUtils;
+import com.forgerock.opendj.util.SubstringReader;
+
+/** Schema builders should be used for incremental construction of new schemas. */
+public final class SchemaBuilder {
+
+    /** Constant used for name to oid mapping when one name actually maps to multiple numerical OID. */
+    public static final String AMBIGUOUS_OID = "<ambiguous-oid>";
+
+    private static final String ATTR_SUBSCHEMA_SUBENTRY = "subschemaSubentry";
+
+    private static final String[] SUBSCHEMA_ATTRS = { ATTR_LDAP_SYNTAXES,
+        ATTR_ATTRIBUTE_TYPES, ATTR_DIT_CONTENT_RULES, ATTR_DIT_STRUCTURE_RULES,
+        ATTR_MATCHING_RULE_USE, ATTR_MATCHING_RULES, ATTR_NAME_FORMS, ATTR_OBJECT_CLASSES };
+
+    private static final Filter SUBSCHEMA_FILTER = Filter.valueOf("(objectClass=subschema)");
+
+    private static final String[] SUBSCHEMA_SUBENTRY_ATTRS = { ATTR_SUBSCHEMA_SUBENTRY };
+
+    /**
+     * Constructs a search request for retrieving the subschemaSubentry
+     * attribute from the named entry.
+     */
+    private static SearchRequest getReadSchemaForEntrySearchRequest(final DN dn) {
+        return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, Filter.objectClassPresent(),
+                SUBSCHEMA_SUBENTRY_ATTRS);
+    }
+
+    /** Constructs a search request for retrieving the named subschema sub-entry. */
+    private static SearchRequest getReadSchemaSearchRequest(final DN dn) {
+        return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, SUBSCHEMA_FILTER,
+                SUBSCHEMA_ATTRS);
+    }
+
+    private static DN getSubschemaSubentryDN(final DN name, final Entry entry) throws LdapException {
+        final Attribute subentryAttr = entry.getAttribute(ATTR_SUBSCHEMA_SUBENTRY);
+
+        if (subentryAttr == null || subentryAttr.isEmpty()) {
+            // Did not get the subschema sub-entry attribute.
+            throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
+                    ERR_NO_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString()).toString());
+        }
+
+        final String dnString = subentryAttr.iterator().next().toString();
+        DN subschemaDN;
+        try {
+            subschemaDN = DN.valueOf(dnString);
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
+                    ERR_INVALID_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString(), dnString,
+                            e.getMessageObject()).toString());
+        }
+        return subschemaDN;
+    }
+
+    private Map<Integer, DITStructureRule> id2StructureRules;
+    private Map<String, List<AttributeType>> name2AttributeTypes;
+    private Map<String, List<DITContentRule>> name2ContentRules;
+    private Map<String, List<MatchingRule>> name2MatchingRules;
+    private Map<String, List<MatchingRuleUse>> name2MatchingRuleUses;
+    private Map<String, List<NameForm>> name2NameForms;
+    private Map<String, List<ObjectClass>> name2ObjectClasses;
+    private Map<String, List<DITStructureRule>> name2StructureRules;
+    private Map<String, List<DITStructureRule>> nameForm2StructureRules;
+    private Map<String, AttributeType> numericOID2AttributeTypes;
+    private Map<String, DITContentRule> numericOID2ContentRules;
+    private Map<String, MatchingRule> numericOID2MatchingRules;
+    private Map<String, MatchingRuleUse> numericOID2MatchingRuleUses;
+    private Map<String, NameForm> numericOID2NameForms;
+    private Map<String, ObjectClass> numericOID2ObjectClasses;
+    private Map<String, Syntax> numericOID2Syntaxes;
+    private Map<String, List<NameForm>> objectClass2NameForms;
+    private Map<String, String> name2OIDs;
+    private String schemaName;
+    private List<LocalizableMessage> warnings;
+    private Options options;
+
+    /** A schema which should be copied into this builder on any mutation. */
+    private Schema copyOnWriteSchema;
+
+    /** A unique ID which can be used to uniquely identify schemas constructed without a name. */
+    private static final AtomicInteger NEXT_SCHEMA_ID = new AtomicInteger();
+
+    /** Creates a new schema builder with no schema elements and default compatibility options. */
+    public SchemaBuilder() {
+        preLazyInitBuilder(null, null);
+    }
+
+    /**
+     * Creates a new schema builder containing all of the schema elements
+     * contained in the provided subschema subentry. Any problems encountered
+     * while parsing the entry can be retrieved using the returned schema's
+     * {@link Schema#getWarnings()} method.
+     *
+     * @param entry
+     *            The subschema subentry to be parsed.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public SchemaBuilder(final Entry entry) {
+        preLazyInitBuilder(entry.getName().toString(), null);
+        addSchema(entry, true);
+    }
+
+    /**
+     * Creates a new schema builder containing all of the schema elements from
+     * the provided schema and its compatibility options.
+     *
+     * @param schema
+     *            The initial contents of the schema builder.
+     * @throws NullPointerException
+     *             If {@code schema} was {@code null}.
+     */
+    public SchemaBuilder(final Schema schema) {
+        preLazyInitBuilder(schema.getSchemaName(), schema);
+    }
+
+    /**
+     * Creates a new schema builder with no schema elements and default
+     * compatibility options.
+     *
+     * @param schemaName
+     *            The user-friendly name of this schema which may be used for
+     *            debugging purposes.
+     */
+    public SchemaBuilder(final String schemaName) {
+        preLazyInitBuilder(schemaName, null);
+    }
+
+    private Boolean allowsMalformedNamesAndOptions() {
+        return options.get(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+    }
+
+    /**
+     * Adds the provided attribute type definition to this schema builder.
+     *
+     * @param definition
+     *            The attribute type definition.
+     * @param overwrite
+     *            {@code true} if any existing attribute type with the same OID
+     *            should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided attribute type definition could not be
+     *             parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addAttributeType(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the definition was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRTYPE_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
+            AttributeType.Builder atBuilder = new AttributeType.Builder(oid, this);
+            atBuilder.definition(definition);
+            String superiorType = null;
+            String syntax = null;
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the definition. But before we start, set default
+            // values for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    atBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    atBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete.
+                    atBuilder.obsolete(true);
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the superior attribute
+                    // type from which this attribute type should inherit its
+                    // properties.
+                    superiorType = readOID(reader, allowsMalformedNamesAndOptions());
+                } else if ("equality".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the equality matching
+                    // rule to use for this attribute type.
+                    atBuilder.equalityMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
+                } else if ("ordering".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the ordering matching
+                    // rule to use for this attribute type.
+                    atBuilder.orderingMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
+                } else if ("substr".equalsIgnoreCase(tokenName)) {
+                    // This specifies the name or OID of the substring matching
+                    // rule to use for this attribute type.
+                    atBuilder.substringMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
+                } else if ("syntax".equalsIgnoreCase(tokenName)) {
+                    // This specifies the numeric OID of the syntax for this
+                    // matching rule. It may optionally be immediately followed
+                    // by an open curly brace, an integer definition, and a close
+                    // curly brace to suggest the minimum number of characters
+                    // that should be allowed in values of that type. This
+                    // implementation will ignore any such length because it
+                    // does not impose any practical limit on the length of attribute
+                    // values.
+                    syntax = readOIDLen(reader, allowsMalformedNamesAndOptions());
+                } else if ("single-value".equalsIgnoreCase(tokenName)) {
+                    // This indicates that attributes of this type are allowed
+                    // to have at most one value.
+                    atBuilder.singleValue(true);
+                } else if ("collective".equalsIgnoreCase(tokenName)) {
+                    // This indicates that attributes of this type are collective
+                    // (i.e., have their values generated dynamically in some way).
+                    atBuilder.collective(true);
+                } else if ("no-user-modification".equalsIgnoreCase(tokenName)) {
+                    // This indicates that the values of attributes of this type
+                    // are not to be modified by end users.
+                    atBuilder.noUserModification(true);
+                } else if ("usage".equalsIgnoreCase(tokenName)) {
+                    // This specifies the usage string for this attribute type.
+                    // It should be followed by one of the strings
+                    // "userApplications", "directoryOperation",
+                    // "distributedOperation", or "dSAOperation".
+                    int length = 0;
+
+                    reader.skipWhitespaces();
+                    reader.mark();
+
+                    while (" )".indexOf(reader.read()) == -1) {
+                        length++;
+                    }
+                    reader.reset();
+                    final String usageStr = reader.read(length);
+                    if ("userapplications".equalsIgnoreCase(usageStr)) {
+                        atBuilder.usage(AttributeUsage.USER_APPLICATIONS);
+                    } else if ("directoryoperation".equalsIgnoreCase(usageStr)) {
+                        atBuilder.usage(AttributeUsage.DIRECTORY_OPERATION);
+                    } else if ("distributedoperation".equalsIgnoreCase(usageStr)) {
+                        atBuilder.usage(AttributeUsage.DISTRIBUTED_OPERATION);
+                    } else if ("dsaoperation".equalsIgnoreCase(usageStr)) {
+                        atBuilder.usage(AttributeUsage.DSA_OPERATION);
+                    } else {
+                        throw new LocalizedIllegalArgumentException(
+                            WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_ATTRIBUTE_USAGE1.get(definition, usageStr));
+                    }
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    atBuilder.extraProperties(tokenName, readExtensions(reader));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_ATTRTYPE_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            final List<String> approxRules = atBuilder.getExtraProperties().get(SCHEMA_PROPERTY_APPROX_RULE);
+            if (approxRules != null && !approxRules.isEmpty()) {
+                atBuilder.approximateMatchingRule(approxRules.get(0));
+            }
+
+            if (superiorType == null && syntax == null && !options.get(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX)) {
+                throw new LocalizedIllegalArgumentException(
+                    WARN_ATTR_SYNTAX_ATTRTYPE_MISSING_SYNTAX_AND_SUPERIOR.get(definition));
+            }
+
+            atBuilder.superiorType(superiorType)
+                     .syntax(syntax);
+
+            return atBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg = ERR_ATTR_SYNTAX_ATTRTYPE_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+    }
+
+    /**
+     * Adds the provided DIT content rule definition to this schema builder.
+     *
+     * @param definition
+     *            The DIT content rule definition.
+     * @param overwrite
+     *            {@code true} if any existing DIT content rule with the same
+     *            OID should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided DIT content rule definition could not be
+     *             parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addDITContentRule(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DCR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final DITContentRule.Builder contentRuleBuilder =
+                    buildDITContentRule(readOID(reader, allowsMalformedNamesAndOptions()));
+            contentRuleBuilder.definition(definition);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    contentRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    contentRuleBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete.
+                    contentRuleBuilder.obsolete(true);
+                } else if ("aux".equalsIgnoreCase(tokenName)) {
+                    contentRuleBuilder.auxiliaryObjectClasses(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    contentRuleBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    contentRuleBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if ("not".equalsIgnoreCase(tokenName)) {
+                    contentRuleBuilder.prohibitedAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    contentRuleBuilder.extraProperties(tokenName, readExtensions(reader));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_DCR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            return contentRuleBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg = ERR_ATTR_SYNTAX_DCR_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+    }
+
+    /**
+     * Adds the provided DIT structure rule definition to this schema builder.
+     *
+     * @param definition
+     *            The DIT structure rule definition.
+     * @param overwrite
+     *            {@code true} if any existing DIT structure rule with the same
+     *            OID should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided DIT structure rule definition could not be
+     *             parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addDITStructureRule(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final DITStructureRule.Builder ruleBuilder = new DITStructureRule.Builder(readRuleID(reader), this);
+            String nameForm = null;
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    ruleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    ruleBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete.
+                    ruleBuilder.obsolete(true);
+                } else if ("form".equalsIgnoreCase(tokenName)) {
+                    nameForm = readOID(reader, allowsMalformedNamesAndOptions());
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    ruleBuilder.superiorRules(readRuleIDs(reader));
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    ruleBuilder.extraProperties(tokenName, readExtensions(reader));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                            ERR_ATTR_SYNTAX_DSR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            if (nameForm == null) {
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM.get(definition));
+            }
+            ruleBuilder.nameForm(nameForm);
+
+            return ruleBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg = ERR_ATTR_SYNTAX_DSR_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+    }
+
+    /**
+     * Adds the provided enumeration syntax definition to this schema builder.
+     *
+     * @param oid
+     *            The OID of the enumeration syntax definition.
+     * @param description
+     *            The description of the enumeration syntax definition.
+     * @param overwrite
+     *            {@code true} if any existing syntax with the same OID should
+     *            be overwritten.
+     * @param enumerations
+     *            The range of values which attribute values must match in order
+     *            to be valid.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     */
+    public SchemaBuilder addEnumerationSyntax(final String oid, final String description,
+            final boolean overwrite, final String... enumerations) {
+        Reject.ifNull((Object) enumerations);
+        return buildSyntax(oid)
+                .description(description)
+                .extraProperties("X-ENUM", enumerations)
+                .addToSchema(overwrite);
+    }
+
+    /**
+     * Adds the provided matching rule definition to this schema builder.
+     *
+     * @param definition
+     *            The matching rule definition.
+     * @param overwrite
+     *            {@code true} if any existing matching rule with the same OID
+     *            should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided matching rule definition could not be parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addMatchingRule(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final MatchingRule.Builder matchingRuleBuilder = new MatchingRule.Builder(
+                readOID(reader, allowsMalformedNamesAndOptions()), this);
+            matchingRuleBuilder.definition(definition);
+
+            String syntax = null;
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    matchingRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the matching rule. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    matchingRuleBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the matching rule should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                    matchingRuleBuilder.obsolete(true);
+                } else if ("syntax".equalsIgnoreCase(tokenName)) {
+                    syntax = readOID(reader, allowsMalformedNamesAndOptions());
+                    matchingRuleBuilder.syntaxOID(syntax);
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    final List<String> extensions = readExtensions(reader);
+                    matchingRuleBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_MR_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that a syntax was specified.
+            if (syntax == null) {
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_NO_SYNTAX.get(definition));
+            }
+            matchingRuleBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg =
+                    ERR_ATTR_SYNTAX_MR_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+        return this;
+    }
+
+    /**
+     * Adds the provided matching rule use definition to this schema builder.
+     *
+     * @param definition
+     *            The matching rule use definition.
+     * @param overwrite
+     *            {@code true} if any existing matching rule use with the same
+     *            OID should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided matching rule use definition could not be
+     *             parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addMatchingRuleUse(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final MatchingRuleUse.Builder useBuilder =
+                    buildMatchingRuleUse(readOID(reader, allowsMalformedNamesAndOptions()));
+            Set<String> attributes = null;
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    useBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    useBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete.
+                    useBuilder.obsolete(true);
+                } else if ("applies".equalsIgnoreCase(tokenName)) {
+                    attributes = readOIDs(reader, allowsMalformedNamesAndOptions());
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    useBuilder.extraProperties(tokenName, readExtensions(reader));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that the set of attributes was defined.
+            if (attributes == null || attributes.isEmpty()) {
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_NO_ATTR.get(definition));
+            }
+            useBuilder.attributes(attributes);
+
+            return useBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg = ERR_ATTR_SYNTAX_MRUSE_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * attribute type before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildAttributeType("attributetype-oid").name("attribute type name").addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the attribute type definition.
+     * @return A builder to continue building the AttributeType.
+     */
+    public AttributeType.Builder buildAttributeType(final String oid) {
+        lazyInitBuilder();
+        return new AttributeType.Builder(oid, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * DIT structure rule before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * final int myRuleID = ...;
+     * builder.buildDITStructureRule(myRuleID).name("DIT structure rule name").addToSchema();
+     * </pre>
+     *
+     * @param ruleID
+     *            The ID of the DIT structure rule.
+     * @return A builder to continue building the DITStructureRule.
+     */
+    public DITStructureRule.Builder buildDITStructureRule(final int ruleID) {
+        lazyInitBuilder();
+        return new DITStructureRule.Builder(ruleID, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new matching rule before adding it to the
+     * schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildMatchingRule("matchingrule-oid").name("matching rule name").addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the matching rule definition.
+     * @return A builder to continue building the MatchingRule.
+     */
+    public MatchingRule.Builder buildMatchingRule(final String oid) {
+        lazyInitBuilder();
+        return new MatchingRule.Builder(oid, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * matching rule use before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildMatchingRuleUse("matchingrule-oid")
+     *        .name("matching rule use name")
+     *        .addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the matching rule definition.
+     * @return A builder to continue building the MatchingRuleUse.
+     */
+    public MatchingRuleUse.Builder buildMatchingRuleUse(final String oid) {
+        lazyInitBuilder();
+        return new MatchingRuleUse.Builder(oid, this);
+    }
+
+    /**
+     * Adds the provided name form definition to this schema builder.
+     *
+     * @param definition
+     *            The name form definition.
+     * @param overwrite
+     *            {@code true} if any existing name form with the same OID
+     *            should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided name form definition could not be parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addNameForm(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, c);
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final NameForm.Builder nameFormBuilder = new NameForm.Builder(
+                readOID(reader, allowsMalformedNamesAndOptions()), this);
+            nameFormBuilder.definition(definition);
+
+            // Required properties :
+            String structuralOID = null;
+            Collection<String> requiredAttributes = Collections.emptyList();
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    nameFormBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    nameFormBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete. We do not need to do any more
+                    // parsing for this token.
+                    nameFormBuilder.obsolete(true);
+                } else if ("oc".equalsIgnoreCase(tokenName)) {
+                    structuralOID = readOID(reader, allowsMalformedNamesAndOptions());
+                    nameFormBuilder.structuralObjectClassOID(structuralOID);
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    requiredAttributes = readOIDs(reader, allowsMalformedNamesAndOptions());
+                    nameFormBuilder.requiredAttributes(requiredAttributes);
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    nameFormBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    final List<String> extensions = readExtensions(reader);
+                    nameFormBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_NAME_FORM_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            // Make sure that a structural class was specified. If not, then
+            // it cannot be valid and the name form cannot be build.
+            if (structuralOID == null) {
+                throw new LocalizedIllegalArgumentException(
+                    ERR_ATTR_SYNTAX_NAME_FORM_NO_STRUCTURAL_CLASS1.get(definition));
+            }
+
+            if (requiredAttributes.isEmpty()) {
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_NO_REQUIRED_ATTR.get(definition));
+            }
+
+            nameFormBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg =
+                    ERR_ATTR_SYNTAX_NAME_FORM_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+        return this;
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * DIT content rule before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildDITContentRule("structuralobjectclass-oid").name("DIT content rule name").addToSchema();
+     * </pre>
+     *
+     * @param structuralClassOID
+     *            The OID of the structural objectclass for the DIT content rule to build.
+     * @return A builder to continue building the DITContentRule.
+     */
+    public Builder buildDITContentRule(String structuralClassOID) {
+        lazyInitBuilder();
+        return new DITContentRule.Builder(structuralClassOID, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * name form before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildNameForm("1.2.3.4").name("myNameform").addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the name form definition.
+     * @return A builder to continue building the NameForm.
+     */
+    public NameForm.Builder buildNameForm(final String oid) {
+        lazyInitBuilder();
+        return new NameForm.Builder(oid, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * object class before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildObjectClass("objectclass-oid").name("object class name").addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the object class definition.
+     * @return A builder to continue building the ObjectClass.
+     */
+    public ObjectClass.Builder buildObjectClass(final String oid) {
+        lazyInitBuilder();
+        return new ObjectClass.Builder(oid, this);
+    }
+
+    /**
+     * Returns a builder which can be used for incrementally constructing a new
+     * syntax before adding it to the schema. Example usage:
+     *
+     * <pre>
+     * SchemaBuilder builder = ...;
+     * builder.buildSyntax("1.2.3.4").addToSchema();
+     * </pre>
+     *
+     * @param oid
+     *            The OID of the syntax definition.
+     * @return A builder to continue building the syntax.
+     */
+    public Syntax.Builder buildSyntax(final String oid) {
+        lazyInitBuilder();
+        return new Syntax.Builder(oid, this);
+    }
+
+    /**
+     * Returns an attribute type builder whose fields are initialized to the
+     * values of the provided attribute type. This method should be used when
+     * duplicating attribute types from external schemas or when modifying
+     * existing attribute types.
+     *
+     * @param attributeType
+     *            The attribute type source.
+     * @return A builder to continue building the AttributeType.
+     */
+    public AttributeType.Builder buildAttributeType(final AttributeType attributeType) {
+        lazyInitBuilder();
+        return new AttributeType.Builder(attributeType, this);
+    }
+
+    /**
+     * Returns a DIT content rule builder whose fields are initialized to the
+     * values of the provided DIT content rule. This method should be used when
+     * duplicating DIT content rules from external schemas or when modifying
+     * existing DIT content rules.
+     *
+     * @param contentRule
+     *            The DIT content rule source.
+     * @return A builder to continue building the DITContentRule.
+     */
+    public DITContentRule.Builder buildDITContentRule(DITContentRule contentRule) {
+        lazyInitBuilder();
+        return new DITContentRule.Builder(contentRule, this);
+    }
+
+    /**
+     * Returns an DIT structure rule builder whose fields are initialized to the
+     * values of the provided rule. This method should be used when duplicating
+     * structure rules from external schemas or when modifying existing
+     * structure rules.
+     *
+     * @param structureRule
+     *            The DIT structure rule source.
+     * @return A builder to continue building the DITStructureRule.
+     */
+    public DITStructureRule.Builder buildDITStructureRule(final DITStructureRule structureRule) {
+        lazyInitBuilder();
+        return new DITStructureRule.Builder(structureRule, this);
+    }
+
+    /**
+     * Returns a matching rule builder whose fields are initialized to the
+     * values of the provided matching rule. This method should be used when
+     * duplicating matching rules from external schemas or when modifying
+     * existing matching rules.
+     *
+     * @param matchingRule
+     *            The matching rule source.
+     * @return A builder to continue building the MatchingRule.
+     */
+    public MatchingRule.Builder buildMatchingRule(final MatchingRule matchingRule) {
+        lazyInitBuilder();
+        return new MatchingRule.Builder(matchingRule, this);
+    }
+
+    /**
+     * Returns a matching rule use builder whose fields are initialized to the
+     * values of the provided matching rule use object. This method should be used when
+     * duplicating matching rule uses from external schemas or when modifying
+     * existing matching rule uses.
+     *
+     * @param matchingRuleUse
+     *            The matching rule use source.
+     * @return A builder to continue building the MatchingRuleUse.
+     */
+    public MatchingRuleUse.Builder buildMatchingRuleUse(final MatchingRuleUse matchingRuleUse) {
+        lazyInitBuilder();
+        return new MatchingRuleUse.Builder(matchingRuleUse, this);
+    }
+
+    /**
+     * Returns a name form builder whose fields are initialized to the
+     * values of the provided name form. This method should be used when
+     * duplicating name forms from external schemas or when modifying
+     * existing names forms.
+     *
+     * @param nameForm
+     *            The name form source.
+     * @return A builder to continue building the NameForm.
+     */
+    public NameForm.Builder buildNameForm(final NameForm nameForm) {
+        lazyInitBuilder();
+        return new NameForm.Builder(nameForm, this);
+    }
+
+    /**
+     * Returns an object class builder whose fields are initialized to the
+     * values of the provided object class. This method should be used when
+     * duplicating object classes from external schemas or when modifying
+     * existing object classes.
+     *
+     * @param objectClass
+     *            The object class source.
+     * @return A builder to continue building the ObjectClass.
+     */
+    public ObjectClass.Builder buildObjectClass(final ObjectClass objectClass) {
+        lazyInitBuilder();
+        return new ObjectClass.Builder(objectClass, this);
+    }
+
+    /**
+     * Returns a syntax builder whose fields are initialized to the
+     * values of the provided syntax. This method should be used when
+     * duplicating syntaxes from external schemas or when modifying
+     * existing syntaxes.
+     *
+     * @param syntax
+     *            The syntax source.
+     * @return A builder to continue building the Syntax.
+     */
+    public Syntax.Builder buildSyntax(final Syntax syntax) {
+        lazyInitBuilder();
+        return new Syntax.Builder(syntax, this);
+    }
+
+    /**
+     * Adds the provided object class definition to this schema builder.
+     *
+     * @param definition
+     *            The object class definition.
+     * @param overwrite
+     *            {@code true} if any existing object class with the same OID
+     *            should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided object class definition could not be parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addObjectClass(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_OBJECTCLASS_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message =  ERR_ATTR_SYNTAX_OBJECTCLASS_EXPECTED_OPEN_PARENTHESIS1.get(
+                            definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters is the OID.
+            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
+            Set<String> superiorClasses = emptySet();
+            ObjectClassType ocType = null;
+            ObjectClass.Builder ocBuilder = new ObjectClass.Builder(oid, this).definition(definition);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("name".equalsIgnoreCase(tokenName)) {
+                    ocBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the attribute type. It
+                    // is an arbitrary string of characters enclosed in single
+                    // quotes.
+                    ocBuilder.description(readQuotedString(reader));
+                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
+                    // This indicates whether the attribute type should be
+                    // considered obsolete.
+                    ocBuilder.obsolete(true);
+                } else if ("sup".equalsIgnoreCase(tokenName)) {
+                    superiorClasses = readOIDs(reader, allowsMalformedNamesAndOptions());
+                } else if ("abstract".equalsIgnoreCase(tokenName)) {
+                    // This indicates that entries must not include this
+                    // objectclass unless they also include a non-abstract
+                    // objectclass that inherits from this class.
+                    ocType = ABSTRACT;
+                } else if ("structural".equalsIgnoreCase(tokenName)) {
+                    ocType = STRUCTURAL;
+                } else if ("auxiliary".equalsIgnoreCase(tokenName)) {
+                    ocType = AUXILIARY;
+                } else if ("must".equalsIgnoreCase(tokenName)) {
+                    ocBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if ("may".equalsIgnoreCase(tokenName)) {
+                    ocBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    final List<String> extensions = readExtensions(reader);
+                    ocBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            if (EXTENSIBLE_OBJECT_OBJECTCLASS_OID.equals(oid)) {
+                addObjectClass(newExtensibleObjectObjectClass(
+                    ocBuilder.getDescription(), ocBuilder.getExtraProperties(), this), overwrite);
+                return this;
+            } else {
+                if (ocType == STRUCTURAL && superiorClasses.isEmpty()) {
+                    superiorClasses = singleton(TOP_OBJECTCLASS_NAME);
+                }
+                ocBuilder.superiorObjectClasses(superiorClasses)
+                         .type(ocType);
+                return ocBuilder.addToSchema(overwrite);
+            }
+        } catch (final DecodeException e) {
+            throw new LocalizedIllegalArgumentException(
+                ERR_ATTR_SYNTAX_OBJECTCLASS_INVALID1.get(definition, e.getMessageObject()), e.getCause());
+        }
+    }
+
+    /**
+     * Adds the provided pattern syntax definition to this schema builder.
+     *
+     * @param oid
+     *            The OID of the pattern syntax definition.
+     * @param description
+     *            The description of the pattern syntax definition.
+     * @param pattern
+     *            The regular expression pattern which attribute values must
+     *            match in order to be valid.
+     * @param overwrite
+     *            {@code true} if any existing syntax with the same OID should
+     *            be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     */
+    public SchemaBuilder addPatternSyntax(final String oid, final String description,
+            final Pattern pattern, final boolean overwrite) {
+        Reject.ifNull(pattern);
+        return buildSyntax(oid)
+                .description(description)
+                .extraProperties("X-PATTERN", pattern.toString())
+                .addToSchema(overwrite);
+    }
+
+    /**
+     * Reads the schema elements contained in the named subschema sub-entry and
+     * adds them to this schema builder.
+     * <p>
+     * If the requested schema is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the subschema sub-entry.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public SchemaBuilder addSchema(final Connection connection, final DN name,
+            final boolean overwrite) throws LdapException {
+        // The call to addSchema will perform copyOnWrite.
+        final SearchRequest request = getReadSchemaSearchRequest(name);
+        final Entry entry = connection.searchSingleEntry(request);
+        return addSchema(entry, overwrite);
+    }
+
+    /**
+     * Adds all of the schema elements contained in the provided subschema
+     * subentry to this schema builder. Any problems encountered while parsing
+     * the entry can be retrieved using the returned schema's
+     * {@link Schema#getWarnings()} method.
+     *
+     * @param entry
+     *            The subschema subentry to be parsed.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    public SchemaBuilder addSchema(final Entry entry, final boolean overwrite) {
+        Reject.ifNull(entry);
+
+        lazyInitBuilder();
+
+        Attribute attr = entry.getAttribute(Schema.ATTR_LDAP_SYNTAXES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addSyntax(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addAttributeType(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_OBJECT_CLASSES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addObjectClass(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_MATCHING_RULE_USE);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addMatchingRuleUse(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_MATCHING_RULES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addMatchingRule(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_DIT_CONTENT_RULES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addDITContentRule(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addDITStructureRule(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        attr = entry.getAttribute(Schema.ATTR_NAME_FORMS);
+        if (attr != null) {
+            for (final ByteString def : attr) {
+                try {
+                    addNameForm(def.toString(), overwrite);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    warnings.add(e.getMessageObject());
+                }
+            }
+        }
+
+        return this;
+    }
+
+    /**
+     * Adds all of the schema elements in the provided schema to this schema
+     * builder.
+     *
+     * @param schema
+     *            The schema to be copied into this schema builder.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and conflicting schema
+     *             elements were found.
+     * @throws NullPointerException
+     *             If {@code schema} was {@code null}.
+     */
+    public SchemaBuilder addSchema(final Schema schema, final boolean overwrite) {
+        Reject.ifNull(schema);
+
+        lazyInitBuilder();
+
+        addSchema0(schema, overwrite);
+        return this;
+    }
+
+    /**
+     * Asynchronously reads the schema elements contained in the named subschema
+     * sub-entry and adds them to this schema builder.
+     * <p>
+     * If the requested schema is not returned by the Directory Server then the
+     * request will fail with an {@link EntryNotFoundException}.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the subschema sub-entry.
+     *            the operation result when it is received, may be {@code null}.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A promise representing the updated schema builder.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public LdapPromise<SchemaBuilder> addSchemaAsync(final Connection connection, final DN name,
+        final boolean overwrite) {
+        // The call to addSchema will perform copyOnWrite.
+        return connection.searchSingleEntryAsync(getReadSchemaSearchRequest(name)).then(
+                new Function<SearchResultEntry, SchemaBuilder, LdapException>() {
+                    @Override
+                    public SchemaBuilder apply(SearchResultEntry result) throws LdapException {
+                        addSchema(result, overwrite);
+                        return SchemaBuilder.this;
+                    }
+                });
+    }
+
+    /**
+     * Reads the schema elements contained in the subschema sub-entry which
+     * applies to the named entry and adds them to this schema builder.
+     * <p>
+     * If the requested entry or its associated schema are not returned by the
+     * Directory Server then the request will fail with an
+     * {@link EntryNotFoundException}.
+     * <p>
+     * This implementation first reads the {@code subschemaSubentry} attribute
+     * of the entry in order to identify the schema and then invokes
+     * {@link #addSchemaForEntry(Connection, DN, boolean)} to read the schema.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the entry whose schema is to be
+     *            located.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A reference to this schema builder.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public SchemaBuilder addSchemaForEntry(final Connection connection, final DN name,
+            final boolean overwrite) throws LdapException {
+        // The call to addSchema will perform copyOnWrite.
+        final SearchRequest request = getReadSchemaForEntrySearchRequest(name);
+        final Entry entry = connection.searchSingleEntry(request);
+        final DN subschemaDN = getSubschemaSubentryDN(name, entry);
+        return addSchema(connection, subschemaDN, overwrite);
+    }
+
+    /**
+     * Asynchronously reads the schema elements contained in the subschema
+     * sub-entry which applies to the named entry and adds them to this schema
+     * builder.
+     * <p>
+     * If the requested entry or its associated schema are not returned by the
+     * Directory Server then the request will fail with an
+     * {@link EntryNotFoundException}.
+     * <p>
+     * This implementation first reads the {@code subschemaSubentry} attribute
+     * of the entry in order to identify the schema and then invokes
+     * {@link #addSchemaAsync(Connection, DN, boolean)} to read the schema.
+     *
+     * @param connection
+     *            A connection to the Directory Server whose schema is to be
+     *            read.
+     * @param name
+     *            The distinguished name of the entry whose schema is to be
+     *            located.
+     * @param overwrite
+     *            {@code true} if existing schema elements with the same
+     *            conflicting OIDs should be overwritten.
+     * @return A promise representing the updated schema builder.
+     * @throws UnsupportedOperationException
+     *             If the connection does not support search operations.
+     * @throws IllegalStateException
+     *             If the connection has already been closed, i.e. if
+     *             {@code connection.isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code connection} or {@code name} was {@code null}.
+     */
+    public LdapPromise<SchemaBuilder> addSchemaForEntryAsync(final Connection connection, final DN name,
+            final boolean overwrite) {
+        return connection.searchSingleEntryAsync(getReadSchemaForEntrySearchRequest(name)).thenAsync(
+                new AsyncFunction<SearchResultEntry, SchemaBuilder, LdapException>() {
+                    @Override
+                    public Promise<SchemaBuilder, LdapException> apply(SearchResultEntry result) throws LdapException {
+                        final DN subschemaDN = getSubschemaSubentryDN(name, result);
+                        return addSchemaAsync(connection, subschemaDN, overwrite);
+                    }
+                });
+    }
+
+    /**
+     * Adds the provided substitution syntax definition to this schema builder.
+     *
+     * @param oid
+     *            The OID of the substitution syntax definition.
+     * @param description
+     *            The description of the substitution syntax definition.
+     * @param substituteSyntax
+     *            The OID of the syntax whose implementation should be
+     *            substituted.
+     * @param overwrite
+     *            {@code true} if any existing syntax with the same OID should
+     *            be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     */
+    public SchemaBuilder addSubstitutionSyntax(final String oid, final String description,
+            final String substituteSyntax, final boolean overwrite) {
+        Reject.ifNull(substituteSyntax);
+        return buildSyntax(oid)
+                .description(description)
+                .extraProperties("X-SUBST", substituteSyntax)
+                .addToSchema(overwrite);
+    }
+
+    /**
+     * Adds the provided syntax definition to this schema builder.
+     *
+     * @param definition
+     *            The syntax definition.
+     * @param overwrite
+     *            {@code true} if any existing syntax with the same OID should
+     *            be overwritten.
+     * @return A reference to this schema builder.
+     * @throws ConflictingSchemaElementException
+     *             If {@code overwrite} was {@code false} and a conflicting
+     *             schema element was found.
+     * @throws LocalizedIllegalArgumentException
+     *             If the provided syntax definition could not be parsed.
+     * @throws NullPointerException
+     *             If {@code definition} was {@code null}.
+     */
+    public SchemaBuilder addSyntax(final String definition, final boolean overwrite) {
+        Reject.ifNull(definition);
+
+        lazyInitBuilder();
+
+        try {
+            final SubstringReader reader = new SubstringReader(definition);
+
+            // We'll do this a character at a time. First, skip over any
+            // leading whitespace.
+            reader.skipWhitespaces();
+
+            if (reader.remaining() <= 0) {
+                // This means that the value was empty or contained only
+                // whitespace. That is illegal.
+                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE1.get(definition));
+            }
+
+            // The next character must be an open parenthesis. If it is not,
+            // then that is an error.
+            final char c = reader.read();
+            if (c != '(') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS.get(
+                    definition, reader.pos() - 1, String.valueOf(c));
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            // Skip over any spaces immediately following the opening
+            // parenthesis.
+            reader.skipWhitespaces();
+
+            // The next set of characters must be the OID.
+            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
+            final Syntax.Builder syntaxBuilder = new Syntax.Builder(oid, this).definition(definition);
+
+            // At this point, we should have a pretty specific syntax that
+            // describes what may come next, but some of the components are
+            // optional and it would be pretty easy to put something in the
+            // wrong order, so we will be very flexible about what we can
+            // accept. Just look at the next token, figure out what it is and
+            // how to treat what comes after it, then repeat until we get to
+            // the end of the value. But before we start, set default values
+            // for everything else we might need to know.
+            while (true) {
+                final String tokenName = readTokenName(reader);
+
+                if (tokenName == null) {
+                    // No more tokens.
+                    break;
+                } else if ("desc".equalsIgnoreCase(tokenName)) {
+                    // This specifies the description for the syntax. It is an
+                    // arbitrary string of characters enclosed in single quotes.
+                    syntaxBuilder.description(readQuotedString(reader));
+                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
+                    // This must be a non-standard property and it must be
+                    // followed by either a single definition in single quotes
+                    // or an open parenthesis followed by one or more values in
+                    // single quotes separated by spaces followed by a close
+                    // parenthesis.
+                    final List<String> extensions = readExtensions(reader);
+                    syntaxBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
+                } else {
+                    throw new LocalizedIllegalArgumentException(
+                        ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_TOKEN1.get(definition, tokenName));
+                }
+            }
+
+            syntaxBuilder.addToSchema(overwrite);
+        } catch (final DecodeException e) {
+            final LocalizableMessage msg =
+                    ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID1.get(definition, e.getMessageObject());
+            throw new LocalizedIllegalArgumentException(msg, e.getCause());
+        }
+        return this;
+    }
+
+    Options getOptions() {
+        lazyInitBuilder();
+
+        return options;
+    }
+
+    /**
+     * Removes the named attribute type from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the attribute type to be removed.
+     * @return {@code true} if the attribute type was found.
+     */
+    public boolean removeAttributeType(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final AttributeType element = numericOID2AttributeTypes.get(nameOrOid);
+        if (element != null) {
+            removeAttributeType(element);
+            return true;
+        }
+        final List<AttributeType> elements = name2AttributeTypes.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final AttributeType e : elements) {
+                removeAttributeType(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named DIT content rule from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the DIT content rule to be removed.
+     * @return {@code true} if the DIT content rule was found.
+     */
+    public boolean removeDITContentRule(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final DITContentRule element = numericOID2ContentRules.get(nameOrOid);
+        if (element != null) {
+            removeDITContentRule(element);
+            return true;
+        }
+        final List<DITContentRule> elements = name2ContentRules.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final DITContentRule e : elements) {
+                removeDITContentRule(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the specified DIT structure rule from this schema builder.
+     *
+     * @param ruleID
+     *            The ID of the DIT structure rule to be removed.
+     * @return {@code true} if the DIT structure rule was found.
+     */
+    public boolean removeDITStructureRule(final int ruleID) {
+        lazyInitBuilder();
+
+        final DITStructureRule element = id2StructureRules.get(ruleID);
+        if (element != null) {
+            removeDITStructureRule(element);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named matching rule from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule to be removed.
+     * @return {@code true} if the matching rule was found.
+     */
+    public boolean removeMatchingRule(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final MatchingRule element = numericOID2MatchingRules.get(nameOrOid);
+        if (element != null) {
+            removeMatchingRule(element);
+            return true;
+        }
+        final List<MatchingRule> elements = name2MatchingRules.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final MatchingRule e : elements) {
+                removeMatchingRule(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named matching rule use from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the matching rule use to be removed.
+     * @return {@code true} if the matching rule use was found.
+     */
+    public boolean removeMatchingRuleUse(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final MatchingRuleUse element = numericOID2MatchingRuleUses.get(nameOrOid);
+        if (element != null) {
+            removeMatchingRuleUse(element);
+            return true;
+        }
+        final List<MatchingRuleUse> elements = name2MatchingRuleUses.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final MatchingRuleUse e : elements) {
+                removeMatchingRuleUse(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named name form from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the name form to be removed.
+     * @return {@code true} if the name form was found.
+     */
+    public boolean removeNameForm(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final NameForm element = numericOID2NameForms.get(nameOrOid);
+        if (element != null) {
+            removeNameForm(element);
+            return true;
+        }
+        final List<NameForm> elements = name2NameForms.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final NameForm e : elements) {
+                removeNameForm(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named object class from this schema builder.
+     *
+     * @param nameOrOid
+     *            The name or OID of the object class to be removed.
+     * @return {@code true} if the object class was found.
+     */
+    public boolean removeObjectClass(final String nameOrOid) {
+        lazyInitBuilder();
+
+        final ObjectClass element = numericOID2ObjectClasses.get(nameOrOid);
+        if (element != null) {
+            removeObjectClass(element);
+            return true;
+        }
+        final List<ObjectClass> elements = name2ObjectClasses.get(toLowerCase(nameOrOid));
+        if (elements != null) {
+            for (final ObjectClass e : elements) {
+                removeObjectClass(e);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the named syntax from this schema builder.
+     *
+     * @param numericOID
+     *            The name of the syntax to be removed.
+     * @return {@code true} if the syntax was found.
+     */
+    public boolean removeSyntax(final String numericOID) {
+        lazyInitBuilder();
+
+        final Syntax element = numericOID2Syntaxes.get(numericOID);
+        if (element != null) {
+            removeSyntax(element);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Sets a schema option overriding any previous values for the option.
+     *
+     * @param <T>
+     *            The option type.
+     * @param option
+     *            Option with which the specified value is to be associated.
+     * @param value
+     *            Value to be associated with the specified option.
+     * @return A reference to this schema builder.
+     * @throws UnsupportedOperationException
+     *             If the schema builder options are read only.
+     */
+    public <T> SchemaBuilder setOption(final Option<T> option, T value) {
+        getOptions().set(option, value);
+        return this;
+    }
+
+    /**
+     * Returns a strict {@code Schema} containing all of the schema elements
+     * contained in this schema builder as well as the same set of schema
+     * compatibility options.
+     * <p>
+     * This method does not alter the contents of this schema builder.
+     *
+     * @return A {@code Schema} containing all of the schema elements contained
+     *         in this schema builder as well as the same set of schema
+     *         compatibility options
+     */
+    public Schema toSchema() {
+        // If this schema builder was initialized from another schema and no
+        // modifications have been made since then we can simply return the
+        // original schema.
+        if (copyOnWriteSchema != null) {
+            return copyOnWriteSchema;
+        }
+
+        // We still need to ensure that this builder has been initialized
+        // (otherwise some fields may still be null).
+        lazyInitBuilder();
+
+        final String localSchemaName;
+        if (schemaName != null) {
+            localSchemaName = schemaName + "-" + NEXT_SCHEMA_ID.getAndIncrement();
+        } else {
+            localSchemaName = "Schema#" + NEXT_SCHEMA_ID.getAndIncrement();
+        }
+
+        Syntax defaultSyntax = numericOID2Syntaxes.get(options.get(DEFAULT_SYNTAX_OID));
+        if (defaultSyntax == null) {
+            defaultSyntax = Schema.getCoreSchema().getDefaultSyntax();
+        }
+
+        MatchingRule defaultMatchingRule =  numericOID2MatchingRules.get(options.get(DEFAULT_MATCHING_RULE_OID));
+        if (defaultMatchingRule == null) {
+            defaultMatchingRule = Schema.getCoreSchema().getDefaultMatchingRule();
+        }
+
+        final Schema schema =
+                new Schema.StrictImpl(localSchemaName, options,
+                        defaultSyntax, defaultMatchingRule, numericOID2Syntaxes,
+                        numericOID2MatchingRules, numericOID2MatchingRuleUses,
+                        numericOID2AttributeTypes, numericOID2ObjectClasses, numericOID2NameForms,
+                        numericOID2ContentRules, id2StructureRules, name2MatchingRules,
+                        name2MatchingRuleUses, name2AttributeTypes, name2ObjectClasses,
+                        name2NameForms, name2ContentRules, name2StructureRules,
+                        objectClass2NameForms, nameForm2StructureRules, name2OIDs, warnings).asStrictSchema();
+        validate(schema);
+
+        // Re-init this builder so that it can continue to be used afterwards.
+        preLazyInitBuilder(schemaName, schema);
+
+        return schema;
+    }
+
+    private void registerNameToOIDMapping(String name, String anOID) {
+        if (name2OIDs.put(name, anOID) != null) {
+            name2OIDs.put(name, AMBIGUOUS_OID);
+        }
+    }
+
+    SchemaBuilder addAttributeType(final AttributeType attribute, final boolean overwrite) {
+        AttributeType conflictingAttribute;
+        if (numericOID2AttributeTypes.containsKey(attribute.getOID())) {
+            conflictingAttribute = numericOID2AttributeTypes.get(attribute.getOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_ATTRIBUTE_OID.get(attribute.getNameOrOID(),
+                                attribute.getOID(), conflictingAttribute.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeAttributeType(conflictingAttribute);
+        }
+
+        numericOID2AttributeTypes.put(attribute.getOID(), attribute);
+        for (final String name : attribute.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<AttributeType> attrs = name2AttributeTypes.get(lowerName);
+            if (attrs == null) {
+                name2AttributeTypes.put(lowerName, Collections.singletonList(attribute));
+            } else if (attrs.size() == 1) {
+                attrs = new ArrayList<>(attrs);
+                attrs.add(attribute);
+                name2AttributeTypes.put(lowerName, attrs);
+            } else {
+                attrs.add(attribute);
+            }
+        }
+
+        return this;
+    }
+
+    SchemaBuilder addDITContentRule(final DITContentRule rule, final boolean overwrite) {
+        DITContentRule conflictingRule;
+        if (numericOID2ContentRules.containsKey(rule.getStructuralClassOID())) {
+            conflictingRule = numericOID2ContentRules.get(rule.getStructuralClassOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_DIT_CONTENT_RULE1.get(rule.getNameOrOID(), rule
+                                .getStructuralClassOID(), conflictingRule.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeDITContentRule(conflictingRule);
+        }
+
+        numericOID2ContentRules.put(rule.getStructuralClassOID(), rule);
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<DITContentRule> rules = name2ContentRules.get(lowerName);
+            if (rules == null) {
+                name2ContentRules.put(lowerName, Collections.singletonList(rule));
+            } else if (rules.size() == 1) {
+                rules = new ArrayList<>(rules);
+                rules.add(rule);
+                name2ContentRules.put(lowerName, rules);
+            } else {
+                rules.add(rule);
+            }
+        }
+
+        return this;
+    }
+
+    SchemaBuilder addDITStructureRule(final DITStructureRule rule, final boolean overwrite) {
+        DITStructureRule conflictingRule;
+        if (id2StructureRules.containsKey(rule.getRuleID())) {
+            conflictingRule = id2StructureRules.get(rule.getRuleID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_DIT_STRUCTURE_RULE_ID.get(rule.getNameOrRuleID(),
+                                rule.getRuleID(), conflictingRule.getNameOrRuleID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeDITStructureRule(conflictingRule);
+        }
+
+        id2StructureRules.put(rule.getRuleID(), rule);
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<DITStructureRule> rules = name2StructureRules.get(lowerName);
+            if (rules == null) {
+                name2StructureRules.put(lowerName, Collections.singletonList(rule));
+            } else if (rules.size() == 1) {
+                rules = new ArrayList<>(rules);
+                rules.add(rule);
+                name2StructureRules.put(lowerName, rules);
+            } else {
+                rules.add(rule);
+            }
+        }
+
+        return this;
+    }
+
+    SchemaBuilder addMatchingRuleUse(final MatchingRuleUse use, final boolean overwrite) {
+        MatchingRuleUse conflictingUse;
+        if (numericOID2MatchingRuleUses.containsKey(use.getMatchingRuleOID())) {
+            conflictingUse = numericOID2MatchingRuleUses.get(use.getMatchingRuleOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_MATCHING_RULE_USE.get(use.getNameOrOID(), use
+                                .getMatchingRuleOID(), conflictingUse.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeMatchingRuleUse(conflictingUse);
+        }
+
+        numericOID2MatchingRuleUses.put(use.getMatchingRuleOID(), use);
+        for (final String name : use.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName);
+            if (uses == null) {
+                name2MatchingRuleUses.put(lowerName, Collections.singletonList(use));
+            } else if (uses.size() == 1) {
+                uses = new ArrayList<>(uses);
+                uses.add(use);
+                name2MatchingRuleUses.put(lowerName, uses);
+            } else {
+                uses.add(use);
+            }
+        }
+
+        return this;
+    }
+
+    SchemaBuilder addMatchingRule(final MatchingRule rule, final boolean overwrite) {
+        Reject.ifTrue(rule.isValidated(),
+                "Matching rule has already been validated, it can't be added");
+        MatchingRule conflictingRule;
+        if (numericOID2MatchingRules.containsKey(rule.getOID())) {
+            conflictingRule = numericOID2MatchingRules.get(rule.getOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_MR_OID.get(rule.getNameOrOID(), rule.getOID(),
+                                conflictingRule.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeMatchingRule(conflictingRule);
+        }
+
+        numericOID2MatchingRules.put(rule.getOID(), rule);
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<MatchingRule> rules = name2MatchingRules.get(lowerName);
+            if (rules == null) {
+                name2MatchingRules.put(lowerName, Collections.singletonList(rule));
+            } else if (rules.size() == 1) {
+                rules = new ArrayList<>(rules);
+                rules.add(rule);
+                name2MatchingRules.put(lowerName, rules);
+            } else {
+                rules.add(rule);
+            }
+        }
+        return this;
+    }
+
+    SchemaBuilder addNameForm(final NameForm form, final boolean overwrite) {
+        NameForm conflictingForm;
+        if (numericOID2NameForms.containsKey(form.getOID())) {
+            conflictingForm = numericOID2NameForms.get(form.getOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_NAME_FORM_OID.get(form.getNameOrOID(),
+                                form.getOID(), conflictingForm.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeNameForm(conflictingForm);
+        }
+
+        numericOID2NameForms.put(form.getOID(), form);
+        for (final String name : form.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<NameForm> forms = name2NameForms.get(lowerName);
+            if (forms == null) {
+                name2NameForms.put(lowerName, Collections.singletonList(form));
+            } else if (forms.size() == 1) {
+                forms = new ArrayList<>(forms);
+                forms.add(form);
+                name2NameForms.put(lowerName, forms);
+            } else {
+                forms.add(form);
+            }
+        }
+        return this;
+    }
+
+    SchemaBuilder addObjectClass(final ObjectClass oc, final boolean overwrite) {
+        ObjectClass conflictingOC;
+        if (numericOID2ObjectClasses.containsKey(oc.getOID())) {
+            conflictingOC = numericOID2ObjectClasses.get(oc.getOID());
+            if (!overwrite) {
+                final LocalizableMessage message =
+                        ERR_SCHEMA_CONFLICTING_OBJECTCLASS_OID1.get(oc.getNameOrOID(), oc.getOID(),
+                                conflictingOC.getNameOrOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeObjectClass(conflictingOC);
+        }
+
+        numericOID2ObjectClasses.put(oc.getOID(), oc);
+        for (final String name : oc.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            List<ObjectClass> classes = name2ObjectClasses.get(lowerName);
+            if (classes == null) {
+                name2ObjectClasses.put(lowerName, Collections.singletonList(oc));
+            } else if (classes.size() == 1) {
+                classes = new ArrayList<>(classes);
+                classes.add(oc);
+                name2ObjectClasses.put(lowerName, classes);
+            } else {
+                classes.add(oc);
+            }
+        }
+
+        return this;
+    }
+
+    private void addSchema0(final Schema schema, final boolean overwrite) {
+        // All of the schema elements must be duplicated because validation will
+        // cause them to update all their internal references which, although
+        // unlikely, may be different in the new schema.
+
+        for (final Syntax syntax : schema.getSyntaxes()) {
+            buildSyntax(syntax).addToSchema(overwrite);
+        }
+
+        for (final MatchingRule matchingRule : schema.getMatchingRules()) {
+            buildMatchingRule(matchingRule).addToSchema(overwrite);
+        }
+
+        for (final MatchingRuleUse matchingRuleUse : schema.getMatchingRuleUses()) {
+            buildMatchingRuleUse(matchingRuleUse).addToSchema(overwrite);
+        }
+
+        for (final AttributeType attributeType : schema.getAttributeTypes()) {
+            buildAttributeType(attributeType).addToSchema(overwrite);
+        }
+
+        for (final ObjectClass objectClass : schema.getObjectClasses()) {
+            buildObjectClass(objectClass).addToSchema(overwrite);
+        }
+
+        for (final NameForm nameForm : schema.getNameForms()) {
+            buildNameForm(nameForm).addToSchema(overwrite);
+        }
+
+        for (final DITContentRule contentRule : schema.getDITContentRules()) {
+            buildDITContentRule(contentRule).addToSchema(overwrite);
+        }
+
+        for (final DITStructureRule structureRule : schema.getDITStuctureRules()) {
+            buildDITStructureRule(structureRule).addToSchema(overwrite);
+        }
+    }
+
+    SchemaBuilder addSyntax(final Syntax syntax, final boolean overwrite) {
+        Reject.ifTrue(syntax.isValidated(), "Syntax has already been validated, it can't be added");
+        Syntax conflictingSyntax;
+        if (numericOID2Syntaxes.containsKey(syntax.getOID())) {
+            conflictingSyntax = numericOID2Syntaxes.get(syntax.getOID());
+            if (!overwrite) {
+                final LocalizableMessage message = ERR_SCHEMA_CONFLICTING_SYNTAX_OID.get(syntax.toString(),
+                        syntax.getOID(), conflictingSyntax.getOID());
+                throw new ConflictingSchemaElementException(message);
+            }
+            removeSyntax(conflictingSyntax);
+        }
+
+        numericOID2Syntaxes.put(syntax.getOID(), syntax);
+        return this;
+    }
+
+    private void lazyInitBuilder() {
+        // Lazy initialization.
+        if (numericOID2Syntaxes == null) {
+            options = Options.defaultOptions();
+
+            numericOID2Syntaxes = new LinkedHashMap<>();
+            numericOID2MatchingRules = new LinkedHashMap<>();
+            numericOID2MatchingRuleUses = new LinkedHashMap<>();
+            numericOID2AttributeTypes = new LinkedHashMap<>();
+            numericOID2ObjectClasses = new LinkedHashMap<>();
+            numericOID2NameForms = new LinkedHashMap<>();
+            numericOID2ContentRules = new LinkedHashMap<>();
+            id2StructureRules = new LinkedHashMap<>();
+
+            name2MatchingRules = new LinkedHashMap<>();
+            name2MatchingRuleUses = new LinkedHashMap<>();
+            name2AttributeTypes = new LinkedHashMap<>();
+            name2ObjectClasses = new LinkedHashMap<>();
+            name2NameForms = new LinkedHashMap<>();
+            name2ContentRules = new LinkedHashMap<>();
+            name2StructureRules = new LinkedHashMap<>();
+
+            objectClass2NameForms = new HashMap<>();
+            nameForm2StructureRules = new HashMap<>();
+            name2OIDs = new HashMap<>();
+            warnings = new LinkedList<>();
+
+            if (copyOnWriteSchema != null) {
+                // Copy the schema.
+                addSchema0(copyOnWriteSchema, true);
+                options = Options.copyOf(copyOnWriteSchema.getOptions());
+                copyOnWriteSchema = null;
+            }
+        }
+    }
+
+    private void preLazyInitBuilder(final String schemaName, final Schema copyOnWriteSchema) {
+        this.schemaName = schemaName;
+        this.copyOnWriteSchema = copyOnWriteSchema;
+
+        this.options = null;
+
+        this.numericOID2Syntaxes = null;
+        this.numericOID2MatchingRules = null;
+        this.numericOID2MatchingRuleUses = null;
+        this.numericOID2AttributeTypes = null;
+        this.numericOID2ObjectClasses = null;
+        this.numericOID2NameForms = null;
+        this.numericOID2ContentRules = null;
+        this.id2StructureRules = null;
+
+        this.name2MatchingRules = null;
+        this.name2MatchingRuleUses = null;
+        this.name2AttributeTypes = null;
+        this.name2ObjectClasses = null;
+        this.name2NameForms = null;
+        this.name2ContentRules = null;
+        this.name2StructureRules = null;
+
+        this.objectClass2NameForms = null;
+        this.nameForm2StructureRules = null;
+        this.warnings = null;
+    }
+
+    private void removeAttributeType(final AttributeType attributeType) {
+        numericOID2AttributeTypes.remove(attributeType.getOID());
+        for (final String name : attributeType.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<AttributeType> attributes = name2AttributeTypes.get(lowerName);
+            if (attributes != null && attributes.contains(attributeType)) {
+                if (attributes.size() <= 1) {
+                    name2AttributeTypes.remove(lowerName);
+                } else {
+                    attributes.remove(attributeType);
+                }
+            }
+        }
+    }
+
+    private void removeDITContentRule(final DITContentRule rule) {
+        numericOID2ContentRules.remove(rule.getStructuralClassOID());
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<DITContentRule> rules = name2ContentRules.get(lowerName);
+            if (rules != null && rules.contains(rule)) {
+                if (rules.size() <= 1) {
+                    name2ContentRules.remove(lowerName);
+                } else {
+                    rules.remove(rule);
+                }
+            }
+        }
+    }
+
+    private void removeDITStructureRule(final DITStructureRule rule) {
+        id2StructureRules.remove(rule.getRuleID());
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<DITStructureRule> rules = name2StructureRules.get(lowerName);
+            if (rules != null && rules.contains(rule)) {
+                if (rules.size() <= 1) {
+                    name2StructureRules.remove(lowerName);
+                } else {
+                    rules.remove(rule);
+                }
+            }
+        }
+    }
+
+    private void removeMatchingRule(final MatchingRule rule) {
+        numericOID2MatchingRules.remove(rule.getOID());
+        for (final String name : rule.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<MatchingRule> rules = name2MatchingRules.get(lowerName);
+            if (rules != null && rules.contains(rule)) {
+                if (rules.size() <= 1) {
+                    name2MatchingRules.remove(lowerName);
+                } else {
+                    rules.remove(rule);
+                }
+            }
+        }
+    }
+
+    private void removeMatchingRuleUse(final MatchingRuleUse use) {
+        numericOID2MatchingRuleUses.remove(use.getMatchingRuleOID());
+        for (final String name : use.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName);
+            if (uses != null && uses.contains(use)) {
+                if (uses.size() <= 1) {
+                    name2MatchingRuleUses.remove(lowerName);
+                } else {
+                    uses.remove(use);
+                }
+            }
+        }
+    }
+
+    private void removeNameForm(final NameForm form) {
+        numericOID2NameForms.remove(form.getOID());
+        name2NameForms.remove(form.getOID());
+        for (final String name : form.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<NameForm> forms = name2NameForms.get(lowerName);
+            if (forms != null && forms.contains(form)) {
+                if (forms.size() <= 1) {
+                    name2NameForms.remove(lowerName);
+                } else {
+                    forms.remove(form);
+                }
+            }
+        }
+    }
+
+    private void removeObjectClass(final ObjectClass oc) {
+        numericOID2ObjectClasses.remove(oc.getOID());
+        name2ObjectClasses.remove(oc.getOID());
+        for (final String name : oc.getNames()) {
+            final String lowerName = StaticUtils.toLowerCase(name);
+            final List<ObjectClass> classes = name2ObjectClasses.get(lowerName);
+            if (classes != null && classes.contains(oc)) {
+                if (classes.size() <= 1) {
+                    name2ObjectClasses.remove(lowerName);
+                } else {
+                    classes.remove(oc);
+                }
+            }
+        }
+    }
+
+    private void removeSyntax(final Syntax syntax) {
+        for (Map.Entry<String, List<String>> property : syntax.getExtraProperties().entrySet()) {
+            if ("x-enum".equalsIgnoreCase(property.getKey())) {
+                removeMatchingRule(OMR_OID_GENERIC_ENUM + "." + syntax.getOID());
+                break;
+            }
+        }
+        numericOID2Syntaxes.remove(syntax.getOID());
+    }
+
+    private void validate(final Schema schema) {
+        // Verify all references in all elements
+        for (final Syntax syntax : numericOID2Syntaxes.values().toArray(
+                new Syntax[numericOID2Syntaxes.values().size()])) {
+            try {
+                syntax.validate(schema, warnings);
+                registerNameToOIDMapping(syntax.getName(), syntax.getOID());
+            } catch (final SchemaException e) {
+                removeSyntax(syntax);
+                warnings.add(ERR_SYNTAX_VALIDATION_FAIL
+                        .get(syntax.toString(), e.getMessageObject()));
+            }
+        }
+
+        for (final MatchingRule rule : numericOID2MatchingRules.values().toArray(
+                new MatchingRule[numericOID2MatchingRules.values().size()])) {
+            try {
+                rule.validate(schema, warnings);
+                for (final String name : rule.getNames()) {
+                    registerNameToOIDMapping(StaticUtils.toLowerCase(name), rule.getOID());
+                }
+            } catch (final SchemaException e) {
+                removeMatchingRule(rule);
+                warnings.add(ERR_MR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject()));
+            }
+        }
+
+        // Attribute types need special processing because they have
+        // hierarchical dependencies.
+        final List<AttributeType> invalidAttributeTypes = new LinkedList<>();
+        for (final AttributeType attributeType : numericOID2AttributeTypes.values()) {
+            attributeType.validate(schema, invalidAttributeTypes, warnings);
+        }
+
+        for (final AttributeType attributeType : invalidAttributeTypes) {
+            removeAttributeType(attributeType);
+        }
+
+        for (final AttributeType attributeType : numericOID2AttributeTypes.values()) {
+            for (final String name : attributeType.getNames()) {
+                registerNameToOIDMapping(StaticUtils.toLowerCase(name), attributeType.getOID());
+            }
+        }
+
+        // Object classes need special processing because they have hierarchical
+        // dependencies.
+        final List<ObjectClass> invalidObjectClasses = new LinkedList<>();
+        for (final ObjectClass objectClass : numericOID2ObjectClasses.values()) {
+            objectClass.validate(schema, invalidObjectClasses, warnings);
+        }
+
+        for (final ObjectClass objectClass : invalidObjectClasses) {
+            removeObjectClass(objectClass);
+        }
+
+        for (final ObjectClass objectClass : numericOID2ObjectClasses.values()) {
+            for (final String name : objectClass.getNames()) {
+                registerNameToOIDMapping(StaticUtils.toLowerCase(name), objectClass.getOID());
+            }
+        }
+        for (final MatchingRuleUse use : numericOID2MatchingRuleUses.values().toArray(
+                new MatchingRuleUse[numericOID2MatchingRuleUses.values().size()])) {
+            try {
+                use.validate(schema);
+                for (final String name : use.getNames()) {
+                    registerNameToOIDMapping(StaticUtils.toLowerCase(name), use.getMatchingRuleOID());
+                }
+            } catch (final SchemaException e) {
+                removeMatchingRuleUse(use);
+                warnings.add(ERR_MRU_VALIDATION_FAIL.get(use.toString(), e.getMessageObject()));
+            }
+        }
+
+        for (final NameForm form : numericOID2NameForms.values().toArray(
+                new NameForm[numericOID2NameForms.values().size()])) {
+            try {
+                form.validate(schema);
+
+                // build the objectClass2NameForms map
+                final String ocOID = form.getStructuralClass().getOID();
+                List<NameForm> forms = objectClass2NameForms.get(ocOID);
+                if (forms == null) {
+                    objectClass2NameForms.put(ocOID, Collections.singletonList(form));
+                } else if (forms.size() == 1) {
+                    forms = new ArrayList<>(forms);
+                    forms.add(form);
+                    objectClass2NameForms.put(ocOID, forms);
+                } else {
+                    forms.add(form);
+                }
+                for (final String name : form.getNames()) {
+                    registerNameToOIDMapping(StaticUtils.toLowerCase(name), form.getOID());
+                }
+            } catch (final SchemaException e) {
+                removeNameForm(form);
+                warnings.add(ERR_NAMEFORM_VALIDATION_FAIL
+                        .get(form.toString(), e.getMessageObject()));
+            }
+        }
+
+        for (final DITContentRule rule : numericOID2ContentRules.values().toArray(
+                new DITContentRule[numericOID2ContentRules.values().size()])) {
+            try {
+                rule.validate(schema, warnings);
+                for (final String name : rule.getNames()) {
+                    registerNameToOIDMapping(StaticUtils.toLowerCase(name), rule.getStructuralClassOID());
+                }
+            } catch (final SchemaException e) {
+                removeDITContentRule(rule);
+                warnings.add(ERR_DCR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject()));
+            }
+        }
+
+        // DIT structure rules need special processing because they have
+        // hierarchical dependencies.
+        final List<DITStructureRule> invalidStructureRules = new LinkedList<>();
+        for (final DITStructureRule rule : id2StructureRules.values()) {
+            rule.validate(schema, invalidStructureRules, warnings);
+        }
+
+        for (final DITStructureRule rule : invalidStructureRules) {
+            removeDITStructureRule(rule);
+        }
+
+        for (final DITStructureRule rule : id2StructureRules.values()) {
+            // build the nameForm2StructureRules map
+            final String ocOID = rule.getNameForm().getOID();
+            List<DITStructureRule> rules = nameForm2StructureRules.get(ocOID);
+            if (rules == null) {
+                nameForm2StructureRules.put(ocOID, Collections.singletonList(rule));
+            } else if (rules.size() == 1) {
+                rules = new ArrayList<>(rules);
+                rules.add(rule);
+                nameForm2StructureRules.put(ocOID, rules);
+            } else {
+                rules.add(rule);
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaConstants.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaConstants.java
new file mode 100644
index 0000000..c8c9411
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaConstants.java
@@ -0,0 +1,761 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 Manuel Gaupp
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * This class defines a number of constants used by Directory Server schema
+ * elements, like matching rules, syntaxes, attribute types, and objectclasses.
+ */
+final class SchemaConstants {
+    /**
+     * RFC 2251, Section 4.5.1: 'If the client does not want any attributes returned,
+     * it can specify a list containing only the attribute with OID "1.1".
+     * This OID was chosen arbitrarily and does not correspond to any attribute in use.'
+     *
+     * @see <a href="http://www.rfc-editor.org/rfc/rfc2251.txt">RFC 2251 for LDAP v3</a>
+     */
+    public static final String NO_ATTRIBUTES = "1.1";
+
+    /** The IANA-assigned base OID for all things under the OpenDS umbrella. */
+    public static final String OID_OPENDS_BASE = "1.3.6.1.4.1.26027";
+
+    /** The base OID that will be used for the OpenDS Directory Server project. */
+    public static final String OID_OPENDS_SERVER_BASE = OID_OPENDS_BASE + ".1";
+
+    /** The base OID that will be used for OpenDS Directory Server attribute type definitions. */
+    public static final String OID_OPENDS_SERVER_ATTRIBUTE_TYPE_BASE = OID_OPENDS_SERVER_BASE + ".1";
+    /** The base OID that will be used for OpenDS Directory Server object class definitions. */
+    public static final String OID_OPENDS_SERVER_OBJECT_CLASS_BASE = OID_OPENDS_SERVER_BASE + ".2";
+    /** The base OID that will be used for OpenDS Directory Server attribute syntax definitions. */
+    public static final String OID_OPENDS_SERVER_ATTRIBUTE_SYNTAX_BASE = OID_OPENDS_SERVER_BASE + ".3";
+    /** The base OID that will be used for OpenDS Directory Server matching rule definitions. */
+    public static final String OID_OPENDS_SERVER_MATCHING_RULE_BASE = OID_OPENDS_SERVER_BASE + ".4";
+    /** The base OID that will be used for OpenDS Directory Server control definitions. */
+    public static final String OID_OPENDS_SERVER_CONTROL_BASE = OID_OPENDS_SERVER_BASE + ".5";
+    /** The base OID that will be used for OpenDS Directory Server extended operation definitions. */
+    public static final String OID_OPENDS_SERVER_EXTENDED_OPERATION_BASE = OID_OPENDS_SERVER_BASE + ".6";
+
+    /**
+     * The base OID that will be used for general-purpose (i.e., "other") types
+     * of OIDs that need to be allocated for the OpenDS Directory Server.
+     */
+    public static final String OID_OPENDS_SERVER_GENERAL_USE_BASE = OID_OPENDS_SERVER_BASE + ".9";
+
+    /**
+     * The base OID that will be used for temporary or experimental OIDs within
+     * the OpenDS Directory Server.
+     */
+    public static final String OID_OPENDS_SERVER_EXPERIMENTAL_BASE = OID_OPENDS_SERVER_BASE + ".999";
+
+    /** The description for the doubleMetaphoneApproximateMatch approximate matching rule. */
+    public static final String AMR_DOUBLE_METAPHONE_DESCRIPTION =
+            "Double Metaphone Approximate Match";
+    /** The name for the doubleMetaphoneApproximateMatch approximate matching rule. */
+    public static final String AMR_DOUBLE_METAPHONE_NAME = "ds-mr-double-metaphone-approx";
+    /** The OID for the doubleMetaphoneApproximateMatch approximate matching rule. */
+    public static final String AMR_DOUBLE_METAPHONE_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE + ".1";
+
+    /** The description for the authPasswordExactMatch matching rule. */
+    public static final String EMR_AUTH_PASSWORD_EXACT_DESCRIPTION =
+            "authentication password exact matching rule";
+    /** The name for the authPasswordExactMatch equality matching rule. */
+    public static final String EMR_AUTH_PASSWORD_EXACT_NAME = "authPasswordExactMatch";
+    /** The OID for the authPasswordExactMatch equality matching rule. */
+    public static final String EMR_AUTH_PASSWORD_EXACT_OID = "1.3.6.1.4.1.4203.1.2.2";
+    /** The description for the authPasswordMatch matching rule. */
+    public static final String EMR_AUTH_PASSWORD_DESCRIPTION =
+            "authentication password matching rule";
+
+    /** The name for the authPasswordMatch equality matching rule. */
+    public static final String EMR_AUTH_PASSWORD_NAME = "authPasswordMatch";
+    /** The OID for the authPasswordMatch equality matching rule. */
+    public static final String EMR_AUTH_PASSWORD_OID = "1.3.6.1.4.1.4203.1.2.3";
+
+    /** The name for the bitStringMatch equality matching rule. */
+    public static final String EMR_BIT_STRING_NAME = "bitStringMatch";
+    /** The OID for the bitStringMatch equality matching rule. */
+    public static final String EMR_BIT_STRING_OID = "2.5.13.16";
+
+    /** The name for the booleanMatch equality matching rule. */
+    public static final String EMR_BOOLEAN_NAME = "booleanMatch";
+    /** The OID for the booleanMatch equality matching rule. */
+    public static final String EMR_BOOLEAN_OID = "2.5.13.13";
+
+    /** The name for the caseExactMatch equality matching rule. */
+    public static final String EMR_CASE_EXACT_NAME = "caseExactMatch";
+    /** The OID for the caseExactMatch equality matching rule. */
+    public static final String EMR_CASE_EXACT_OID = "2.5.13.5";
+
+    /** The name for the caseExactIA5Match equality matching rule. */
+    public static final String EMR_CASE_EXACT_IA5_NAME = "caseExactIA5Match";
+    /** The OID for the caseExactIA5Match equality matching rule. */
+    public static final String EMR_CASE_EXACT_IA5_OID = "1.3.6.1.4.1.1466.109.114.1";
+
+    /** The name for the caseIgnoreMatch equality matching rule. */
+    public static final String EMR_CASE_IGNORE_NAME = "caseIgnoreMatch";
+    /** The OID for the caseIgnoreMatch equality matching rule. */
+    public static final String EMR_CASE_IGNORE_OID = "2.5.13.2";
+
+    /** The name for the caseIgnoreIA5Match equality matching rule. */
+    public static final String EMR_CASE_IGNORE_IA5_NAME = "caseIgnoreIA5Match";
+    /** The OID for the caseIgnoreIA5Match equality matching rule. */
+    public static final String EMR_CASE_IGNORE_IA5_OID = "1.3.6.1.4.1.1466.109.114.2";
+
+    /** The name for the caseIgnoreListMatch equality matching rule. */
+    public static final String EMR_CASE_IGNORE_LIST_NAME = "caseIgnoreListMatch";
+    /** The OID for the caseIgnoreListMatch equality matching rule. */
+    public static final String EMR_CASE_IGNORE_LIST_OID = "2.5.13.11";
+
+    /** The name for the certificateExactMatch equality matching rule. */
+    public static final String EMR_CERTIFICATE_EXACT_NAME = "certificateExactMatch";
+    /** The OID for the certificateExactMatch equality matching rule. */
+    public static final String EMR_CERTIFICATE_EXACT_OID = "2.5.13.34";
+
+    /** The name for the directoryStringFirstComponentMatch equality matching rule. */
+    public static final String EMR_DIRECTORY_STRING_FIRST_COMPONENT_NAME =
+            "directoryStringFirstComponentMatch";
+    /** The OID for the directoryStringFirstComponentMatch equality matching rule. */
+    public static final String EMR_DIRECTORY_STRING_FIRST_COMPONENT_OID = "2.5.13.31";
+
+    /** The name for the distinguishedNameMatch equality matching rule. */
+    public static final String EMR_DN_NAME = "distinguishedNameMatch";
+    /** The OID for the distinguishedNameMatch equality matching rule. */
+    public static final String EMR_DN_OID = "2.5.13.1";
+
+    /** The name for the generalizedTimeMatch equality matching rule. */
+    public static final String EMR_GENERALIZED_TIME_NAME = "generalizedTimeMatch";
+    /** The OID for the generalizedTimeMatch equality matching rule. */
+    public static final String EMR_GENERALIZED_TIME_OID = "2.5.13.27";
+
+    /** The name for the integerMatch equality matching rule. */
+    public static final String EMR_INTEGER_NAME = "integerMatch";
+    /** The OID for the integerMatch equality matching rule. */
+    public static final String EMR_INTEGER_OID = "2.5.13.14";
+
+    /** The name for the integerFirstComponentMatch equality matching rule. */
+    public static final String EMR_INTEGER_FIRST_COMPONENT_NAME = "integerFirstComponentMatch";
+    /** The OID for the integerFirstComponentMatch equality matching rule. */
+    public static final String EMR_INTEGER_FIRST_COMPONENT_OID = "2.5.13.29";
+
+    /** The name for the keywordMatch equality matching rule. */
+    public static final String EMR_KEYWORD_NAME = "keywordMatch";
+    /** The OID for the keywordMatch equality matching rule. */
+    public static final String EMR_KEYWORD_OID = "2.5.13.33";
+
+    /** The name for the numericStringMatch equality matching rule. */
+    public static final String EMR_NUMERIC_STRING_NAME = "numericStringMatch";
+    /** The OID for the numericStringMatch equality matching rule. */
+    public static final String EMR_NUMERIC_STRING_OID = "2.5.13.8";
+
+    /** The name for the octetStringMatch equality matching rule. */
+    public static final String EMR_OCTET_STRING_NAME = "octetStringMatch";
+    /** The OID for the octetStringMatch equality matching rule. */
+    public static final String EMR_OCTET_STRING_OID = "2.5.13.17";
+
+    /** The name for the objectIdentifierMatch equality matching rule. */
+    public static final String EMR_OID_NAME = "objectIdentifierMatch";
+    /** The OID for the objectIdentifierMatch equality matching rule. */
+    public static final String EMR_OID_OID = "2.5.13.0";
+
+    /** The name for the objectIdentifierFirstComponentMatch equality matching rule. */
+    public static final String EMR_OID_FIRST_COMPONENT_NAME = "objectIdentifierFirstComponentMatch";
+    /** The OID for the objectIdentifierFirstComponentMatch equality matching rule. */
+    public static final String EMR_OID_FIRST_COMPONENT_OID = "2.5.13.30";
+
+    /** The name for the presentationAddressMatch equality matching rule. */
+    public static final String EMR_PRESENTATION_ADDRESS_NAME = "presentationAddressMatch";
+    /** The OID for the presentationAddressMatch equality matching rule. */
+    public static final String EMR_PRESENTATION_ADDRESS_OID = "2.5.13.22";
+
+    /** The name for the protocolInformationMatch equality matching rule. */
+    public static final String EMR_PROTOCOL_INFORMATION_NAME = "protocolInformationMatch";
+    /** The OID for the protocolInformationMatch equality matching rule. */
+    public static final String EMR_PROTOCOL_INFORMATION_OID = "2.5.13.24";
+
+    /** The name for the telephoneNumberMatch equality matching rule. */
+    public static final String EMR_TELEPHONE_NAME = "telephoneNumberMatch";
+    /** The OID for the telephoneNumberMatch equality matching rule. */
+    public static final String EMR_TELEPHONE_OID = "2.5.13.20";
+
+    /** The name for the uniqueMemberMatch equality matching rule. */
+    public static final String EMR_UNIQUE_MEMBER_NAME = "uniqueMemberMatch";
+    /** The OID for the uniqueMemberMatch equality matching rule. */
+    public static final String EMR_UNIQUE_MEMBER_OID = "2.5.13.23";
+
+    /** The description for the userPasswordExactMatch matching rule. */
+    public static final String EMR_USER_PASSWORD_EXACT_DESCRIPTION =
+            "user password exact matching rule";
+    /** The name for the userPasswordExactMatch equality matching rule. */
+    public static final String EMR_USER_PASSWORD_EXACT_NAME = "ds-mr-user-password-exact";
+    /** The OID for the userPasswordExactMatch equality matching rule. */
+    public static final String EMR_USER_PASSWORD_EXACT_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE + ".2";
+
+    /** The description for the userPasswordMatch matching rule. */
+    public static final String EMR_USER_PASSWORD_DESCRIPTION = "user password matching rule";
+    /** The name for the userPasswordMatch equality matching rule. */
+    public static final String EMR_USER_PASSWORD_NAME = "ds-mr-user-password-equality";
+    /** The OID for the userPasswordMatch equality matching rule. */
+    public static final String EMR_USER_PASSWORD_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE + ".3";
+
+    /** The name for the uuidMatch equality matching rule. */
+    public static final String EMR_UUID_NAME = "uuidMatch";
+    /** The OID for the uuidMatch equality matching rule. */
+    public static final String EMR_UUID_OID = "1.3.6.1.1.16.2";
+
+    /** The name for the wordMatch equality matching rule. */
+    public static final String EMR_WORD_NAME = "wordMatch";
+    /** The OID for the wordMatch equality matching rule. */
+    public static final String EMR_WORD_OID = "2.5.13.32";
+
+    /** The Description for the partialDateAndTimeMatchingRule ordering matching rule. */
+    public static final String MR_PARTIAL_DATE_AND_TIME_DESCRIPTION = "partial date and time matching";
+    /** The Name for the partialDateAndTimeMatchingRule ordering matching rule. */
+    public static final String MR_PARTIAL_DATE_AND_TIME_NAME = "partialDateAndTimeMatchingRule";
+    /** The OID for the partialDateAndTimeMatchingRule ordering matching rule. */
+    public static final String MR_PARTIAL_DATE_AND_TIME_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE  + ".7";
+
+    /** The name for the caseExactOrderingMatch ordering matching rule. */
+    public static final String OMR_CASE_EXACT_NAME = "caseExactOrderingMatch";
+    /** The OID for the caseExactOrderingMatch ordering matching rule. */
+    public static final String OMR_CASE_EXACT_OID = "2.5.13.6";
+
+    /** The name for the caseIgnoreOrderingMatch ordering matching rule. */
+    public static final String OMR_CASE_IGNORE_NAME = "caseIgnoreOrderingMatch";
+    /** The OID for the caseIgnoreOrderingMatch ordering matching rule. */
+    public static final String OMR_CASE_IGNORE_OID = "2.5.13.3";
+
+    /** The name for the generalizedTimeOrderingMatch ordering matching rule. */
+    public static final String OMR_GENERALIZED_TIME_NAME = "generalizedTimeOrderingMatch";
+    /** The OID for the generalizedTimeOrderingMatch ordering matching rule. */
+    public static final String OMR_GENERALIZED_TIME_OID = "2.5.13.28";
+
+    /** The name for the integerOrderingMatch ordering matching rule. */
+    public static final String OMR_INTEGER_NAME = "integerOrderingMatch";
+    /** The OID for the integerOrderingMatch ordering matching rule. */
+    public static final String OMR_INTEGER_OID = "2.5.13.15";
+
+    /** The name for the numericStringOrderingMatch ordering matching rule. */
+    public static final String OMR_NUMERIC_STRING_NAME = "numericStringOrderingMatch";
+    /** The OID for the numericStringOrderingMatch ordering matching rule. */
+    public static final String OMR_NUMERIC_STRING_OID = "2.5.13.9";
+
+    /** The name for the octetStringOrderingMatch ordering matching rule. */
+    public static final String OMR_OCTET_STRING_NAME = "octetStringOrderingMatch";
+    /** The OID for the octetStringOrderingMatch ordering matching rule. */
+    public static final String OMR_OCTET_STRING_OID = "2.5.13.18";
+
+    /** The Description for the relativeTimeGreaterThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_GREATER_THAN_DESCRIPTION =
+        "greater-than relative time for time-based searches";
+    /** The Name for the relativeTimeGreaterThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_GREATER_THAN_NAME = "relativeTimeGTOrderingMatch";
+    /** The alternative name for the relativeTimeGreaterThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_GREATER_THAN_ALT_NAME = "relativeTimeOrderingMatch.gt";
+    /** The OID for the relativeTimeGreaterThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_GREATER_THAN_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE  + ".5";
+
+    /** The Description for the relativeTimeLessThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_LESS_THAN_DESCRIPTION =
+        "less-than relative time for time-based searches";
+    /** The Name for the relativeTimeLessThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_LESS_THAN_NAME = "relativeTimeLTOrderingMatch";
+    /** The alternative name for the relativeTimeLessThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_LESS_THAN_ALT_NAME = "relativeTimeOrderingMatch.lt";
+    /** The OID for the relativeTimeLessThan ordering matching rule. */
+    public static final String OMR_RELATIVE_TIME_LESS_THAN_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE  + ".6";
+
+    /** The name for the uuidOrderingMatch ordering matching rule. */
+    public static final String OMR_UUID_NAME = "uuidOrderingMatch";
+    /** The OID for the uuidOrderingMatch ordering matching rule. */
+    public static final String OMR_UUID_OID = "1.3.6.1.1.16.3";
+
+    /** The name for the enumOrderingMatch ordering matching rule. */
+    public static final String OMR_GENERIC_ENUM_NAME = "enumOrderingMatch";
+    /** The oid for the generic enum syntax ordering matching rule. */
+    public static final String OMR_OID_GENERIC_ENUM = "1.3.6.1.4.1.26027.1.4.8";
+
+    /** The name for the caseExactSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_EXACT_NAME = "caseExactSubstringsMatch";
+    /** The OID for the caseExactSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_EXACT_OID = "2.5.13.7";
+
+    /** The name for the caseExactIA5SubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_EXACT_IA5_NAME = "caseExactIA5SubstringsMatch";
+    /**
+     * The OID for the caseExactIA5SubstringsMatch substring matching rule. //
+     * FIXME -- This needs to be updated once a real OID is assigned.
+     */
+    public static final String SMR_CASE_EXACT_IA5_OID = OID_OPENDS_SERVER_MATCHING_RULE_BASE + ".902";
+
+    /** The name for the caseIgnoreSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_NAME = "caseIgnoreSubstringsMatch";
+    /** The OID for the caseIgnoreSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_OID = "2.5.13.4";
+
+    /** The name for the caseIgnoreIA5SubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_IA5_NAME = "caseIgnoreIA5SubstringsMatch";
+    /** The OID for the caseIgnoreIA5SubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_IA5_OID = "1.3.6.1.4.1.1466.109.114.3";
+
+    /** The name for the caseIgnoreListSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_LIST_NAME = "caseIgnoreListSubstringsMatch";
+    /** The OID for the caseIgnoreListSubstringsMatch substring matching rule. */
+    public static final String SMR_CASE_IGNORE_LIST_OID = "2.5.13.12";
+
+    /** The name for the numericStringSubstringsMatch substring matching rule. */
+    public static final String SMR_NUMERIC_STRING_NAME = "numericStringSubstringsMatch";
+    /** The OID for the numericStringSubstringsMatch substring matching rule. */
+    public static final String SMR_NUMERIC_STRING_OID = "2.5.13.10";
+
+    /** The name for the octetStringSubstringsMatch substring matching rule. */
+    public static final String SMR_OCTET_STRING_NAME = "octetStringSubstringsMatch";
+    /** The OID for the octetStringSubstringsMatch substring matching rule. */
+    public static final String SMR_OCTET_STRING_OID = "2.5.13.19";
+
+    /** The name for the telephoneNumberSubstringsMatch substring matching rule. */
+    public static final String SMR_TELEPHONE_NAME = "telephoneNumberSubstringsMatch";
+    /** The OID for the telephoneNumberSubstringsMatch substring matching rule. */
+    public static final String SMR_TELEPHONE_OID = "2.5.13.21";
+
+    /** The OID for the absolute subtree specification attribute syntax. */
+    public static final String SYNTAX_ABSOLUTE_SUBTREE_SPECIFICATION_OID =
+            OID_OPENDS_SERVER_ATTRIBUTE_SYNTAX_BASE + ".3";
+    /** The description for the absolute subtree specification attribute syntax. */
+    public static final String SYNTAX_ABSOLUTE_SUBTREE_SPECIFICATION_DESCRIPTION =
+            "Absolute Subtree Specification";
+    /** The name for the absolute subtree specification attribute syntax. */
+    public static final String SYNTAX_ABSOLUTE_SUBTREE_SPECIFICATION_NAME =
+            "ds-absolute-subtree-specification";
+
+    /** The OID for the aci attribute syntax. */
+    public static final String SYNTAX_ACI_OID = OID_OPENDS_SERVER_ATTRIBUTE_SYNTAX_BASE + ".4";
+    /** The description for aci attribute syntax. */
+    public static final String SYNTAX_ACI_DESCRIPTION = "Sun-defined Access Control Information";
+    /** The name for the aci attribute syntax. */
+    public static final String SYNTAX_ACI_NAME = "ds-syntax-dseecompat-aci";
+
+    /** The description for the attribute type description attribute syntax. */
+    public static final String SYNTAX_ATTRIBUTE_TYPE_DESCRIPTION = "Attribute Type Description";
+    /** The name for the attribute type description attribute syntax. */
+    public static final String SYNTAX_ATTRIBUTE_TYPE_NAME = "AttributeTypeDescription";
+    /** The OID for the attribute type description attribute syntax. */
+    public static final String SYNTAX_ATTRIBUTE_TYPE_OID = "1.3.6.1.4.1.1466.115.121.1.3";
+
+    /** The description for the auth password attribute syntax. */
+    public static final String SYNTAX_AUTH_PASSWORD_DESCRIPTION = "Authentication Password Syntax";
+    /** The name for the auth password attribute syntax. */
+    public static final String SYNTAX_AUTH_PASSWORD_NAME = "AuthenticationPasswordSyntax";
+    /** The OID for the auth password attribute syntax. */
+    public static final String SYNTAX_AUTH_PASSWORD_OID = "1.3.6.1.4.1.4203.1.1.2";
+
+    /** The description for the binary attribute syntax. */
+    public static final String SYNTAX_BINARY_DESCRIPTION = "Binary";
+    /** The name for the binary attribute syntax. */
+    public static final String SYNTAX_BINARY_NAME = "Binary";
+    /** The OID for the binary attribute syntax. */
+    public static final String SYNTAX_BINARY_OID = "1.3.6.1.4.1.1466.115.121.1.5";
+
+    /** The description for the bit string attribute syntax. */
+    public static final String SYNTAX_BIT_STRING_DESCRIPTION = "Bit String";
+    /** The name for the bit string attribute syntax. */
+    public static final String SYNTAX_BIT_STRING_NAME = "BitString";
+    /** The OID for the bit string attribute syntax. */
+    public static final String SYNTAX_BIT_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.6";
+
+    /** The description for the Boolean attribute syntax. */
+    public static final String SYNTAX_BOOLEAN_DESCRIPTION = "Boolean";
+    /** The name for the Boolean attribute syntax. */
+    public static final String SYNTAX_BOOLEAN_NAME = "Boolean";
+    /** The OID for the Boolean attribute syntax. */
+    public static final String SYNTAX_BOOLEAN_OID = "1.3.6.1.4.1.1466.115.121.1.7";
+
+    /** The description for the certificate attribute syntax. */
+    public static final String SYNTAX_CERTIFICATE_DESCRIPTION = "Certificate";
+    /** The name for the certificate attribute syntax. */
+    public static final String SYNTAX_CERTIFICATE_NAME = "Certificate";
+    /** The OID for the certificate attribute syntax. */
+    public static final String SYNTAX_CERTIFICATE_OID = "1.3.6.1.4.1.1466.115.121.1.8";
+
+    /** The description for the certificate exact assertion attribute syntax. */
+    public static final String SYNTAX_CERTIFICATE_EXACT_ASSERTION_DESCRIPTION = "X.509 Certificate Exact Assertion";
+    /** The name for the certificate exact assertion attribute syntax. */
+    public static final String SYNTAX_CERTIFICATE_EXACT_ASSERTION_NAME = "CertificateExactAssertion";
+    /**
+     * The OID for the Certificate Exact Assertion syntax used for assertion
+     * values in extensible match filters.
+     */
+    public static final String SYNTAX_CERTIFICATE_EXACT_ASSERTION_OID = "1.3.6.1.1.15.1";
+
+    /** The description for the certificate list attribute syntax. */
+    public static final String SYNTAX_CERTLIST_DESCRIPTION = "Certificate List";
+    /** The name for the certificate list attribute syntax. */
+    public static final String SYNTAX_CERTLIST_NAME = "CertificateList";
+    /** The OID for the certificate list attribute syntax. */
+    public static final String SYNTAX_CERTLIST_OID = "1.3.6.1.4.1.1466.115.121.1.9";
+
+    /** The description for the certificate pair attribute syntax. */
+    public static final String SYNTAX_CERTPAIR_DESCRIPTION = "Certificate Pair";
+    /** The name for the certificate pair attribute syntax. */
+    public static final String SYNTAX_CERTPAIR_NAME = "CertificatePair";
+    /** The OID for the certificate pair attribute syntax. */
+    public static final String SYNTAX_CERTPAIR_OID = "1.3.6.1.4.1.1466.115.121.1.10";
+
+    /** The description for the country string attribute syntax. */
+    public static final String SYNTAX_COUNTRY_STRING_DESCRIPTION = "Country String";
+    /** The name for the country string attribute syntax. */
+    public static final String SYNTAX_COUNTRY_STRING_NAME = "CountryString";
+    /** The OID for the country string attribute syntax. */
+    public static final String SYNTAX_COUNTRY_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.11";
+
+    /** The description for the delivery method attribute syntax. */
+    public static final String SYNTAX_DELIVERY_METHOD_DESCRIPTION = "Delivery Method";
+    /** The name for the delivery method attribute syntax. */
+    public static final String SYNTAX_DELIVERY_METHOD_NAME = "DeliveryMethod";
+    /** The OID for the delivery method attribute syntax. */
+    public static final String SYNTAX_DELIVERY_METHOD_OID = "1.3.6.1.4.1.1466.115.121.1.14";
+
+    /** The description for the Directory String attribute syntax. */
+    public static final String SYNTAX_DIRECTORY_STRING_DESCRIPTION = "Directory String";
+    /** The name for the Directory String attribute syntax. */
+    public static final String SYNTAX_DIRECTORY_STRING_NAME = "DirectoryString";
+    /** The OID for the Directory String attribute syntax. */
+    public static final String SYNTAX_DIRECTORY_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.15";
+
+    /** The description for the DIT content rule description attribute syntax. */
+    public static final String SYNTAX_DIT_CONTENT_RULE_DESCRIPTION = "DIT Content Rule Description";
+    /** The name for the DIT content rule description attribute syntax. */
+    public static final String SYNTAX_DIT_CONTENT_RULE_NAME = "DITContentRuleDescription";
+    /** The OID for the DIT content rule description attribute syntax. */
+    public static final String SYNTAX_DIT_CONTENT_RULE_OID = "1.3.6.1.4.1.1466.115.121.1.16";
+
+    /** The description for the DIT structure rule description attribute syntax. */
+    public static final String SYNTAX_DIT_STRUCTURE_RULE_DESCRIPTION =
+            "DIT Structure Rule Description";
+    /** The name for the DIT structure rule description attribute syntax. */
+    public static final String SYNTAX_DIT_STRUCTURE_RULE_NAME = "DITStructureRuleDescription";
+    /** The OID for the DIT structure rule description attribute syntax. */
+    public static final String SYNTAX_DIT_STRUCTURE_RULE_OID = "1.3.6.1.4.1.1466.115.121.1.17";
+
+    /** The description for the distinguished name attribute syntax. */
+    public static final String SYNTAX_DN_DESCRIPTION = "DN";
+    /** The name for the distinguished name attribute syntax. */
+    public static final String SYNTAX_DN_NAME = "DN";
+    /** The OID for the distinguished name attribute syntax. */
+    public static final String SYNTAX_DN_OID = "1.3.6.1.4.1.1466.115.121.1.12";
+
+    /** The description for the enhanced guide attribute syntax. */
+    public static final String SYNTAX_ENHANCED_GUIDE_DESCRIPTION = "Enhanced Guide";
+    /** The name for the enhanced guide attribute syntax. */
+    public static final String SYNTAX_ENHANCED_GUIDE_NAME = "EnhancedGuide";
+    /** The OID for the enhanced guide attribute syntax. */
+    public static final String SYNTAX_ENHANCED_GUIDE_OID = "1.3.6.1.4.1.1466.115.121.1.21";
+
+    /** The description for the facsimile telephone number attribute syntax. */
+    public static final String SYNTAX_FAXNUMBER_DESCRIPTION = "Facsimile Telephone Number";
+    /** The name for the facsimile telephone number attribute syntax. */
+    public static final String SYNTAX_FAXNUMBER_NAME = "FacsimileTelephoneNumber";
+    /** The OID for the facsimile telephone number attribute syntax. */
+    public static final String SYNTAX_FAXNUMBER_OID = "1.3.6.1.4.1.1466.115.121.1.22";
+
+    /** The description for the fax attribute syntax. */
+    public static final String SYNTAX_FAX_DESCRIPTION = "Fax";
+    /** The name for the fax attribute syntax. */
+    public static final String SYNTAX_FAX_NAME = "Fax";
+    /** The OID for the fax attribute syntax. */
+    public static final String SYNTAX_FAX_OID = "1.3.6.1.4.1.1466.115.121.1.23";
+
+    /** The description for the generalized time attribute syntax. */
+    public static final String SYNTAX_GENERALIZED_TIME_DESCRIPTION = "Generalized Time";
+    /** The name for the generalized time attribute syntax. */
+    public static final String SYNTAX_GENERALIZED_TIME_NAME = "GeneralizedTime";
+    /** The OID for the generalized time attribute syntax. */
+    public static final String SYNTAX_GENERALIZED_TIME_OID = "1.3.6.1.4.1.1466.115.121.1.24";
+
+    /** The description for the guide attribute syntax. */
+    public static final String SYNTAX_GUIDE_DESCRIPTION = "Guide";
+    /** The name for the guide attribute syntax. */
+    public static final String SYNTAX_GUIDE_NAME = "Guide";
+    /** The OID for the guide attribute syntax. */
+    public static final String SYNTAX_GUIDE_OID = "1.3.6.1.4.1.1466.115.121.1.25";
+
+    /** The description for the IA5 string attribute syntax. */
+    public static final String SYNTAX_IA5_STRING_DESCRIPTION = "IA5 String";
+    /** The name for the IA5 string attribute syntax. */
+    public static final String SYNTAX_IA5_STRING_NAME = "IA5String";
+    /** The OID for the IA5 string attribute syntax. */
+    public static final String SYNTAX_IA5_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.26";
+
+    /** The description for the integer attribute syntax. */
+    public static final String SYNTAX_INTEGER_DESCRIPTION = "Integer";
+    /** The name for the integer attribute syntax. */
+    public static final String SYNTAX_INTEGER_NAME = "Integer";
+    /** The OID for the integer attribute syntax. */
+    public static final String SYNTAX_INTEGER_OID = "1.3.6.1.4.1.1466.115.121.1.27";
+
+    /** The description for the JPEG attribute syntax. */
+    public static final String SYNTAX_JPEG_DESCRIPTION = "JPEG";
+    /** The name for the JPEG attribute syntax. */
+    public static final String SYNTAX_JPEG_NAME = "JPEG";
+    /** The OID for the JPEG attribute syntax. */
+    public static final String SYNTAX_JPEG_OID = "1.3.6.1.4.1.1466.115.121.1.28";
+
+    /** The description for the LDAP syntax description attribute syntax. */
+    public static final String SYNTAX_LDAP_SYNTAX_DESCRIPTION = "LDAP Syntax Description";
+    /** The name for the LDAP syntax description attribute syntax. */
+    public static final String SYNTAX_LDAP_SYNTAX_NAME = "LDAPSyntaxDescription";
+    /** The OID for the LDAP syntax description attribute syntax. */
+    public static final String SYNTAX_LDAP_SYNTAX_OID = "1.3.6.1.4.1.1466.115.121.1.54";
+
+    /** The description for the matching rule description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_DESCRIPTION = "Matching Rule Description";
+    /** The name for the matching rule description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_NAME = "MatchingRuleDescription";
+    /** The OID for the matching rule description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_OID = "1.3.6.1.4.1.1466.115.121.1.30";
+
+    /** The description for the matching rule use description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_USE_DESCRIPTION =
+            "Matching Rule Use Description";
+    /** The name for the matching rule use description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_USE_NAME = "MatchingRuleUseDescription";
+    /** The OID for the matching rule use description attribute syntax. */
+    public static final String SYNTAX_MATCHING_RULE_USE_OID = "1.3.6.1.4.1.1466.115.121.1.31";
+
+    /** The description for the name and optional uid attribute syntax. */
+    public static final String SYNTAX_NAME_AND_OPTIONAL_UID_DESCRIPTION = "Name and Optional UID";
+    /** The name for the name and optional uid attribute syntax. */
+    public static final String SYNTAX_NAME_AND_OPTIONAL_UID_NAME = "NameAndOptionalUID";
+    /** The OID for the name and optional uid attribute syntax. */
+    public static final String SYNTAX_NAME_AND_OPTIONAL_UID_OID = "1.3.6.1.4.1.1466.115.121.1.34";
+
+    /** The description for the name form description attribute syntax. */
+    public static final String SYNTAX_NAME_FORM_DESCRIPTION = "Name Form Description";
+    /** The name for the name form description attribute syntax. */
+    public static final String SYNTAX_NAME_FORM_NAME = "NameFormDescription";
+    /** The OID for the name form description attribute syntax. */
+    public static final String SYNTAX_NAME_FORM_OID = "1.3.6.1.4.1.1466.115.121.1.35";
+
+    /** The description for the numeric string attribute syntax. */
+    public static final String SYNTAX_NUMERIC_STRING_DESCRIPTION = "Numeric String";
+    /** The name for the numeric string attribute syntax. */
+    public static final String SYNTAX_NUMERIC_STRING_NAME = "NumericString";
+    /** The OID for the numeric string attribute syntax. */
+    public static final String SYNTAX_NUMERIC_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.36";
+
+    /** The description for the object class description attribute syntax. */
+    public static final String SYNTAX_OBJECTCLASS_DESCRIPTION = "Object Class Description";
+    /** The name for the object class description attribute syntax. */
+    public static final String SYNTAX_OBJECTCLASS_NAME = "ObjectClassDescription";
+    /** The OID for the object class description attribute syntax. */
+    public static final String SYNTAX_OBJECTCLASS_OID = "1.3.6.1.4.1.1466.115.121.1.37";
+
+    /** The description for the octet string attribute syntax. */
+    public static final String SYNTAX_OCTET_STRING_DESCRIPTION = "Octet String";
+    /** The name for the octet string attribute syntax. */
+    public static final String SYNTAX_OCTET_STRING_NAME = "OctetString";
+    /** The OID for the octet string attribute syntax. */
+    public static final String SYNTAX_OCTET_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.40";
+
+    /** The description for the object identifier attribute syntax. */
+    public static final String SYNTAX_OID_DESCRIPTION = "OID";
+    /** The name for the object identifier attribute syntax. */
+    public static final String SYNTAX_OID_NAME = "OID";
+    /** The OID for the object identifier attribute syntax. */
+    public static final String SYNTAX_OID_OID = "1.3.6.1.4.1.1466.115.121.1.38";
+
+    /** The description for the other mailbox attribute syntax. */
+    public static final String SYNTAX_OTHER_MAILBOX_DESCRIPTION = "Other Mailbox";
+    /** The name for the other mailbox attribute syntax. */
+    public static final String SYNTAX_OTHER_MAILBOX_NAME = "OtherMailbox";
+    /** The OID for the other mailbox attribute syntax. */
+    public static final String SYNTAX_OTHER_MAILBOX_OID = "1.3.6.1.4.1.1466.115.121.1.39";
+
+    /** The description for the postal address attribute syntax. */
+    public static final String SYNTAX_POSTAL_ADDRESS_DESCRIPTION = "Postal Address";
+    /** The name for the postal address attribute syntax. */
+    public static final String SYNTAX_POSTAL_ADDRESS_NAME = "PostalAddress";
+    /** The OID for the postal address attribute syntax. */
+    public static final String SYNTAX_POSTAL_ADDRESS_OID = "1.3.6.1.4.1.1466.115.121.1.41";
+
+    /** The description for the presentation address attribute syntax. */
+    public static final String SYNTAX_PRESENTATION_ADDRESS_DESCRIPTION = "Presentation Address";
+    /** The name for the presentation address attribute syntax. */
+    public static final String SYNTAX_PRESENTATION_ADDRESS_NAME = "PresentationAddress";
+    /** The OID for the presentation address attribute syntax. */
+    public static final String SYNTAX_PRESENTATION_ADDRESS_OID = "1.3.6.1.4.1.1466.115.121.1.43";
+
+    /** The description for the printable string attribute syntax. */
+    public static final String SYNTAX_PRINTABLE_STRING_DESCRIPTION = "Printable String";
+    /** The name for the printable string attribute syntax. */
+    public static final String SYNTAX_PRINTABLE_STRING_NAME = "PrintableString";
+    /** The OID for the printable string attribute syntax. */
+    public static final String SYNTAX_PRINTABLE_STRING_OID = "1.3.6.1.4.1.1466.115.121.1.44";
+
+    /** The description for the protocol information attribute syntax. */
+    public static final String SYNTAX_PROTOCOL_INFORMATION_DESCRIPTION = "Protocol Information";
+    /** The name for the protocol information attribute syntax. */
+    public static final String SYNTAX_PROTOCOL_INFORMATION_NAME = "ProtocolInformation";
+    /** The OID for the protocol information attribute syntax. */
+    public static final String SYNTAX_PROTOCOL_INFORMATION_OID = "1.3.6.1.4.1.1466.115.121.1.42";
+
+    /** The OID for the relative subtree specification attribute syntax. */
+    public static final String SYNTAX_RELATIVE_SUBTREE_SPECIFICATION_OID =
+            OID_OPENDS_SERVER_ATTRIBUTE_SYNTAX_BASE + ".2";
+    /** The description for the relative subtree specification attribute syntax. */
+    public static final String SYNTAX_RELATIVE_SUBTREE_SPECIFICATION_DESCRIPTION =
+            "Relative Subtree Specification";
+    /** The name for the relative subtree specification attribute syntax. */
+    public static final String SYNTAX_RELATIVE_SUBTREE_SPECIFICATION_NAME =
+            "ds-relative-subtree-specification";
+
+    /** The OID for the RFC3672 subtree specification attribute syntax. */
+    public static final String SYNTAX_RFC3672_SUBTREE_SPECIFICATION_OID =
+            "1.3.6.1.4.1.1466.115.121.1.45";
+    /** The description for the RFC3672 subtree specification attribute syntax. */
+    public static final String SYNTAX_RFC3672_SUBTREE_SPECIFICATION_DESCRIPTION =
+            "RFC3672 Subtree Specification";
+    /** The name for the RFC3672 subtree specification attribute syntax. */
+    public static final String SYNTAX_RFC3672_SUBTREE_SPECIFICATION_NAME = "SubtreeSpecification";
+
+    /** The description for the substring assertion attribute syntax. */
+    public static final String SYNTAX_SUBSTRING_ASSERTION_DESCRIPTION = "Substring Assertion";
+    /** The name for the substring assertion attribute syntax. */
+    public static final String SYNTAX_SUBSTRING_ASSERTION_NAME = "SubstringAssertion";
+    /**
+     * The OID for the Substring Assertion syntax used for assertion values in
+     * extensible match filters.
+     */
+    public static final String SYNTAX_SUBSTRING_ASSERTION_OID = "1.3.6.1.4.1.1466.115.121.1.58";
+
+    /** The description for the supported algorithm attribute syntax. */
+    public static final String SYNTAX_SUPPORTED_ALGORITHM_DESCRIPTION = "Supported Algorithm";
+    /** The name for the supported algorithm attribute syntax. */
+    public static final String SYNTAX_SUPPORTED_ALGORITHM_NAME = "SupportedAlgorithm";
+    /**
+     * The OID for the Substring Assertion syntax used for assertion values in
+     * extensible match filters.
+     */
+    public static final String SYNTAX_SUPPORTED_ALGORITHM_OID = "1.3.6.1.4.1.1466.115.121.1.49";
+
+    /** The description for the telephone number attribute syntax. */
+    public static final String SYNTAX_TELEPHONE_DESCRIPTION = "Telephone Number";
+    /** The name for the telephone number attribute syntax. */
+    public static final String SYNTAX_TELEPHONE_NAME = "TelephoneNumber";
+    /** The OID for the telephone number attribute syntax. */
+    public static final String SYNTAX_TELEPHONE_OID = "1.3.6.1.4.1.1466.115.121.1.50";
+
+    /** The description for the teletex terminal identifier attribute syntax. */
+    public static final String SYNTAX_TELETEX_TERM_ID_DESCRIPTION = "Teletex Terminal Identifier";
+    /** The name for the teletex terminal identifier attribute syntax. */
+    public static final String SYNTAX_TELETEX_TERM_ID_NAME = "TeletexTerminalIdentifier";
+    /** The OID for the teletex terminal identifier attribute syntax. */
+    public static final String SYNTAX_TELETEX_TERM_ID_OID = "1.3.6.1.4.1.1466.115.121.1.51";
+
+    /** The description for the telex number attribute syntax. */
+    public static final String SYNTAX_TELEX_DESCRIPTION = "Telex Number";
+    /** The name for the telex number attribute syntax. */
+    public static final String SYNTAX_TELEX_NAME = "TelexNumber";
+    /** The OID for the telex number attribute syntax. */
+    public static final String SYNTAX_TELEX_OID = "1.3.6.1.4.1.1466.115.121.1.52";
+
+    /** The description for the user password attribute syntax. */
+    public static final String SYNTAX_USER_PASSWORD_DESCRIPTION = "User Password";
+    /** The name for the user password attribute syntax. */
+    public static final String SYNTAX_USER_PASSWORD_NAME = "ds-syntax-user-password";
+    /** The OID for the user password attribute syntax. */
+    public static final String SYNTAX_USER_PASSWORD_OID = OID_OPENDS_SERVER_ATTRIBUTE_SYNTAX_BASE + ".1";
+
+    /** The description for the UTC time attribute syntax. */
+    public static final String SYNTAX_UTC_TIME_DESCRIPTION = "UTC Time";
+    /** The name for the UTC time attribute syntax. */
+    public static final String SYNTAX_UTC_TIME_NAME = "UTCTime";
+    /** The OID for the UTC time attribute syntax. */
+    public static final String SYNTAX_UTC_TIME_OID = "1.3.6.1.4.1.1466.115.121.1.53";
+
+    /** The description for the UUID attribute syntax. */
+    public static final String SYNTAX_UUID_DESCRIPTION = "UUID";
+    /** The name for the UUID attribute syntax. */
+    public static final String SYNTAX_UUID_NAME = "UUID";
+    /** The OID for the UUID attribute syntax. */
+    public static final String SYNTAX_UUID_OID = "1.3.6.1.1.16.1";
+
+    /** The description for the "top" objectclass. */
+    public static final String TOP_OBJECTCLASS_DESCRIPTION = "Topmost ObjectClass";
+    /** The name of the "top" objectclass. */
+    public static final String TOP_OBJECTCLASS_NAME = "top";
+    /** The OID for the "top" objectclass. */
+    public static final String TOP_OBJECTCLASS_OID = "2.5.6.0";
+
+    /** The name for the relative time greater-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_GT_NAME = "relativeTimeGTOrderingMatch";
+    /** The alternative name for the relative time greater-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_GT_ALT_NAME = "relativeTimeOrderingMatch.gt";
+    /** The OID for the relative time greater-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_GT_OID = "1.3.6.1.4.1.26027.1.4.5";
+
+    /** The name for the relative time less-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_LT_NAME = "relativeTimeLTOrderingMatch";
+    /** The alternative name for the relative time less-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_LT_ALT_NAME = "relativeTimeOrderingMatch.lt";
+    /** The OID for the relative time less-than extensible ordering matching rule. */
+    public static final String EXT_OMR_RELATIVE_TIME_LT_OID = "1.3.6.1.4.1.26027.1.4.6";
+
+    /** The OID for the partial date and time extensible matching rule. */
+    public static final String EXT_PARTIAL_DATE_TIME_OID = "1.3.6.1.4.1.26027.1.4.7";
+    /** The name for the partial date and time extensible rule. */
+    public static final String EXT_PARTIAL_DATE_TIME_NAME = "partialDateAndTimeMatchingRule";
+
+    /**
+     * The name of the schema extension that will be used to specify the
+     * approximate matching rule that should be used for a given attribute type.
+     */
+    public static final String SCHEMA_PROPERTY_APPROX_RULE = "X-APPROX";
+
+    /** The name of the schema property that will be used to specify the origin of a schema element. */
+    public static final String SCHEMA_PROPERTY_ORIGIN = "X-ORIGIN";
+
+    /** The OID for the extensibleObject objectclass. */
+    public static final String EXTENSIBLE_OBJECT_OBJECTCLASS_OID = "1.3.6.1.4.1.1466.101.120.111";
+    /** The name for the extensibleObject objectclass. */
+    public static final String EXTENSIBLE_OBJECT_OBJECTCLASS_NAME = "extensibleObject";
+
+    /** The value representing just one space character. */
+    public static final ByteString SINGLE_SPACE_VALUE = ByteString.valueOfUtf8(" ");
+
+    /** The normalized true value. */
+    public static final ByteString TRUE_VALUE = ByteString.valueOfUtf8("TRUE");
+    /** The normalized false value. */
+    public static final ByteString FALSE_VALUE = ByteString.valueOfUtf8("FALSE");
+
+    /** The name of the time zone for universal coordinated time (UTC). */
+    public static final String TIME_ZONE_UTC = "UTC";
+
+    /**
+     * The date format string that will be used to construct and parse dates
+     * represented using generalized time with a two-digit year. It is assumed
+     * that the provided date formatter will be set to UTC.
+     */
+    public static final String DATE_FORMAT_UTC_TIME = "yyMMddHHmmss'Z'";
+
+    /** Prevent instantiation. */
+    private SchemaConstants() {
+        // Nothing to do.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaElement.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaElement.java
new file mode 100644
index 0000000..c1f2eb7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaElement.java
@@ -0,0 +1,299 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaUtils.unmodifiableCopyOfExtraProperties;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An abstract base class for LDAP schema definitions which contain an
+ * description, and an optional set of extra properties.
+ * <p>
+ * This class defines common properties and behaviour of the various types of
+ * schema definitions (e.g. object class definitions, and attribute type
+ * definitions).
+ */
+abstract class SchemaElement {
+    static abstract class SchemaElementBuilder<T extends SchemaElementBuilder<T>> {
+        private String definition;
+        private String description;
+        private final Map<String, List<String>> extraProperties;
+        private final SchemaBuilder schemaBuilder;
+
+        SchemaElementBuilder(final SchemaBuilder schemaBuilder) {
+            this.schemaBuilder = schemaBuilder;
+            this.description = "";
+            this.extraProperties = new LinkedHashMap<>(1);
+        }
+
+        SchemaElementBuilder(final SchemaBuilder schemaBuilder, final SchemaElement copy) {
+            this.schemaBuilder = schemaBuilder;
+            this.description = copy.description;
+            this.extraProperties = new LinkedHashMap<>(copy.extraProperties);
+        }
+
+        /*
+         * The abstract methods in this class are required in order to obtain
+         * meaningful Javadoc. If the methods were defined in this class then
+         * the resulting Javadoc in sub-class is invalid. The only workaround is
+         * to make the methods abstract, provide "xxx0" implementations, and
+         * override the abstract methods in sub-classes as delegates to the
+         * "xxx0" methods. Ghastly! Thanks Javadoc.
+         */
+
+        /**
+         * Sets the description.
+         *
+         * @param description
+         *            The description, which may be {@code null} in which case
+         *            the empty string will be used.
+         * @return This builder.
+         */
+        public abstract T description(final String description);
+
+        /**
+         * Adds the provided collection of extended properties.
+         *
+         * @param extraProperties
+         *            The collection of extended properties.
+         * @return This builder.
+         */
+        public abstract T extraProperties(final Map<String, List<String>> extraProperties);
+
+        /**
+         * Adds the provided extended property.
+         *
+         * @param extensionName
+         *            The name of the extended property.
+         * @param extensionValues
+         *            The optional list of values for the extended property.
+         * @return This builder.
+         */
+        public abstract T extraProperties(final String extensionName, final String... extensionValues);
+
+        /**
+         * Adds the provided extended property.
+         *
+         * @param extensionName
+         *            The name of the extended property.
+         * @param extensionValues
+         *            The optional list of values for the extended property.
+         * @return This builder.
+         */
+        public T extraProperties(final String extensionName, final List<String> extensionValues) {
+            return extraProperties(extensionName, extensionValues.toArray(new String[extensionValues.size()]));
+        }
+
+        /**
+         * Removes all extra properties.
+         *
+         * @return This builder.
+         */
+        public abstract T removeAllExtraProperties();
+
+        /**
+         * Removes the specified extended property.
+         *
+         * @param extensionName
+         *            The name of the extended property.
+         * @param extensionValues
+         *            The optional list of values for the extended property,
+         *            which may be empty indicating that the entire property
+         *            should be removed.
+         * @return This builder.
+         */
+        public abstract T removeExtraProperty(final String extensionName,
+                final String... extensionValues);
+
+        T definition(final String definition) {
+            this.definition = definition;
+            return getThis();
+        }
+
+        T description0(final String description) {
+            this.description = description == null ? "" : description;
+            return getThis();
+        }
+
+        T extraProperties0(final Map<String, List<String>> extraProperties) {
+            this.extraProperties.putAll(extraProperties);
+            return getThis();
+        }
+
+        T extraProperties0(final String extensionName, final String... extensionValues) {
+            if (this.extraProperties.get(extensionName) != null) {
+                final List<String> tempExtraProperties =
+                        new ArrayList<>(this.extraProperties.get(extensionName));
+                tempExtraProperties.addAll(Arrays.asList(extensionValues));
+                this.extraProperties.put(extensionName, tempExtraProperties);
+            } else {
+                this.extraProperties.put(extensionName, Arrays.asList(extensionValues));
+            }
+            return getThis();
+        }
+
+        String getDescription() {
+            return description;
+        }
+
+        Map<String, List<String>> getExtraProperties() {
+            return extraProperties;
+        }
+
+        SchemaBuilder getSchemaBuilder() {
+            return schemaBuilder;
+        }
+
+        abstract T getThis();
+
+        T removeAllExtraProperties0() {
+            this.extraProperties.clear();
+            return getThis();
+        }
+
+        T removeExtraProperty0(final String extensionName, final String... extensionValues) {
+            if (this.extraProperties.get(extensionName) != null && extensionValues.length > 0) {
+                final List<String> tempExtraProperties =
+                        new ArrayList<>(this.extraProperties.get(extensionName));
+                tempExtraProperties.removeAll(Arrays.asList(extensionValues));
+                this.extraProperties.put(extensionName, tempExtraProperties);
+            } else if (this.extraProperties.get(extensionName) != null) {
+                this.extraProperties.remove(extensionName);
+            }
+            return getThis();
+        }
+    }
+
+    /** Lazily created string representation. */
+    private String definition;
+
+    /** The description for this definition. */
+    private final String description;
+
+    /** The set of additional name-value pairs. */
+    private final Map<String, List<String>> extraProperties;
+
+    SchemaElement() {
+        this.description = "";
+        this.extraProperties = Collections.<String, List<String>> emptyMap();
+        this.definition = null;
+    }
+
+    SchemaElement(final SchemaElementBuilder<?> builder) {
+        this.description = builder.description;
+        this.extraProperties = unmodifiableCopyOfExtraProperties(builder.extraProperties);
+        this.definition = builder.definition;
+    }
+
+    SchemaElement(final String description, final Map<String, List<String>> extraProperties,
+            final String definition) {
+        Reject.ifNull(description, extraProperties);
+        this.description = description;
+        this.extraProperties = extraProperties; // Should already be unmodifiable.
+        this.definition = definition;
+    }
+
+    @Override
+    public abstract boolean equals(Object obj);
+
+    /**
+     * Returns the description of this schema element, or the empty string if it
+     * does not have a description.
+     *
+     * @return The description of this schema element, or the empty string if it
+     *         does not have a description.
+     */
+    public final String getDescription() {
+        return description;
+    }
+
+    /**
+     * Returns an unmodifiable map containing all of the extra properties
+     * associated with this schema element.
+     *
+     * @return An unmodifiable map containing all of the extra properties
+     *         associated with this schema element.
+     */
+    public final Map<String, List<String>> getExtraProperties() {
+        return extraProperties;
+    }
+
+    @Override
+    public abstract int hashCode();
+
+    /**
+     * Returns the string representation of this schema element as defined in
+     * RFC 2252.
+     *
+     * @return The string representation of this schema element as defined in
+     *         RFC 2252.
+     */
+    @Override
+    public final String toString() {
+        if (definition == null) {
+            definition = buildDefinition();
+        }
+        return definition;
+    }
+
+    final void appendDescription(final StringBuilder buffer) {
+        if (description != null && description.length() > 0) {
+            buffer.append(" DESC '");
+            buffer.append(description);
+            buffer.append("'");
+        }
+    }
+
+    abstract void toStringContent(StringBuilder buffer);
+
+    private final String buildDefinition() {
+        final StringBuilder buffer = new StringBuilder();
+        buffer.append("( ");
+        toStringContent(buffer);
+        if (!extraProperties.isEmpty()) {
+            for (final Map.Entry<String, List<String>> e : extraProperties.entrySet()) {
+                final String property = e.getKey();
+                final List<String> valueList = e.getValue();
+                buffer.append(" ");
+                buffer.append(property);
+                if (valueList.size() == 1) {
+                    buffer.append(" '");
+                    buffer.append(valueList.get(0));
+                    buffer.append("'");
+                } else {
+                    buffer.append(" ( ");
+                    for (final String value : valueList) {
+                        buffer.append("'");
+                        buffer.append(value);
+                        buffer.append("' ");
+                    }
+                    buffer.append(")");
+                }
+            }
+        }
+        buffer.append(" )");
+        return buffer.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaException.java
new file mode 100644
index 0000000..4e3f7d3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaException.java
@@ -0,0 +1,60 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+
+/**
+ * Thrown when a schema could not be decoded or validated.
+ */
+@SuppressWarnings("serial")
+final class SchemaException extends Exception implements LocalizableException {
+    /** The I18N message associated with this exception. */
+    private final LocalizableMessage message;
+
+    /**
+     * Creates a new schema exception with the provided message.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     */
+    public SchemaException(final LocalizableMessage message) {
+        super(String.valueOf(message));
+        this.message = message;
+    }
+
+    /**
+     * Creates a new schema exception with the provided message and cause.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     * @param cause
+     *            The cause which may be later retrieved by the
+     *            {@link #getCause} method. A {@code null} value is permitted,
+     *            and indicates that the cause is nonexistent or unknown.
+     */
+    public SchemaException(final LocalizableMessage message, final Throwable cause) {
+        super(String.valueOf(message), cause);
+        this.message = message;
+    }
+
+    @Override
+    public LocalizableMessage getMessageObject() {
+        return this.message;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaOptions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaOptions.java
new file mode 100644
index 0000000..4df915f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaOptions.java
@@ -0,0 +1,135 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.util.Option;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/**
+ * Common options for LDAP schemas.
+ * <p>
+ * For example you set schema option as you want when using a schema.
+ *
+ * <pre>
+ * // Retrieves options from builder.
+ * SchemaOptions options = new SchemaBuilder().getOptions();
+ * // Creates a new option.
+ * Option myIntegerOption = options.set(Option.of(Integer.class, 0));
+ * // Retrieves option value from SchemaOption
+ * boolean allowMalformedNamesAndOptions = options.get(SchemaOptions.ALLOW_MALFORMED_NAMES_AND_OPTIONS);
+ * </pre>
+ */
+public final class SchemaOptions {
+    /**
+     * Specifies whether the schema should allow certain illegal
+     * characters in OIDs and attribute options. When this compatibility option
+     * is set to {@code true} the following illegal characters will be permitted
+     * in addition to those permitted in section 1.4 of RFC 4512:
+     *
+     * <pre>
+     * USCORE  = %x5F ; underscore ("_")
+     * DOT     = %x2E ; period (".")
+     * </pre>
+     *
+     * By default this compatibility option is set to {@code true} because these
+     * characters are often used for naming purposes (such as collation rules).
+     */
+    public static final Option<Boolean> ALLOW_MALFORMED_NAMES_AND_OPTIONS = Option.withDefault(true);
+
+    /**
+     * Specifies whether the schema should allow attribute type definitions that do not declare a superior attribute
+     * type or syntax. When this compatibility option is set to {@code true} invalid attribute type definitions will
+     * use the default syntax specifed by the {@link #DEFAULT_SYNTAX_OID} option.
+     * <p>
+     * By default this compatibility option is set to {@code true} in order to remain compatible with previous
+     * versions of OpenDJ.
+     */
+    public static final Option<Boolean> ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX = Option.withDefault(true);
+
+    /**
+     * Specifies whether the JPEG Photo syntax should allow values which
+     * do not conform to the JFIF or Exif specifications.
+     * <p>
+     * By default this compatibility option is set to {@code true}.
+     */
+    public static final Option<Boolean> ALLOW_MALFORMED_JPEG_PHOTOS = Option.withDefault(true);
+
+    /**
+     * Specifies whether the Certificate syntax should allow values which
+     * do not conform to the X.509 specifications.
+     * <p>
+     * By default this compatibility option is set to {@code true}.
+     */
+    public static final Option<Boolean> ALLOW_MALFORMED_CERTIFICATES = Option.withDefault(true);
+
+    /**
+     * Specifies whether the Telephone Number syntax should allow values
+     * which do not conform to the E.123 international telephone number format.
+     * <p>
+     * By default this compatibility option is set to {@code true}.
+     */
+    public static final Option<Boolean> ALLOW_NON_STANDARD_TELEPHONE_NUMBERS = Option.withDefault(true);
+
+    /**
+     * Specifies whether zero-length values will be allowed by the
+     * Directory String syntax. This is technically forbidden by the LDAP
+     * specification, but it was allowed in earlier versions of the server, and
+     * the discussion of the directory string syntax in RFC 2252 does not
+     * explicitly state that they are not allowed.
+     * <p>
+     * By default this compatibility option is set to {@code false}.
+     */
+    public static final Option<Boolean> ALLOW_ZERO_LENGTH_DIRECTORY_STRINGS = Option.withDefault(false);
+
+    /**
+     * Specifies the OID of the default syntax which will be used when parsing
+     * unrecognized attributes.
+     * <p>
+     * By default the {@link SchemaConstants#SYNTAX_OCTET_STRING_OID OctetString}
+     * syntax will be used.
+     */
+    public static final Option<String> DEFAULT_SYNTAX_OID = Option.of(String.class, SYNTAX_OCTET_STRING_OID);
+
+    /**
+     * Specifies the OID of the default matching rule which will be used when
+     * parsing unrecognized attributes.
+     * <p>
+     * By default the {@link SchemaConstants#EMR_OCTET_STRING_OID OctetString}
+     * matching rule will be used.
+     */
+    public static final Option<String> DEFAULT_MATCHING_RULE_OID = Option.of(String.class, EMR_OCTET_STRING_OID);
+
+    /**
+     * Indicates whether country code values are required to strictly
+     * comply with the standard definition for this syntax.
+     * <p>
+     * When set to false, country codes will not be validated and, as a result
+     * any string containing 2 characters will be acceptable.
+     * By default this compatibility option is set to {@code true}.
+     */
+    public static final Option<Boolean> STRICT_FORMAT_FOR_COUNTRY_STRINGS = Option.withDefault(true);
+
+    /**
+     * Indicates whether the minimum upper bound value should be stripped from
+     * the Attribute Type Syntax Description.
+     * <p>
+     * By default this compatibility option is set to {@code false}.
+     */
+    public static final Option<Boolean> STRIP_UPPER_BOUND_FOR_ATTRIBUTE_TYPE = Option.withDefault(false);
+
+    private SchemaOptions() { }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaUtils.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaUtils.java
new file mode 100644
index 0000000..30fa0ba
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaUtils.java
@@ -0,0 +1,776 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+import static com.forgerock.opendj.util.StringPrepProfile.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * Schema utility methods.
+ */
+final class SchemaUtils {
+    /**
+     * Reads the value for an "extra" parameter. It will handle a single
+     * unquoted word (which is technically illegal, but we'll allow it), a
+     * single quoted string, or an open parenthesis followed by a
+     * space-delimited set of quoted strings or unquoted words followed by a
+     * close parenthesis.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @return The "extra" parameter value that was read.
+     * @throws DecodeException
+     *             If a problem occurs while attempting to read the value.
+     */
+    static List<String> readExtensions(final SubstringReader reader) throws DecodeException {
+        int length = 0;
+        List<String> values;
+
+        // Skip over any leading spaces.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            // Look at the next character. If it is a quote, then parse until
+            // the next quote and end. If it is an open parenthesis, then
+            // parse individual values until the close parenthesis and end.
+            // Otherwise, parse until the next space and end.
+            char c = reader.read();
+            if (c == '\'') {
+                reader.mark();
+                // Parse until the closing quote.
+                while (reader.read() != '\'') {
+                    length++;
+                }
+
+                reader.reset();
+                values = Collections.singletonList(reader.read(length));
+                reader.read();
+            } else if (c == '(') {
+                // Skip over any leading spaces
+                reader.skipWhitespaces();
+                reader.mark();
+
+                c = reader.read();
+                if (c == ')') {
+                    values = Collections.emptyList();
+                } else {
+                    values = new ArrayList<>();
+                    do {
+                        reader.reset();
+                        values.add(readQuotedString(reader));
+                        reader.skipWhitespaces();
+                        reader.mark();
+                    } while (reader.read() != ')');
+                    values = Collections.unmodifiableList(values);
+                }
+            } else {
+                // Parse until the next space.
+                do {
+                    length++;
+                } while (reader.read() != ' ');
+
+                reader.reset();
+                values = Collections.singletonList(reader.read(length));
+            }
+
+            return values;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    static List<String> readNameDescriptors(final SubstringReader reader,
+            final boolean allowCompatChars) throws DecodeException {
+        List<String> values;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+
+        try {
+            reader.mark();
+            char c = reader.read();
+            if (c == '\'') {
+                reader.reset();
+                values = Collections.singletonList(readQuotedDescriptor(reader, allowCompatChars));
+            } else if (c == '(') {
+                // Skip over any leading spaces
+                reader.skipWhitespaces();
+                reader.mark();
+
+                c = reader.read();
+                if (c == ')') {
+                    values = Collections.emptyList();
+                } else {
+                    values = new LinkedList<>();
+                    do {
+                        reader.reset();
+                        values.add(readQuotedDescriptor(reader, allowCompatChars));
+                        reader.skipWhitespaces();
+                        reader.mark();
+                    } while (reader.read() != ')');
+                    values = Collections.unmodifiableList(values);
+                }
+            } else {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+            }
+
+            return values;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /**
+     * Reads the attribute description or numeric OID, skipping over any leading
+     * or trailing spaces.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @param allowCompatChars
+     *            {@code true} if certain illegal characters should be allowed
+     *            for compatibility reasons.
+     * @return The attribute description or numeric OID read from the
+     *         definition.
+     * @throws DecodeException
+     *             If a problem is encountered while reading the name or OID.
+     */
+    static String readOID(final SubstringReader reader, final boolean allowCompatChars)
+            throws DecodeException {
+        int length = 0;
+        boolean enclosingQuote = false;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        if (reader.remaining() > 0) {
+            // The next character must be either numeric (for an OID) or
+            // alphabetic (for an attribute description).
+            if (reader.read() == '\'') {
+                enclosingQuote = true;
+                reader.mark();
+            } else {
+                reader.reset();
+            }
+        }
+
+        if (reader.remaining() > 0) {
+            char c = reader.read();
+            length++;
+
+            if (isDigit(c)) {
+                // This must be a numeric OID. In that case, we will accept
+                // only digits and periods, but not consecutive periods.
+                boolean lastWasPeriod = false;
+
+                while (reader.remaining() > 0
+                        && (c = reader.read()) != ' '
+                        && c != ')'
+                        && (c != '\'' || !enclosingQuote)) {
+                    if (c == '.') {
+                        if (lastWasPeriod) {
+                            throw DecodeException.error(
+                                    ERR_ATTR_SYNTAX_OID_CONSECUTIVE_PERIODS1.get(reader.pos() - 1));
+                        }
+                        lastWasPeriod = true;
+                    } else if (!isDigit(c)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER1.get(c, reader.pos() - 1));
+                    } else {
+                        lastWasPeriod = false;
+                    }
+
+                    length++;
+                }
+
+                if (lastWasPeriod) {
+                    throw DecodeException.error(
+                            ERR_ATTR_SYNTAX_OID_ENDS_WITH_PERIOD1.get(reader.pos() - 1));
+                }
+            } else if (isAlpha(c)) {
+                // This must be an attribute description. In this case, we will
+                // only accept alphabetic characters, numeric digits, and the hyphen.
+                while (reader.remaining() > 0
+                        && (c = reader.read()) != ' '
+                        && c != ')'
+                        && (c != '\'' || !enclosingQuote)) {
+                    if (length == 0 && !isAlpha(c)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                    }
+
+                    if (!isKeyChar(c, allowCompatChars)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                    }
+
+                    length++;
+                }
+            } else {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+            }
+
+            if (enclosingQuote && c != '\'') {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_EXPECTED_QUOTE_AT_POS1.get(reader.pos() - 1, c));
+            }
+        }
+
+        if (length == 0) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_OID_NO_VALUE1.get(reader.pos() - 1));
+        }
+
+        reader.reset();
+        final String oid = reader.read(length);
+        if (enclosingQuote) {
+            reader.read();
+        }
+
+        return oid;
+    }
+
+    /**
+     * Reads the next OID from the definition, skipping over any leading spaces.
+     * The OID may be followed by a integer length in brackets.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @param allowCompatChars
+     *            {@code true} if certain illegal characters should be allowed
+     *            for compatibility reasons.
+     * @return The OID read from the definition.
+     * @throws DecodeException
+     *             If a problem is encountered while reading the token name.
+     */
+    static String readOIDLen(final SubstringReader reader, final boolean allowCompatChars)
+            throws DecodeException {
+        int length = 1;
+        boolean enclosingQuote = false;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            // The next character must be either numeric (for an OID) or
+            // alphabetic (for an attribute description).
+            char c = reader.read();
+            if (c == '\'') {
+                enclosingQuote = true;
+                reader.mark();
+                c = reader.read();
+            }
+            if (isDigit(c)) {
+                boolean lastWasPeriod = false;
+                while ((c = reader.read()) != ' '
+                        && c != '{'
+                        && (c != '\'' || !enclosingQuote)) {
+                    if (c == '.') {
+                        if (lastWasPeriod) {
+                            throw DecodeException.error(
+                                    ERR_ATTR_SYNTAX_OID_CONSECUTIVE_PERIODS1.get(reader.pos() - 1));
+                        }
+                        lastWasPeriod = true;
+                    } else if (!isDigit(c)) {
+                        /*
+                         * Technically, this must be an illegal character.
+                         * However, it is possible that someone just got sloppy
+                         * and did not include a space between the name/OID and
+                         * a closing parenthesis.
+                         * In that case, we'll assume it's the end of the value.
+                         */
+                        if (c == ')') {
+                            break;
+                        }
+
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER1.get(c, reader.pos() - 1));
+                    } else {
+                        lastWasPeriod = false;
+                    }
+                    length++;
+                }
+
+                if (length == 0) {
+                    throw DecodeException.error(
+                            ERR_ATTR_SYNTAX_OID_NO_VALUE1.get(reader.pos() - 1));
+                }
+            } else if (isAlpha(c)) {
+                // This must be an attribute description. In this case, we will
+                // only accept alphabetic characters, numeric digits, and the hyphen.
+                while ((c = reader.read()) != ' '
+                        && c != ')'
+                        && c != '{'
+                        && (c != '\'' || !enclosingQuote)) {
+                    if (length == 0 && !isAlpha(c)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                    }
+
+                    if (!isKeyChar(c, allowCompatChars)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                    }
+
+                    length++;
+                }
+            } else {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+            }
+
+            reader.reset();
+
+            // Return the position of the first non-space character after the
+            // token.
+            final String oid = reader.read(length);
+
+            reader.mark();
+            c = reader.read();
+            if (c == '{') {
+                reader.mark();
+                // The only thing we'll allow here will be numeric digits and
+                // the closing curly brace.
+                while ((c = reader.read()) != '}') {
+                    if (!isDigit(c)) {
+                        throw DecodeException.error(
+                                ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER1.get(reader.getString(),
+                                        reader.pos() - 1));
+                    }
+                }
+            } else if (c == '\'') {
+                reader.mark();
+            } else {
+                reader.reset();
+            }
+
+            return oid;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    static Set<String> readOIDs(final SubstringReader reader, final boolean allowCompatChars)
+            throws DecodeException {
+        Set<String> values;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            final char c = reader.read();
+            if (c == '(') {
+                values = new LinkedHashSet<>();
+                do {
+                    values.add(readOID(reader, allowCompatChars));
+
+                    // Skip over any trailing spaces
+                    reader.skipWhitespaces();
+                } while (reader.read() != ')');
+                values = Collections.unmodifiableSet(values);
+            } else {
+                reader.reset();
+                values = Collections.singleton(readOID(reader, allowCompatChars));
+            }
+
+            return values;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /**
+     * Reads the value of a string enclosed in single quotes, skipping over the
+     * quotes and any leading spaces.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @return The string value read from the definition.
+     * @throws DecodeException
+     *             If a problem is encountered while reading the quoted string.
+     */
+    static String readQuotedString(final SubstringReader reader) throws DecodeException {
+        int length = 0;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+
+        try {
+            // The next character must be a single quote.
+            final char c = reader.read();
+            if (c != '\'') {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_EXPECTED_QUOTE_AT_POS1.get(reader.pos() - 1, c));
+            }
+
+            // Read until we find the closing quote.
+            reader.mark();
+            while (reader.read() != '\'') {
+                length++;
+            }
+
+            reader.reset();
+
+            final String str = reader.read(length);
+            reader.read();
+            return str;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /**
+     * Reads the next ruleid from the definition, skipping over any leading
+     * spaces.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @return The ruleid read from the definition.
+     * @throws DecodeException
+     *             If a problem is encountered while reading the token name.
+     */
+    static Integer readRuleID(final SubstringReader reader) throws DecodeException {
+        // This must be a ruleid. In that case, we will accept
+        // only digits.
+        int length = 0;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            while (reader.read() != ' ') {
+                length++;
+            }
+
+            if (length == 0) {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_RULE_ID_NO_VALUE1.get(reader.pos() - 1));
+            }
+
+            reader.reset();
+            final String ruleID = reader.read(length);
+
+            try {
+                return Integer.valueOf(ruleID);
+            } catch (final NumberFormatException e) {
+                throw DecodeException.error(ERR_ATTR_SYNTAX_RULE_ID_INVALID1.get(ruleID));
+            }
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    static Set<Integer> readRuleIDs(final SubstringReader reader) throws DecodeException {
+        Set<Integer> values;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            final char c = reader.read();
+            if (c == '(') {
+                values = new LinkedHashSet<>();
+                do {
+                    values.add(readRuleID(reader));
+
+                    // Skip over any trailing spaces
+                    reader.skipWhitespaces();
+                } while (reader.read() != ')');
+                values = Collections.unmodifiableSet(values);
+            } else {
+                reader.reset();
+                values = Collections.singleton(readRuleID(reader));
+            }
+
+            return values;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /**
+     * Reads the next token name from the definition, skipping over any leading
+     * or trailing spaces or <code>null</code> if there are no more tokens to
+     * read.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @return The token name read from the definition or <code>null</code> .
+     * @throws DecodeException
+     *             If a problem is encountered while reading the token name.
+     */
+    static String readTokenName(final SubstringReader reader) throws DecodeException {
+        String token = null;
+        int length = 0;
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+        reader.mark();
+
+        try {
+            // Read until we find the next space.
+            char c;
+            while ((c = reader.read()) != ' ' && c != ')') {
+                length++;
+            }
+
+            if (length > 0) {
+                reader.reset();
+                token = reader.read(length);
+            }
+
+            // Skip over any trailing spaces after the value.
+            reader.skipWhitespaces();
+
+            if (token == null && reader.remaining() > 0) {
+                reader.reset();
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_UNEXPECTED_CLOSE_PARENTHESIS1.get(length));
+            }
+
+            return token;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /**
+     * Returns an unmodifiable copy of the provided schema element extra
+     * properties.
+     *
+     * @param extraProperties
+     *            The schema element extra properties.
+     * @return An unmodifiable copy of the provided schema element extra
+     *         properties.
+     */
+    static Map<String, List<String>> unmodifiableCopyOfExtraProperties(
+            final Map<String, List<String>> extraProperties) {
+        if (extraProperties == null || extraProperties.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        final Map<String, List<String>> tmp = new LinkedHashMap<>(extraProperties.size());
+        for (final Map.Entry<String, List<String>> e : extraProperties.entrySet()) {
+            tmp.put(e.getKey(), unmodifiableCopyOfList(e.getValue()));
+        }
+        return Collections.unmodifiableMap(tmp);
+    }
+
+    static <E> List<E> unmodifiableCopyOfList(final List<E> l) {
+        if (l == null || l.isEmpty()) {
+            return Collections.emptyList();
+        } else if (l.size() == 1) {
+            return Collections.singletonList(l.get(0));
+        } else {
+            final List<E> copy = new LinkedList<>(l);
+            return Collections.unmodifiableList(copy);
+        }
+    }
+
+    static <E> Set<E> unmodifiableCopyOfSet(final Set<E> s) {
+        if (s == null || s.isEmpty()) {
+            return Collections.emptySet();
+        } else if (s.size() == 1) {
+            return Collections.singleton(s.iterator().next());
+        } else {
+            final Set<E> copy = new LinkedHashSet<>(s);
+            return Collections.unmodifiableSet(copy);
+        }
+    }
+
+    /**
+     * Reads the value of a string enclosed in single quotes, skipping over the
+     * quotes and any leading spaces.
+     *
+     * @param reader
+     *            The string representation of the definition.
+     * @param allowCompatChars
+     *            {@code true} if certain illegal characters should be allowed
+     *            for compatibility reasons.
+     * @return The string value read from the definition.
+     * @throws DecodeException
+     *             If a problem is encountered while reading the quoted string.
+     */
+    private static String readQuotedDescriptor(final SubstringReader reader,
+            final boolean allowCompatChars) throws DecodeException {
+        int length = 0;
+
+        // Skip over any spaces at the beginning of the value.
+        reader.skipWhitespaces();
+
+        try {
+            // The next character must be a single quote.
+            char c = reader.read();
+            if (c != '\'') {
+                throw DecodeException.error(
+                        ERR_ATTR_SYNTAX_EXPECTED_QUOTE_AT_POS1.get(reader.pos() - 1, c));
+            }
+
+            // Read until we find the closing quote.
+            reader.mark();
+            while ((c = reader.read()) != '\'') {
+                if (length == 0 && !isAlpha(c)) {
+                    throw DecodeException.error(
+                            ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                }
+
+                if (!isKeyChar(c, allowCompatChars)) {
+                    throw DecodeException.error(
+                            ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1.get(c, reader.pos() - 1));
+                }
+
+                length++;
+            }
+
+            reader.reset();
+
+            final String descr = reader.read(length);
+            reader.read();
+            return descr;
+        } catch (final StringIndexOutOfBoundsException e) {
+            throw DecodeException.error(ERR_ATTR_SYNTAX_TRUNCATED_VALUE1.get());
+        }
+    }
+
+    /** Prevent instantiation. */
+    private SchemaUtils() {
+        // Nothing to do.
+    }
+
+    private static ByteString singleSpaceOrEmpty(final ByteSequence value) {
+        if (value.length() > 0) {
+            // This should only happen if the value is composed entirely of
+            // spaces. In that case, the normalized value is a single space.
+            return SchemaConstants.SINGLE_SPACE_VALUE;
+        }
+        // The value is empty, so it is already normalized.
+        return ByteString.empty();
+    }
+
+    static ByteString normalizeStringListAttributeValue(final ByteSequence value, boolean trim, boolean foldCase) {
+        final StringBuilder buffer = new StringBuilder();
+        prepareUnicode(buffer, value, trim, foldCase);
+
+        if (buffer.length() == 0) {
+            return singleSpaceOrEmpty(value);
+        }
+        trimUnnecessarySpacesInStringList(buffer);
+        return ByteString.valueOfUtf8(buffer);
+    }
+
+    private static void trimUnnecessarySpacesInStringList(StringBuilder buffer) {
+        // Replace any consecutive spaces with a single space. Any spaces
+        // around a dollar sign will also be removed.
+        for (int pos = buffer.length() - 1; pos > 0; pos--) {
+            if (buffer.charAt(pos) == ' ') {
+                if (buffer.charAt(pos - 1) == '$') {
+                    if (pos <= 1 || buffer.charAt(pos - 2) != '\\') {
+                        buffer.delete(pos, pos + 1);
+                    }
+                } else if (buffer.charAt(pos + 1) == '$') {
+                    buffer.delete(pos, pos + 1);
+                }
+            }
+        }
+    }
+
+    static ByteString normalizeStringAttributeValue(final ByteSequence value, final boolean trim,
+            final boolean foldCase) {
+        final StringBuilder buffer = new StringBuilder();
+        prepareUnicode(buffer, value, trim, foldCase);
+
+        if (buffer.length() == 0) {
+            return singleSpaceOrEmpty(value);
+        }
+        return ByteString.valueOfUtf8(buffer);
+    }
+
+    static ByteString normalizeIA5StringAttributeValue(final ByteSequence value, boolean trim, boolean foldCase)
+            throws DecodeException {
+        final StringBuilder buffer = new StringBuilder();
+        prepareUnicode(buffer, value, trim, foldCase);
+
+        if (buffer.length() == 0) {
+            return singleSpaceOrEmpty(value);
+        }
+        throwIfIA5IllegalCharacter(buffer, value);
+        return ByteString.valueOfUtf8(buffer);
+    }
+
+    static void throwDecodeException(LocalizedLogger logger, LocalizableMessage message) throws DecodeException {
+        final DecodeException e = DecodeException.error(message);
+        logger.debug(LocalizableMessage.raw("%s", e));
+        throw e;
+    }
+
+    private static void throwIfIA5IllegalCharacter(StringBuilder buffer, ByteSequence value) throws DecodeException {
+        // Replace any consecutive spaces with a single space and watch out
+        // for non-ASCII characters.
+        for (int pos = buffer.length() - 1; pos > 0; pos--) {
+            final char c = buffer.charAt(pos);
+            if ((c & 0x7F) != c) {
+                // This is not a valid character for an IA5 string. If strict
+                // syntax enforcement is enabled, then we'll throw an exception.
+                // Otherwise, we'll get rid of the character.
+                throw DecodeException.error(
+                        WARN_ATTR_SYNTAX_IA5_ILLEGAL_CHARACTER.get(value, c));
+            }
+        }
+    }
+
+    static ByteString normalizeNumericStringAttributeValue(final ByteSequence value) {
+        final StringBuilder buffer = new StringBuilder();
+        prepareUnicode(buffer, value, TRIM, NO_CASE_FOLD);
+
+        // Remove any space
+        for (int pos = buffer.length() - 1; pos > 0; pos--) {
+            char c = buffer.charAt(pos);
+            if (c == ' ') {
+                buffer.delete(pos, pos + 1);
+            }
+        }
+
+        if (buffer.length() == 0) {
+            return ByteString.empty();
+        }
+        return ByteString.valueOfUtf8(buffer);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaValidationPolicy.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaValidationPolicy.java
new file mode 100644
index 0000000..5782888
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SchemaValidationPolicy.java
@@ -0,0 +1,425 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+
+/**
+ * This class provides various schema validation policy options for controlling
+ * how entries should be validated against the directory schema.
+ */
+public final class SchemaValidationPolicy {
+    /**
+     * A call-back which will be called during DIT structure rule schema
+     * validation in order to retrieve the parent of the entry being validated.
+     */
+    public static interface EntryResolver {
+        /**
+         * Returns the named entry in order to enforce DIT structure rules.
+         *
+         * @param dn
+         *            The name of the entry to be returned.
+         * @return The named entry.
+         * @throws LdapException
+         *             If the entry could not be retrieved.
+         */
+        Entry getEntry(DN dn) throws LdapException;
+    }
+
+    /**
+     * An enumeration of the possible actions which can be performed when a
+     * schema validation failure is encountered.
+     */
+    public static enum Action {
+        /**
+         * Schema validation will not be performed.
+         */
+        IGNORE,
+
+        /**
+         * Schema validation will be performed, but failures will not cause the
+         * overall validation to fail. Error messages will be returned.
+         */
+        WARN,
+
+        /**
+         * Schema validation will be performed and failures will cause the
+         * overall validation to fail. Error messages will be returned.
+         */
+        REJECT;
+
+        private Action() {
+            // Nothing to do.
+        }
+
+        /**
+         * Returns {@code true} if this policy is {@code IGNORE}.
+         *
+         * @return {@code true} if this policy is {@code IGNORE}.
+         */
+        public boolean isIgnore() {
+            return this == IGNORE;
+        }
+
+        /**
+         * Returns {@code true} if this policy is {@code REJECT}.
+         *
+         * @return {@code true} if this policy is {@code REJECT}.
+         */
+        public boolean isReject() {
+            return this == REJECT;
+        }
+
+        /**
+         * Returns {@code true} if this policy is {@code WARN}.
+         *
+         * @return {@code true} if this policy is {@code WARN}.
+         */
+        public boolean isWarn() {
+            return this == WARN;
+        }
+
+        /**
+         * Returns {@code true} if this policy is {@code WARN} or {@code REJECT}
+         * .
+         *
+         * @return {@code true} if this policy is {@code WARN} or {@code REJECT}
+         *         .
+         */
+        public boolean needsChecking() {
+            return this != IGNORE;
+        }
+    }
+
+    /**
+     * Creates a copy of the provided schema validation policy.
+     *
+     * @param policy
+     *            The policy to be copied.
+     * @return The copy of the provided schema validation policy.
+     */
+    public static SchemaValidationPolicy copyOf(final SchemaValidationPolicy policy) {
+        return defaultPolicy().assign(policy);
+    }
+
+    /**
+     * Creates a new schema validation policy with default settings. More
+     * specifically:
+     * <ul>
+     * <li>Entries not having a single structural object class will be rejected
+     * <li>Entries having attributes which are not permitted by its object
+     * classes or DIT content rule (if present) will be rejected
+     * <li>Entries not conforming to name forms will be rejected
+     * <li>DIT structure rules will not be ignored
+     * </ul>
+     *
+     * @return The new schema validation policy.
+     */
+    public static SchemaValidationPolicy defaultPolicy() {
+        return new SchemaValidationPolicy();
+    }
+
+    /**
+     * Creates a new schema validation policy which will not perform any schema
+     * validation.
+     *
+     * @return The new schema validation policy.
+     */
+    public static SchemaValidationPolicy ignoreAll() {
+        return new SchemaValidationPolicy().checkAttributesAndObjectClasses(Action.IGNORE)
+                .checkAttributeValues(Action.IGNORE).checkDITContentRules(Action.IGNORE)
+                .checkNameForms(Action.IGNORE).requireSingleStructuralObjectClass(Action.IGNORE);
+    }
+
+    private Action checkNameForms = Action.REJECT;
+    private Action checkDITStructureRules = Action.IGNORE;
+    private Action checkDITContentRules = Action.REJECT;
+    private Action requireSingleStructuralObjectClass = Action.REJECT;
+    private Action checkAttributesAndObjectClasses = Action.REJECT;
+    private Action checkAttributeValues = Action.REJECT;
+    private EntryResolver checkDITStructureRulesEntryResolver;
+
+    /** Prevent direct instantiation. */
+    private SchemaValidationPolicy() {
+        // Nothing to do.
+    }
+
+    /**
+     * Returns the policy for verifying that the user attributes in an entry
+     * conform to its object classes. More specifically, an entry must contain
+     * all required user attributes, and must not contain any user attributes
+     * which are not declared as required or optional by its object classes.
+     * <p>
+     * By default entries which have missing or additional user attributes will
+     * be rejected.
+     *
+     * @return The policy for verifying that the user attributes in an entry
+     *         conform to its object classes.
+     */
+    public Action checkAttributesAndObjectClasses() {
+        return checkAttributesAndObjectClasses;
+    }
+
+    /**
+     * Specifies the policy for verifying that the user attributes in an entry
+     * conform to its object classes. More specifically, an entry must contain
+     * all required user attributes, and must not contain any user attributes
+     * which are not declared as required or optional by its object classes.
+     * <p>
+     * By default entries which have missing or additional user attributes will
+     * be rejected.
+     *
+     * @param policy
+     *            The policy for verifying that the user attributes in an entry
+     *            conform to its object classes.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     */
+    public SchemaValidationPolicy checkAttributesAndObjectClasses(final Action policy) {
+        this.checkAttributesAndObjectClasses = policy;
+        return this;
+    }
+
+    /**
+     * Returns the policy for verifying that the user attributes in an entry
+     * conform to their associated attribute type descriptions. This may
+     * include:
+     * <ul>
+     * <li>checking that there is at least one value
+     * <li>checking that single-valued attributes contain only a single value
+     * <li>checking that there are no duplicate values according to the
+     * attribute's default equality matching rule
+     * <li>checking that attributes which require BER encoding specify the
+     * {@code ;binary} attribute option
+     * <li>checking that the values are valid according to the attribute's
+     * syntax.
+     * </ul>
+     * Schema validation implementations specify exactly which of the above
+     * checks will be performed.
+     * <p>
+     * By default entries which have invalid attribute values will be rejected.
+     *
+     * @return The policy for verifying that the user attributes in an entry
+     *         conform to their associated attribute type descriptions.
+     */
+    public Action checkAttributeValues() {
+        return checkAttributeValues;
+    }
+
+    /**
+     * Specifies the policy for verifying that the user attributes in an entry
+     * conform to their associated attribute type descriptions. This may
+     * include:
+     * <ul>
+     * <li>checking that there is at least one value
+     * <li>checking that single-valued attributes contain only a single value
+     * <li>checking that there are no duplicate values according to the
+     * attribute's default equality matching rule
+     * <li>checking that attributes which require BER encoding specify the
+     * {@code ;binary} attribute option
+     * <li>checking that the values are valid according to the attribute's
+     * syntax.
+     * </ul>
+     * Schema validation implementations specify exactly which of the above
+     * checks will be performed.
+     * <p>
+     * By default entries which have invalid attribute values will be rejected.
+     *
+     * @param policy
+     *            The policy for verifying that the user attributes in an entry
+     *            conform to their associated attribute type descriptions.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     */
+    public SchemaValidationPolicy checkAttributeValues(final Action policy) {
+        this.checkAttributeValues = policy;
+        return this;
+    }
+
+    /**
+     * Returns the policy for validating entries against content rules defined
+     * in the schema.
+     * <p>
+     * By default content rules will be ignored during validation.
+     *
+     * @return The policy for validating entries against content rules defined
+     *         in the schema.
+     */
+    public Action checkDITContentRules() {
+        return checkDITContentRules;
+    }
+
+    /**
+     * Specifies the policy for validating entries against content rules defined
+     * in the schema.
+     * <p>
+     * By default content rules will be ignored during validation.
+     *
+     * @param policy
+     *            The policy for validating entries against content rules
+     *            defined in the schema.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     */
+    public SchemaValidationPolicy checkDITContentRules(final Action policy) {
+        this.checkDITContentRules = policy;
+        return this;
+    }
+
+    /**
+     * Returns the policy for validating entries against structure rules defined
+     * in the schema.
+     * <p>
+     * By default structure rules will be ignored during validation.
+     *
+     * @return The policy for validating entries against structure rules defined
+     *         in the schema.
+     */
+    public Action checkDITStructureRules() {
+        return checkDITStructureRules;
+    }
+
+    /**
+     * Specifies the policy for validating entries against structure rules
+     * defined in the schema.
+     * <p>
+     * By default structure rules will be ignored during validation.
+     *
+     * @param policy
+     *            The policy for validating entries against structure rules
+     *            defined in the schema.
+     * @param resolver
+     *            The parent entry resolver which should be used for retrieving
+     *            the parent entry during DIT structure rule validation.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     * @throws IllegalArgumentException
+     *             If {@code resolver} was {@code null} and
+     *             {@code checkDITStructureRules} is either {@code WARN} or
+     *             {@code REJECT}.
+     */
+    public SchemaValidationPolicy checkDITStructureRules(final Action policy,
+            final EntryResolver resolver) {
+        if (checkDITStructureRules.needsChecking() && resolver == null) {
+            throw new IllegalArgumentException(
+                    "Validation of structure rules enabled by resolver was null");
+        }
+        this.checkDITStructureRules = policy;
+        this.checkDITStructureRulesEntryResolver = resolver;
+        return this;
+    }
+
+    /**
+     * Returns parent entry resolver which should be used for retrieving the
+     * parent entry during DIT structure rule validation.
+     * <p>
+     * By default no resolver is defined because structure rules will be ignored
+     * during validation.
+     *
+     * @return The parent entry resolver which should be used for retrieving the
+     *         parent entry during DIT structure rule validation.
+     */
+    public EntryResolver checkDITStructureRulesEntryResolver() {
+        return checkDITStructureRulesEntryResolver;
+    }
+
+    /**
+     * Returns the policy for validating entries against name forms defined in
+     * the schema.
+     * <p>
+     * By default name forms will be ignored during validation.
+     *
+     * @return The policy for validating entries against name forms defined in
+     *         the schema.
+     */
+    public Action checkNameForms() {
+        return checkNameForms;
+    }
+
+    /**
+     * Specifies the policy for validating entries against name forms defined in
+     * the schema.
+     * <p>
+     * By default name forms will be ignored during validation.
+     *
+     * @param policy
+     *            The policy for validating entries against name forms defined
+     *            in the schema.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     */
+    public SchemaValidationPolicy checkNameForms(final Action policy) {
+        this.checkNameForms = policy;
+        return this;
+    }
+
+    /**
+     * Returns the policy for verifying that entries have only a single
+     * structural object class.
+     * <p>
+     * By default entries which do not have a structural object class or which
+     * have more than one structural object class will be rejected.
+     *
+     * @return The policy for checking that entries have one and only one
+     *         structural object class.
+     */
+    public Action requireSingleStructuralObjectClass() {
+        return requireSingleStructuralObjectClass;
+    }
+
+    /**
+     * Specifies the policy for verifying that entries have only a single
+     * structural object class.
+     * <p>
+     * By default entries which do not have a structural object class or which
+     * have more than one structural object class will be rejected.
+     *
+     * @param policy
+     *            The policy for checking that entries have one and only one
+     *            structural object class.
+     * @return A reference to this {@code SchemaValidationPolicy}.
+     */
+    public SchemaValidationPolicy requireSingleStructuralObjectClass(final Action policy) {
+        this.requireSingleStructuralObjectClass = policy;
+        return this;
+    }
+
+    /**
+     * Returns a strict view of the provided schema if the this policy is
+     * configured to check attributes and object class, or a non-strict view of
+     * the schema if not.
+     *
+     * @param schema
+     *            The schema to be adapted according to this policy.
+     * @return A strict or non-strict view of {@code schema} depending on
+     *         {@link #checkAttributesAndObjectClasses()}.
+     */
+    public Schema adaptSchemaForValidation(final Schema schema) {
+        return checkAttributesAndObjectClasses().needsChecking() ? schema.asStrictSchema() : schema
+                .asNonStrictSchema();
+    }
+
+    /** Assigns the provided options to this set of options. */
+    SchemaValidationPolicy assign(final SchemaValidationPolicy policy) {
+        this.checkAttributeValues = policy.checkAttributeValues;
+        this.checkNameForms = policy.checkNameForms;
+        this.checkAttributesAndObjectClasses = policy.checkAttributesAndObjectClasses;
+        this.checkDITContentRules = policy.checkDITContentRules;
+        this.checkDITStructureRules = policy.checkDITStructureRules;
+        this.checkDITStructureRulesEntryResolver = policy.checkDITStructureRulesEntryResolver;
+        this.requireSingleStructuralObjectClass = policy.requireSingleStructuralObjectClass;
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SubstringAssertionSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SubstringAssertionSyntaxImpl.java
new file mode 100644
index 0000000..bb6277f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SubstringAssertionSyntaxImpl.java
@@ -0,0 +1,94 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_SUBSTRING_ASSERTION_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import com.forgerock.opendj.ldap.CoreMessages;
+
+/**
+ * This class defines the substring assertion attribute syntax, which contains
+ * one or more substring components, as used in a substring search filter. For
+ * the purposes of matching, it will be treated like a Directory String syntax
+ * except that approximate matching will not be allowed.
+ */
+final class SubstringAssertionSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_SUBSTRING_ASSERTION_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get the string representation of the value and check its length.
+        // A zero-length value is acceptable. A one-length value is
+        // acceptable as long as it is not an asterisk. For all other
+        // lengths, just ensure that there are no consecutive wildcards.
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+        if (valueLength == 0) {
+            return true;
+        } else if (valueLength == 1) {
+            if (valueString.charAt(0) == '*') {
+                invalidReason.append(CoreMessages.WARN_ATTR_SYNTAX_SUBSTRING_ONLY_WILDCARD.get());
+
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            for (int i = 1; i < valueLength; i++) {
+                if (valueString.charAt(i) == '*' && valueString.charAt(i - 1) == '*') {
+                    invalidReason
+                            .append(CoreMessages.WARN_ATTR_SYNTAX_SUBSTRING_CONSECUTIVE_WILDCARDS
+                                    .get(valueString, i));
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SupportedAlgorithmSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SupportedAlgorithmSyntaxImpl.java
new file mode 100644
index 0000000..ab80a20
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SupportedAlgorithmSyntaxImpl.java
@@ -0,0 +1,66 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_OCTET_STRING_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_SUPPORTED_ALGORITHM_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the supported algorithm attribute syntax. This should
+ * be restricted to holding only X.509 supported algorithms, but we will accept
+ * any set of bytes. It will be treated much like the octet string attribute
+ * syntax.
+ */
+final class SupportedAlgorithmSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_SUPPORTED_ALGORITHM_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_OCTET_STRING_OID;
+    }
+
+    @Override
+    public boolean isBEREncodingRequired() {
+        return true;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // All values will be acceptable for the supported algorithm syntax.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Syntax.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Syntax.java
new file mode 100644
index 0000000..473efde
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Syntax.java
@@ -0,0 +1,473 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.util.Reject;
+
+/**
+ * This class defines a data structure for storing and interacting with an LDAP
+ * syntaxes, which constrain the structure of attribute values stored in an LDAP
+ * directory, and determine the representation of attribute and assertion values
+ * transferred in the LDAP protocol.
+ * <p>
+ * Syntax implementations must extend the {@link SyntaxImpl} interface so they
+ * can be used by OpenDJ to validate attribute values.
+ * <p>
+ * Where ordered sets of names, or extra properties are provided, the ordering
+ * will be preserved when the associated fields are accessed via their getters
+ * or via the {@link #toString()} methods.
+ */
+public final class Syntax extends SchemaElement {
+
+    /** A fluent API for incrementally constructing syntaxes. */
+    public static final class Builder extends SchemaElementBuilder<Builder> {
+
+        private String oid;
+        private SyntaxImpl impl;
+
+        Builder(final Syntax syntax, final SchemaBuilder builder) {
+            super(builder, syntax);
+            this.oid = syntax.oid;
+            this.impl = syntax.impl;
+        }
+
+        Builder(final String oid, final SchemaBuilder builder) {
+            super(builder);
+            oid(oid);
+        }
+
+        /**
+         * Adds this syntax to the schema, throwing a
+         * {@code ConflictingSchemaElementException} if there is an existing
+         * syntax with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         * @throws ConflictingSchemaElementException
+         *             If there is an existing syntax with the same numeric OID.
+         */
+        public SchemaBuilder addToSchema() {
+            return addToSchema(false);
+        }
+
+        /**
+         * Adds this syntax to the schema overwriting any existing syntax with the same numeric OID.
+         *
+         * @return The parent schema builder.
+         */
+        public SchemaBuilder addToSchemaOverwrite() {
+            return addToSchema(true);
+        }
+
+        SchemaBuilder addToSchema(final boolean overwrite) {
+            // Enumeration syntaxes will need their associated matching rule registered now as well.
+            for (final Map.Entry<String, List<String>> property : getExtraProperties().entrySet()) {
+                if ("x-enum".equalsIgnoreCase(property.getKey())) {
+                    final EnumSyntaxImpl enumSyntaxImpl = new EnumSyntaxImpl(oid, property.getValue());
+                    implementation(enumSyntaxImpl);
+                    return getSchemaBuilder().addSyntax(new Syntax(this), overwrite)
+                                             .buildMatchingRule(enumSyntaxImpl.getOrderingMatchingRule())
+                                             .description(getDescription() + " enumeration ordering matching rule")
+                                             .syntaxOID(oid)
+                                             .extraProperties(CoreSchemaImpl.OPENDS_ORIGIN)
+                                             .implementation(new EnumOrderingMatchingRule(enumSyntaxImpl))
+                                             .addToSchemaOverwrite();
+                }
+            }
+            return getSchemaBuilder().addSyntax(new Syntax(this), overwrite);
+        }
+
+        @Override
+        public Builder description(final String description) {
+            return description0(description);
+        }
+
+        @Override
+        public Builder extraProperties(final Map<String, List<String>> extraProperties) {
+            return extraProperties0(extraProperties);
+        }
+
+        @Override
+        public Builder extraProperties(final String extensionName, final String... extensionValues) {
+            return extraProperties0(extensionName, extensionValues);
+        }
+
+        /**
+         * Sets the numeric OID which uniquely identifies this syntax.
+         *
+         * @param oid
+         *            The numeric OID.
+         * @return This builder.
+         */
+        public Builder oid(final String oid) {
+            this.oid = oid;
+            return this;
+        }
+
+        @Override
+        public Builder removeAllExtraProperties() {
+            return removeAllExtraProperties0();
+        }
+
+        @Override
+        public Builder removeExtraProperty(final String extensionName, final String... extensionValues) {
+            return removeExtraProperty0(extensionName, extensionValues);
+        }
+
+        /**
+         * Sets the syntax implementation.
+         *
+         * @param implementation
+         *            The syntax implementation.
+         * @return This builder.
+         */
+        public Builder implementation(final SyntaxImpl implementation) {
+            this.impl = implementation;
+            return this;
+        }
+
+        @Override
+        Builder getThis() {
+            return this;
+        }
+    }
+
+    private final String oid;
+    private MatchingRule equalityMatchingRule;
+    private MatchingRule orderingMatchingRule;
+    private MatchingRule substringMatchingRule;
+    private MatchingRule approximateMatchingRule;
+    private Schema schema;
+    private SyntaxImpl impl;
+
+    private Syntax(final Builder builder) {
+        super(builder);
+
+        // Checks for required attributes.
+        if (builder.oid == null || builder.oid.isEmpty()) {
+            throw new IllegalArgumentException("An OID must be specified.");
+        }
+
+        oid = builder.oid;
+        impl = builder.impl;
+    }
+
+    /**
+     * Creates a syntax representing an unrecognized syntax and whose
+     * implementation is substituted by the schema's default syntax.
+     *
+     * @param schema
+     *            The parent schema.
+     * @param oid
+     *            The numeric OID of the unrecognized syntax.
+     */
+    Syntax(final Schema schema, final String oid) {
+        super("", Collections.singletonMap("X-SUBST", Collections.singletonList(schema.getDefaultSyntax().getOID())),
+                null);
+
+        Reject.ifNull(oid);
+        this.oid = oid;
+        this.schema = schema;
+
+        final Syntax defaultSyntax = schema.getDefaultSyntax();
+        this.impl = defaultSyntax.impl;
+        this.approximateMatchingRule = defaultSyntax.getApproximateMatchingRule();
+        this.equalityMatchingRule = defaultSyntax.getEqualityMatchingRule();
+        this.orderingMatchingRule = defaultSyntax.getOrderingMatchingRule();
+        this.substringMatchingRule = defaultSyntax.getSubstringMatchingRule();
+    }
+
+    /**
+     * Returns {@code true} if the provided object is an attribute syntax having
+     * the same numeric OID as this attribute syntax.
+     *
+     * @param o
+     *            The object to be compared.
+     * @return {@code true} if the provided object is an attribute syntax having
+     *         the same numeric OID as this attribute syntax.
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        } else if (o instanceof Syntax) {
+            final Syntax other = (Syntax) o;
+            return oid.equals(other.oid);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieves the default approximate matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default approximate matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if approximate
+     *         matches will not be allowed for this type by default.
+     */
+    public MatchingRule getApproximateMatchingRule() {
+        return approximateMatchingRule;
+    }
+
+    /**
+     * Retrieves the default equality matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default equality matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if equality matches
+     *         will not be allowed for this type by default.
+     */
+    public MatchingRule getEqualityMatchingRule() {
+        return equalityMatchingRule;
+    }
+
+    /**
+     * Retrieves the OID for this attribute syntax.
+     *
+     * @return The OID for this attribute syntax.
+     */
+    public String getOID() {
+        return oid;
+    }
+
+    /**
+     * Retrieves the name for this attribute syntax.
+     *
+     * @return The name for this attribute syntax.
+     */
+    public String getName() {
+        return impl.getName();
+    }
+
+    /**
+     * Retrieves the default ordering matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default ordering matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if ordering matches
+     *         will not be allowed for this type by default.
+     */
+    public MatchingRule getOrderingMatchingRule() {
+        return orderingMatchingRule;
+    }
+
+    /**
+     * Retrieves the default substring matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default substring matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if substring matches
+     *         will not be allowed for this type by default.
+     */
+    public MatchingRule getSubstringMatchingRule() {
+        return substringMatchingRule;
+    }
+
+    /**
+     * Returns the hash code for this attribute syntax. It will be calculated as
+     * the hash code of the numeric OID.
+     *
+     * @return The hash code for this attribute syntax.
+     */
+    @Override
+    public int hashCode() {
+        return oid.hashCode();
+    }
+
+    /**
+     * Indicates whether this attribute syntax requires that values must be
+     * encoded using the Basic Encoding Rules (BER) used by X.500 directories
+     * and always include the {@code binary} attribute description option.
+     *
+     * @return {@code true} this attribute syntax requires that values must be
+     *         BER encoded and always include the {@code binary} attribute
+     *         description option, or {@code false} if not.
+     * @see <a href="http://tools.ietf.org/html/rfc4522">RFC 4522 - Lightweight
+     *      Directory Access Protocol (LDAP): The Binary Encoding Option </a>
+     */
+    public boolean isBEREncodingRequired() {
+        return impl.isBEREncodingRequired();
+    }
+
+    /**
+     * Indicates whether this attribute syntax would likely be a human readable
+     * string.
+     *
+     * @return {@code true} if this attribute syntax would likely be a human
+     *         readable string or {@code false} if not.
+     */
+    public boolean isHumanReadable() {
+        return impl.isHumanReadable();
+    }
+
+    /**
+     * Indicates whether the provided value is acceptable for use in an
+     * attribute with this syntax. If it is not, then the reason may be appended
+     * to the provided buffer.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            The buffer to which the invalid reason should be appended.
+     * @return {@code true} if the provided value is acceptable for use with
+     *         this syntax, or {@code false} if not.
+     */
+    public boolean valueIsAcceptable(final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        return impl.valueIsAcceptable(schema, value, invalidReason);
+    }
+
+    @Override
+    void toStringContent(final StringBuilder buffer) {
+        buffer.append(oid);
+        appendDescription(buffer);
+    }
+
+    void validate(final Schema schema, final List<LocalizableMessage> warnings)
+            throws SchemaException {
+        this.schema = schema;
+        if (impl == null) {
+            // See if we need to override the implementation of the syntax
+            for (final Map.Entry<String, List<String>> property : getExtraProperties().entrySet()) {
+                // Enums are handled in the schema builder.
+                if ("x-subst".equalsIgnoreCase(property.getKey())) {
+                    /**
+                     * One unimplemented syntax can be substituted by another
+                     * defined syntax. A substitution syntax is an
+                     * LDAPSyntaxDescriptionSyntax with X-SUBST extension.
+                     */
+                    final Iterator<String> values = property.getValue().iterator();
+                    if (values.hasNext()) {
+                        final String value = values.next();
+                        if (value.equals(oid)) {
+                            throw new SchemaException(ERR_ATTR_SYNTAX_CYCLIC_SUB_SYNTAX.get(oid));
+                        }
+                        if (!schema.hasSyntax(value)) {
+                            throw new SchemaException(ERR_ATTR_SYNTAX_UNKNOWN_SUB_SYNTAX.get(oid, value));
+                        }
+                        final Syntax subSyntax = schema.getSyntax(value);
+                        if (subSyntax.impl == null) {
+                            // The substitution syntax was never validated.
+                            subSyntax.validate(schema, warnings);
+                        }
+                        impl = subSyntax.impl;
+                    }
+                } else if ("x-pattern".equalsIgnoreCase(property.getKey())) {
+                    final Iterator<String> values = property.getValue().iterator();
+                    if (values.hasNext()) {
+                        final String value = values.next();
+                        try {
+                            final Pattern pattern = Pattern.compile(value);
+                            impl = new RegexSyntaxImpl(pattern);
+                        } catch (final Exception e) {
+                            throw new SchemaException(
+                                    WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_PATTERN.get(oid, value));
+                        }
+                    }
+                }
+            }
+
+            // Try to find an implementation in the core schema
+            if (impl == null && Schema.getDefaultSchema().hasSyntax(oid)) {
+                impl = Schema.getDefaultSchema().getSyntax(oid).impl;
+            }
+            if (impl == null && Schema.getCoreSchema().hasSyntax(oid)) {
+                impl = Schema.getCoreSchema().getSyntax(oid).impl;
+            }
+
+            if (impl == null) {
+                final Syntax defaultSyntax = schema.getDefaultSyntax();
+                if (defaultSyntax.impl == null) {
+                    // The default syntax was never validated.
+                    defaultSyntax.validate(schema, warnings);
+                }
+                impl = defaultSyntax.impl;
+                final LocalizableMessage message = WARN_ATTR_SYNTAX_NOT_IMPLEMENTED1.get(getDescription(), oid, schema
+                        .getDefaultSyntax().getOID());
+                warnings.add(message);
+            }
+        }
+
+        // Get references to the default matching rules. It will be ok
+        // if we can't find some. Just warn.
+        if (impl.getEqualityMatchingRule() != null) {
+            if (schema.hasMatchingRule(impl.getEqualityMatchingRule())) {
+                equalityMatchingRule = schema.getMatchingRule(impl.getEqualityMatchingRule());
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE.get(impl
+                                .getEqualityMatchingRule(), impl.getName());
+                warnings.add(message);
+            }
+        }
+
+        if (impl.getOrderingMatchingRule() != null) {
+            if (schema.hasMatchingRule(impl.getOrderingMatchingRule())) {
+                orderingMatchingRule = schema.getMatchingRule(impl.getOrderingMatchingRule());
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE.get(impl
+                                .getOrderingMatchingRule(), impl.getName());
+                warnings.add(message);
+            }
+        }
+
+        if (impl.getSubstringMatchingRule() != null) {
+            if (schema.hasMatchingRule(impl.getSubstringMatchingRule())) {
+                substringMatchingRule = schema.getMatchingRule(impl.getSubstringMatchingRule());
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE.get(impl
+                                .getSubstringMatchingRule(), impl.getName());
+                warnings.add(message);
+            }
+        }
+
+        if (impl.getApproximateMatchingRule() != null) {
+            if (schema.hasMatchingRule(impl.getApproximateMatchingRule())) {
+                approximateMatchingRule = schema.getMatchingRule(impl.getApproximateMatchingRule());
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE.get(impl
+                                .getApproximateMatchingRule(), impl.getName());
+                warnings.add(message);
+            }
+        }
+    }
+
+    /**
+     * Indicates if the syntax has been validated, which means it has a non-null
+     * schema.
+     *
+     * @return {@code true} if and only if this syntax has been validated
+     */
+    boolean isValidated() {
+        return schema != null;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SyntaxImpl.java
new file mode 100644
index 0000000..78fb394
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/SyntaxImpl.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This interface defines the set of methods and structures that must be
+ * implemented to define a new attribute syntax.
+ */
+public interface SyntaxImpl {
+    /**
+     * Retrieves the default approximate matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default approximate matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if approximate
+     *         matches will not be allowed for this type by default.
+     */
+    String getApproximateMatchingRule();
+
+    /**
+     * Retrieves the default equality matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default equality matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if equality matches
+     *         will not be allowed for this type by default.
+     */
+    String getEqualityMatchingRule();
+
+    /**
+     * Retrieves the common name for this attribute syntax.
+     *
+     * @return The common name for this attribute syntax.
+     */
+    String getName();
+
+    /**
+     * Retrieves the default ordering matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default ordering matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if ordering matches
+     *         will not be allowed for this type by default.
+     */
+    String getOrderingMatchingRule();
+
+    /**
+     * Retrieves the default substring matching rule that will be used for
+     * attributes with this syntax.
+     *
+     * @return The default substring matching rule that will be used for
+     *         attributes with this syntax, or {@code null} if substring matches
+     *         will not be allowed for this type by default.
+     */
+    String getSubstringMatchingRule();
+
+    /**
+     * Indicates whether this attribute syntax requires that values must be
+     * encoded using the Basic Encoding Rules (BER) used by X.500 directories
+     * and always include the {@code binary} attribute description option.
+     *
+     * @return {@code true} this attribute syntax requires that values must be
+     *         BER encoded and always include the {@code binary} attribute
+     *         description option, or {@code false} if not.
+     * @see <a href="http://tools.ietf.org/html/rfc4522">RFC 4522 - Lightweight
+     *      Directory Access Protocol (LDAP): The Binary Encoding Option </a>
+     */
+    boolean isBEREncodingRequired();
+
+    /**
+     * Indicates whether this attribute syntax would likely be a human readable
+     * string.
+     *
+     * @return {@code true} if this attribute syntax would likely be a human
+     *         readable string or {@code false} if not.
+     */
+    boolean isHumanReadable();
+
+    /**
+     * Indicates whether the provided value is acceptable for use in an
+     * attribute with this syntax. If it is not, then the reason may be appended
+     * to the provided buffer.
+     *
+     * @param schema
+     *            The schema in which this syntax is defined.
+     * @param value
+     *            The value for which to make the determination.
+     * @param invalidReason
+     *            The buffer to which the invalid reason should be appended.
+     * @return {@code true} if the provided value is acceptable for use with
+     *         this syntax, or {@code false} if not.
+     */
+    boolean valueIsAcceptable(Schema schema, ByteSequence value,
+            LocalizableMessageBuilder invalidReason);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..d667950
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberEqualityMatchingRuleImpl.java
@@ -0,0 +1,60 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * This class implements the telephoneNumberMatch matching rule defined in X.520
+ * and referenced in RFC 2252. Note that although the specification calls for a
+ * very rigorous format, this is widely ignored so this matching will compare
+ * only numeric digits and strip out everything else.
+ */
+final class TelephoneNumberEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    TelephoneNumberEqualityMatchingRuleImpl() {
+        super(EMR_TELEPHONE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+        final StringBuilder buffer = new StringBuilder(valueLength);
+
+        // Iterate through the characters in the value and filter out
+        // everything that isn't a digit.
+        for (int i = 0; i < valueLength; i++) {
+            final char c = valueString.charAt(i);
+            if (StaticUtils.isDigit(c)) {
+                buffer.append(c);
+            }
+        }
+
+        return ByteString.valueOfUtf8(buffer);
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSubstringMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSubstringMatchingRuleImpl.java
new file mode 100644
index 0000000..c5bcd0d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSubstringMatchingRuleImpl.java
@@ -0,0 +1,55 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * This class implements the telephoneNumberSubstringsMatch matching rule
+ * defined in X.520 and referenced in RFC 2252. Note that although the
+ * specification calls for a very rigorous format, this is widely ignored so
+ * this matching will compare only numeric digits and strip out everything else.
+ */
+final class TelephoneNumberSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+
+    TelephoneNumberSubstringMatchingRuleImpl() {
+        super(SMR_TELEPHONE_NAME, EMR_TELEPHONE_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) {
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+        final StringBuilder buffer = new StringBuilder(valueLength);
+
+        // Iterate through the characters in the value and filter out
+        // everything that isn't a digit.
+        for (int i = 0; i < valueLength; i++) {
+            final char c = valueString.charAt(i);
+            if (StaticUtils.isDigit(c)) {
+                buffer.append(c);
+            }
+        }
+
+        return ByteString.valueOfUtf8(buffer);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxImpl.java
new file mode 100644
index 0000000..3678b82
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxImpl.java
@@ -0,0 +1,145 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.isDigit;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEPHONE_EMPTY;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_TELEPHONE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_TELEPHONE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_TELEPHONE_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the telephone number attribute syntax, which is defined
+ * in RFC 2252. Note that this can have two modes of operation, depending on its
+ * configuration. Most of the time, it will be very lenient when deciding what
+ * to accept, and will allow anything but only pay attention to the digits.
+ * However, it can also be configured in a "strict" mode, in which case it will
+ * only accept values in the E.123 international telephone number format.
+ */
+final class TelephoneNumberSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_TELEPHONE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_TELEPHONE_NAME;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_TELEPHONE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // No matter what, the value can't be empty or null.
+        String valueStr;
+        if (value == null || (valueStr = value.toString().trim()).length() == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEPHONE_EMPTY.get());
+            return false;
+        }
+
+        final int length = valueStr.length();
+
+        if (!schema.getOption(ALLOW_NON_STANDARD_TELEPHONE_NUMBERS)) {
+            // If the value does not start with a plus sign, then that's not
+            // acceptable.
+            if (valueStr.charAt(0) != '+') {
+                final LocalizableMessage message = ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS.get(valueStr);
+                invalidReason.append(message);
+                return false;
+            }
+
+            // Iterate through the remaining characters in the value. There
+            // must be at least one digit, and it must contain only valid
+            // digits and separator characters.
+            boolean digitSeen = false;
+            for (int i = 1; i < length; i++) {
+                final char c = valueStr.charAt(i);
+                if (isDigit(c)) {
+                    digitSeen = true;
+                } else if (!isSeparator(c)) {
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR.get(valueStr, String.valueOf(c),
+                                    i);
+                    invalidReason.append(message);
+                    return false;
+                }
+            }
+
+            if (!digitSeen) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS.get(valueStr);
+                invalidReason.append(message);
+                return false;
+            }
+
+            // If we've gotten here, then we'll consider it acceptable.
+            return true;
+        } else {
+            // If we are not in strict mode, then all non-empty values
+            // containing at least one digit will be acceptable.
+            for (int i = 0; i < length; i++) {
+                if (isDigit(valueStr.charAt(i))) {
+                    return true;
+                }
+            }
+
+            // If we made it here, then we didn't find any digits.
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS.get(valueStr);
+            invalidReason.append(message);
+            return false;
+        }
+    }
+
+    /**
+     * Indicates whether the provided character is a valid separator for
+     * telephone number components when operating in strict mode.
+     *
+     * @param c
+     *            The character for which to make the determination.
+     * @return <CODE>true</CODE> if the provided character is a valid separator,
+     *         or <CODE>false</CODE> if it is not.
+     */
+    private boolean isSeparator(final char c) {
+        switch (c) {
+        case ' ':
+        case '-':
+            return true;
+        default:
+            return false;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TeletexTerminalIdentifierSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TeletexTerminalIdentifierSyntaxImpl.java
new file mode 100644
index 0000000..c66d91b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TeletexTerminalIdentifierSyntaxImpl.java
@@ -0,0 +1,188 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_TELETEX_TERM_ID_NAME;
+
+import java.util.HashSet;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the teletex terminal identifier attribute syntax, which
+ * contains a printable string (the terminal identifier) followed by zero or
+ * more parameters, which start with a dollar sign and are followed by a
+ * parameter name, a colon, and a value. The parameter value should consist of
+ * any string of bytes (the dollar sign and backslash must be escaped with a
+ * preceding backslash), and the parameter name must be one of the following
+ * strings:
+ * <UL>
+ * <LI>graphic</LI>
+ * <LI>control</LI>
+ * <LI>misc</LI>
+ * <LI>page</LI>
+ * <LI>private</LI>
+ * </UL>
+ */
+final class TeletexTerminalIdentifierSyntaxImpl extends AbstractSyntaxImpl {
+    /**
+     * The set of allowed fax parameter values, formatted entirely in lowercase
+     * characters.
+     */
+    private static final HashSet<String> ALLOWED_TTX_PARAMETERS = new HashSet<>(5);
+
+    static {
+        ALLOWED_TTX_PARAMETERS.add("graphic");
+        ALLOWED_TTX_PARAMETERS.add("control");
+        ALLOWED_TTX_PARAMETERS.add("misc");
+        ALLOWED_TTX_PARAMETERS.add("page");
+        ALLOWED_TTX_PARAMETERS.add("private");
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_TELETEX_TERM_ID_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get a lowercase string representation of the value and find its
+        // length.
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+
+        // The value must contain at least one character.
+        if (valueLength == 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_EMPTY.get());
+            return false;
+        }
+
+        // The first character must be a printable string character.
+        char c = valueString.charAt(0);
+        if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE.get(valueString, String
+                    .valueOf(c), 0));
+            return false;
+        }
+
+        // Continue reading until we find a dollar sign or the end of the
+        // string. Every intermediate character must be a printable string
+        // character.
+        int pos = 1;
+        for (; pos < valueLength; pos++) {
+            c = valueString.charAt(pos);
+            if (c == '$') {
+                pos++;
+                break;
+            } else if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE.get(valueString, c, pos));
+            }
+        }
+
+        if (pos >= valueLength) {
+            // We're at the end of the value, so it must be valid unless the
+            // last character was a dollar sign.
+            if (c == '$') {
+                invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR.get(valueString));
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        // Continue reading until we find the end of the string. Each
+        // substring must be a valid teletex terminal identifier parameter
+        // followed by a colon and the value. Dollar signs must be escaped
+        int paramStartPos = pos;
+        boolean escaped = false;
+        while (pos < valueLength) {
+            if (escaped) {
+                pos++;
+                continue;
+            }
+
+            c = valueString.charAt(pos++);
+            if (c == '\\') {
+                escaped = true;
+                continue;
+            } else if (c == '$') {
+                final String paramStr = valueString.substring(paramStartPos, pos);
+
+                final int colonPos = paramStr.indexOf(':');
+                if (colonPos < 0) {
+                    invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON.get(valueString));
+                    return false;
+                }
+
+                final String paramName = paramStr.substring(0, colonPos);
+                if (!ALLOWED_TTX_PARAMETERS.contains(paramName)) {
+                    invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER.get(
+                            valueString, paramName));
+                    return false;
+                }
+
+                paramStartPos = pos;
+            }
+        }
+
+        // We must be at the end of the value. Read the last parameter and
+        // make sure it is valid.
+        final String paramStr = valueString.substring(paramStartPos);
+        final int colonPos = paramStr.indexOf(':');
+        if (colonPos < 0) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON.get(valueString));
+            return false;
+        }
+
+        final String paramName = paramStr.substring(0, colonPos);
+        if (!ALLOWED_TTX_PARAMETERS.contains(paramName)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER.get(valueString,
+                    paramName));
+            return false;
+        }
+
+        // If we've gotten here, then the value must be valid.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelexNumberSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelexNumberSyntaxImpl.java
new file mode 100644
index 0000000..1a87ae2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TelexNumberSyntaxImpl.java
@@ -0,0 +1,150 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEX_TOO_SHORT;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_TELEX_TRUNCATED;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_TELEX_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the telex number attribute syntax, which contains three
+ * printable strings separated by dollar sign characters. Equality, ordering,
+ * and substring matching will be allowed by default.
+ */
+final class TelexNumberSyntaxImpl extends AbstractSyntaxImpl {
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_TELEX_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get a string representation of the value and find its length.
+        final String valueString = value.toString();
+        final int valueLength = valueString.length();
+
+        if (valueLength < 5) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_TOO_SHORT.get(valueString));
+            return false;
+        }
+
+        // The first character must be a printable string character.
+        char c = valueString.charAt(0);
+        if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE.get(valueString, String
+                    .valueOf(c), 0));
+            return false;
+        }
+
+        // Continue reading until we find a dollar sign. Every intermediate
+        // character must be a printable string character.
+        int pos = 1;
+        for (; pos < valueLength; pos++) {
+            c = valueString.charAt(pos);
+            if (c == '$') {
+                pos++;
+                break;
+            } else if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR.get(valueString, c, pos));
+            }
+        }
+
+        if (pos >= valueLength) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_TRUNCATED.get(valueString));
+            return false;
+        }
+
+        // The next character must be a printable string character.
+        c = valueString.charAt(pos++);
+        if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE.get(valueString, c, pos - 1));
+            return false;
+        }
+
+        // Continue reading until we find another dollar sign. Every
+        // intermediate character must be a printable string character.
+        for (; pos < valueLength; pos++) {
+            c = valueString.charAt(pos);
+            if (c == '$') {
+                pos++;
+                break;
+            } else if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR.get(valueString, c, pos));
+                return false;
+            }
+        }
+
+        if (pos >= valueLength) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_TRUNCATED.get(valueString));
+            return false;
+        }
+
+        // The next character must be a printable string character.
+        c = valueString.charAt(pos++);
+        if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+            invalidReason.append(ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE.get(valueString, String
+                    .valueOf(c), pos - 1));
+            return false;
+        }
+
+        // Continue reading until the end of the value. Every intermediate
+        // character must be a printable string character.
+        for (; pos < valueLength; pos++) {
+            c = valueString.charAt(pos);
+            if (!PrintableStringSyntaxImpl.isPrintableCharacter(c)) {
+                invalidReason.append(ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR.get(valueString, String
+                        .valueOf(c), pos));
+                return false;
+            }
+        }
+
+        // If we've gotten here, then we're at the end of the value and it
+        // is acceptable.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java
new file mode 100644
index 0000000..e3bc58c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/TimeBasedMatchingRulesImpl.java
@@ -0,0 +1,588 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteSequenceReader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+import org.forgerock.util.time.TimeService;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+import static org.forgerock.opendj.ldap.DecodeException.*;
+import static org.forgerock.opendj.ldap.schema.GeneralizedTimeEqualityMatchingRuleImpl.createNormalizedAttributeValue;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+/** Implementations of time-based matching rules. */
+final class TimeBasedMatchingRulesImpl {
+
+    private static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("UTC");
+
+    /** Constants for generating keys. */
+    private static final char SECOND = 's';
+    private static final char MINUTE = 'm';
+    private static final char HOUR = 'h';
+    private static final char MONTH = 'M';
+    private static final char DAY = 'D';
+    private static final char YEAR = 'Y';
+
+    private TimeBasedMatchingRulesImpl() {
+        // not instantiable
+    }
+
+    /**
+     * Creates a relative time greater than matching rule.
+     *
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl relativeTimeGTOMatchingRule() {
+        return new RelativeTimeGreaterThanOrderingMatchingRuleImpl();
+    }
+
+    /**
+     * Creates a relative time less than matching rule.
+     *
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl relativeTimeLTOMatchingRule() {
+        return new RelativeTimeLessThanOrderingMatchingRuleImpl();
+    }
+
+    /**
+     * Creates a partial date and time matching rule.
+     *
+     * @return the matching rule implementation
+     */
+    static MatchingRuleImpl partialDateAndTimeMatchingRule() {
+        return new PartialDateAndTimeMatchingRuleImpl();
+    }
+
+    /** This class defines a matching rule which is used for time-based searches. */
+    private static abstract class TimeBasedMatchingRuleImpl extends AbstractMatchingRuleImpl {
+
+        /** Unit tests can inject fake timestamps if necessary. */
+        final TimeService timeService = TimeService.SYSTEM;
+
+        @Override
+        public final ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException {
+            return GeneralizedTimeEqualityMatchingRuleImpl.normalizeAttributeValue(value);
+        }
+
+        /** Utility method to convert the provided integer and the provided byte representing a digit to an integer. */
+        int multiplyByTenAndAddUnits(int number, byte digitByte) {
+            return number * 10 + (digitByte - '0');
+        }
+    }
+
+    /** Defines the relative time ordering matching rule. */
+    private static abstract class RelativeTimeOrderingMatchingRuleImpl extends TimeBasedMatchingRuleImpl {
+
+        final Indexer indexer = new DefaultIndexer(EMR_GENERALIZED_TIME_NAME);
+
+        @Override
+        public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+            return Collections.singletonList(indexer);
+        }
+
+        /**
+         * Normalize the provided assertion value.
+         * <p>
+         * An assertion value may contain one of the following:
+         * <pre>
+         * s = second
+         * m = minute
+         * h = hour
+         * d = day
+         * w = week
+         * </pre>
+         *
+         * An example assertion is
+         * <pre>
+         *   OID:=(-)1d
+         * </pre>
+         *
+         * where a '-' means that the user intends to search only the expired
+         * events. In this example we are searching for an event expired 1 day
+         * back.
+         * <p>
+         * This method takes the assertion value adds/substracts it to/from the
+         * current time and calculates a time which will be used as a relative
+         * time by inherited rules.
+         */
+        ByteString normalizeAssertionValue(ByteSequence assertionValue) throws DecodeException {
+            int index = 0;
+            boolean signed = false;
+            byte firstByte = assertionValue.byteAt(0);
+
+            if (firstByte == '-') {
+                // Turn the sign on to go back in past.
+                signed = true;
+                index = 1;
+            } else if (firstByte == '+') {
+                // '+" is not required but we won't reject it either.
+                index = 1;
+            }
+
+            long second = 0;
+            long minute = 0;
+            long hour = 0;
+            long day = 0;
+            long week = 0;
+
+            boolean containsTimeUnit = false;
+            int number = 0;
+
+            for (; index < assertionValue.length(); index++) {
+                byte b = assertionValue.byteAt(index);
+                if (isDigit((char) b)) {
+                    number = multiplyByTenAndAddUnits(number, b);
+                } else {
+                    if (containsTimeUnit) {
+                        // We already have time unit found by now.
+                        throw error(WARN_ATTR_CONFLICTING_ASSERTION_FORMAT.get(assertionValue));
+                    }
+                    switch (b) {
+                    case 's':
+                        second = number;
+                        break;
+                    case 'm':
+                        minute = number;
+                        break;
+                    case 'h':
+                        hour = number;
+                        break;
+                    case 'd':
+                        day = number;
+                        break;
+                    case 'w':
+                        week = number;
+                        break;
+                    default:
+                        throw error(WARN_ATTR_INVALID_RELATIVE_TIME_ASSERTION_FORMAT.get(assertionValue, (char) b));
+                    }
+                    containsTimeUnit = true;
+                    number = 0;
+                }
+            }
+
+            if (!containsTimeUnit) {
+                // There was no time unit so assume it is seconds.
+                second = number;
+            }
+
+            long delta = (second + minute * 60 + hour * 3600 + day * 24 * 3600 + week * 7 * 24 * 3600) * 1000;
+            long now = timeService.now();
+            return createNormalizedAttributeValue(signed ? now - delta : now + delta);
+        }
+
+    }
+
+    /** Defines the "greater-than" relative time matching rule. */
+    private static final class RelativeTimeGreaterThanOrderingMatchingRuleImpl extends
+        RelativeTimeOrderingMatchingRuleImpl {
+
+        @Override
+        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
+            final ByteString assertionValue = normalizeAssertionValue(value);
+
+            return new Assertion() {
+                @Override
+                public ConditionResult matches(ByteSequence attributeValue) {
+                    return ConditionResult.valueOf(attributeValue.compareTo(assertionValue) > 0);
+                }
+
+                @Override
+                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                    return factory.createRangeMatchQuery(indexer.getIndexID(), assertionValue, ByteString.empty(),
+                        false, false);
+                }
+            };
+        }
+    }
+
+    /** Defines the "less-than" relative time matching rule. */
+    private static final class RelativeTimeLessThanOrderingMatchingRuleImpl extends
+        RelativeTimeOrderingMatchingRuleImpl {
+
+        @Override
+        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
+            final ByteString assertionValue = normalizeAssertionValue(value);
+
+            return new Assertion() {
+                @Override
+                public ConditionResult matches(ByteSequence attributeValue) {
+                    return ConditionResult.valueOf(attributeValue.compareTo(assertionValue) < 0);
+                }
+
+                @Override
+                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                    return factory.createRangeMatchQuery(indexer.getIndexID(), ByteString.empty(), assertionValue,
+                        false, false);
+                }
+            };
+        }
+    }
+
+    /** Defines the partial date and time matching rule. */
+    private static final class PartialDateAndTimeMatchingRuleImpl extends TimeBasedMatchingRuleImpl {
+
+        private final Indexer indexer = new PartialDateAndTimeIndexer(this);
+
+        @Override
+        public Collection<? extends Indexer> createIndexers(IndexingOptions options) {
+            return Collections.singletonList(indexer);
+        }
+
+        @Override
+        public Assertion getAssertion(final Schema schema, final ByteSequence value) throws DecodeException {
+            final ByteString assertionValue = normalizeAssertionValue(value);
+
+            return new Assertion() {
+                @Override
+                public ConditionResult matches(ByteSequence attributeValue) {
+                    return valuesMatch(attributeValue, assertionValue);
+                }
+
+                @Override
+                public <T> T createIndexQuery(IndexQueryFactory<T> factory) throws DecodeException {
+                    final ByteSequenceReader reader = assertionValue.asReader();
+                    int assertSecond = reader.readByte();
+                    int assertMinute = reader.readByte();
+                    int assertHour = reader.readByte();
+                    int assertDay = reader.readByte();
+                    int assertMonth = reader.readByte();
+                    int assertYear = reader.readCompactUnsignedInt();
+
+                    List<T> queries = new ArrayList<>();
+                    if (assertSecond >= 0) {
+                        queries.add(createExactMatchByteQuery(factory, assertSecond, SECOND));
+                    }
+                    if (assertMinute >= 0) {
+                        queries.add(createExactMatchByteQuery(factory, assertMinute, MINUTE));
+                    }
+                    if (assertHour >= 0) {
+                        queries.add(createExactMatchByteQuery(factory, assertHour, HOUR));
+                    }
+                    if (assertDay > 0) {
+                        queries.add(createExactMatchByteQuery(factory, assertDay, DAY));
+                    }
+                    if (assertMonth >= 0) {
+                        queries.add(createExactMatchByteQuery(factory, assertMonth, MONTH));
+                    }
+                    if (assertYear > 0) {
+                        queries.add(createExactMatchCompactUnsignedQuery(factory, assertYear, YEAR));
+                    }
+                    return factory.createIntersectionQuery(queries);
+                }
+
+                private <T> T createExactMatchByteQuery(
+                        IndexQueryFactory<T> factory, int assertionValue, char type) {
+                    return factory.createExactMatchQuery(
+                            indexer.getIndexID(), byteKey(assertionValue, type));
+                }
+
+                private <T> T createExactMatchCompactUnsignedQuery(
+                        IndexQueryFactory<T> factory, int assertionValue, char type) {
+                    return factory.createExactMatchQuery(
+                            indexer.getIndexID(), compactUnsignedKey(assertionValue, type));
+                }
+            };
+        }
+
+        /**
+         * Normalize the provided assertion value.
+         * <p>
+         * An assertion value may contain one or all of the following:
+         * <pre>
+         * D = day
+         * M = month
+         * Y = year
+         * h = hour
+         * m = month
+         * s = second
+         * </pre>
+         *
+         * An example assertion is
+         * <pre>
+         *  OID:=04M
+         * </pre>
+         * In this example we are searching for entries corresponding to month
+         * of april.
+         * <p>
+         * The normalized value is actually the format of : smhDMY.
+         */
+        private ByteString normalizeAssertionValue(ByteSequence assertionValue) throws DecodeException {
+            final int initDay = 0;
+            final int initValue = -1;
+            int second = initValue;
+            int minute = initValue;
+            int hour = initValue;
+            int day = initDay;
+            int month = initValue;
+            int year = initDay;
+            int number = 0;
+
+            int length = assertionValue.length();
+            for (int index = 0; index < length; index++) {
+                byte b = assertionValue.byteAt(index);
+                if (isDigit((char) b)) {
+                    number = multiplyByTenAndAddUnits(number, b);
+                } else {
+                    switch (b) {
+                    case 's':
+                        if (second != initValue) {
+                            throw error(WARN_ATTR_DUPLICATE_SECOND_ASSERTION_FORMAT.get(assertionValue, day));
+                        }
+                        second = number;
+                        break;
+                    case 'm':
+                        if (minute != initValue) {
+                            throw error(WARN_ATTR_DUPLICATE_MINUTE_ASSERTION_FORMAT.get(assertionValue, day));
+                        }
+                        minute = number;
+                        break;
+                    case 'h':
+                        if (hour != initValue) {
+                            throw error(WARN_ATTR_DUPLICATE_HOUR_ASSERTION_FORMAT.get(assertionValue, day));
+                        }
+                        hour = number;
+                        break;
+                    case 'D':
+                        if (number == 0) {
+                            throw error(WARN_ATTR_INVALID_DAY_ASSERTION_FORMAT.get(assertionValue, number));
+                        } else if (day != initDay) {
+                            throw error(WARN_ATTR_DUPLICATE_DAY_ASSERTION_FORMAT.get(assertionValue, day));
+                        }
+                        day = number;
+                        break;
+                    case 'M':
+                        if (number == 0) {
+                            throw error(WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT.get(assertionValue, number));
+                        } else if (month != initValue) {
+                            throw error(WARN_ATTR_DUPLICATE_MONTH_ASSERTION_FORMAT.get(assertionValue, month));
+                        }
+                        month = number;
+                        break;
+                    case 'Y':
+                        if (number == 0) {
+                            throw error(WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT.get(assertionValue, number));
+                        } else if (year != initDay) {
+                            throw error(WARN_ATTR_DUPLICATE_YEAR_ASSERTION_FORMAT.get(assertionValue, year));
+                        }
+                        year = number;
+                        break;
+                    default:
+                        throw error(WARN_ATTR_INVALID_PARTIAL_TIME_ASSERTION_FORMAT.get(assertionValue, (char) b));
+                    }
+                    number = 0;
+                }
+            }
+
+            month = toCalendarMonth(month, assertionValue);
+
+            // Validate year, month , day , hour, minute and second in that order.
+            // -1 values are allowed when these values have not been provided
+            if (year < 0) {
+                // A future date is allowed.
+                throw error(WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT.get(assertionValue, year));
+            }
+            if (isDayInvalid(day, month, year)) {
+                throw error(WARN_ATTR_INVALID_DAY_ASSERTION_FORMAT.get(assertionValue, day));
+            }
+            if (hour < initValue || hour > 23) {
+                throw error(WARN_ATTR_INVALID_HOUR_ASSERTION_FORMAT.get(assertionValue, hour));
+            }
+            if (minute < initValue || minute > 59) {
+                throw error(WARN_ATTR_INVALID_MINUTE_ASSERTION_FORMAT.get(assertionValue, minute));
+            }
+            // Consider leap seconds.
+            if (second < initValue || second > 60) {
+                throw error(WARN_ATTR_INVALID_SECOND_ASSERTION_FORMAT.get(assertionValue, second));
+            }
+
+            // Since we reached here we have a valid assertion value.
+            // Construct a normalized value in the order: SECOND MINUTE HOUR DAY MONTH YEAR.
+            // Using compact unsigned for year will use only two bytes until the year 16384 :)
+            return new ByteStringBuilder(5 + 2)
+                .appendByte(second).appendByte(minute).appendByte(hour)
+                .appendByte(day).appendByte(month).appendCompactUnsigned(year).toByteString();
+        }
+
+        private boolean isDayInvalid(int day, int month, int year) {
+            switch (day) {
+            case 29:
+                return month == Calendar.FEBRUARY && !isLeapYear(year);
+            case 30:
+                return month == Calendar.FEBRUARY;
+            case 31:
+                return month != -1
+                    && month != Calendar.JANUARY
+                    && month != Calendar.MARCH
+                    && month != Calendar.MAY
+                    && month != Calendar.JULY
+                    && month != Calendar.AUGUST
+                    && month != Calendar.OCTOBER
+                    && month != Calendar.DECEMBER;
+            default:
+                return day < 0 || day > 31;
+            }
+        }
+
+        private boolean isLeapYear(int year) {
+            return year % 400 == 0 || (year % 100 != 0 && year % 4 == 0);
+        }
+
+        private int toCalendarMonth(int month, ByteSequence value) throws DecodeException {
+            if (month == -1) {
+                // just allow this.
+                return -1;
+            } else if (1 <= month && month <= 12) {
+                // java.util.Calendar months are numbered from 0
+                return month - 1;
+            }
+            throw error(WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT.get(value, month));
+        }
+
+        private ConditionResult valuesMatch(ByteSequence attributeValue, ByteSequence assertionValue) {
+            // Build the information from the attribute value.
+            GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC);
+            cal.setLenient(false);
+            cal.setTimeInMillis(attributeValue.toByteString().toLong() + GeneralizedTime.MIN_GENERALIZED_TIME_MS);
+            int second = cal.get(Calendar.SECOND);
+            int minute = cal.get(Calendar.MINUTE);
+            int hour = cal.get(Calendar.HOUR_OF_DAY);
+            int day = cal.get(Calendar.DAY_OF_MONTH);
+            int month = cal.get(Calendar.MONTH);
+            int year = cal.get(Calendar.YEAR);
+
+            // Build the information from the assertion value.
+            ByteSequenceReader r = assertionValue.asReader();
+            int assertSecond = r.readByte();
+            int assertMinute = r.readByte();
+            int assertHour = r.readByte();
+            int assertDay = r.readByte();
+            int assertMonth = r.readByte();
+            int assertYear = r.readCompactUnsignedInt();
+
+            // All the non-zero and non -1 values should match.
+            return ConditionResult.valueOf(
+                (assertSecond == -1 || assertSecond == second)
+                    && (assertMinute == -1 || assertMinute == minute)
+                    && (assertHour == -1 || assertHour == hour)
+                    && (assertDay == 0 || assertDay == day)
+                    && (assertMonth == -1 || assertMonth == month)
+                    && (assertYear == 0 || assertYear == year));
+        }
+
+        /**
+         * Decomposes an attribute value into a set of partial date and time
+         * index keys.
+         *
+         * @param attributeValue
+         *            The normalized attribute value
+         * @param keys
+         *            A set into which the keys will be inserted.
+         */
+        private void timeKeys(ByteSequence attributeValue, Collection<ByteString> keys) {
+            long timeInMillis = 0L;
+            try {
+                timeInMillis = GeneralizedTime.valueOf(attributeValue.toString()).getTimeInMillis();
+            } catch (IllegalArgumentException e) {
+                return;
+            }
+            GregorianCalendar cal = new GregorianCalendar(TIME_ZONE_UTC);
+            cal.setTimeInMillis(timeInMillis);
+            addKeyIfNotZero(keys, cal, Calendar.SECOND, SECOND);
+            addKeyIfNotZero(keys, cal, Calendar.MINUTE, MINUTE);
+            addKeyIfNotZero(keys, cal, Calendar.HOUR_OF_DAY, HOUR);
+            addKeyIfNotZero(keys, cal, Calendar.DAY_OF_MONTH, DAY);
+            addKeyIfNotZero(keys, cal, Calendar.MONTH, MONTH);
+            addKeyIfNotZero(keys, cal, Calendar.YEAR, YEAR);
+        }
+
+        private void addKeyIfNotZero(Collection<ByteString> keys, GregorianCalendar cal, int calField, char type) {
+            int value = cal.get(calField);
+            if (value >= 0) {
+                switch (type) {
+                case SECOND:
+                case MINUTE:
+                case HOUR:
+                case DAY:
+                case MONTH:
+                    keys.add(byteKey(value, type));
+                    break;
+
+                case YEAR:
+                    keys.add(compactUnsignedKey(value, type));
+                    break;
+
+                default:
+                    break;
+                }
+            }
+        }
+
+        private ByteString byteKey(int value, char type) {
+            return new ByteStringBuilder().appendInt(type).appendByte(value).toByteString();
+        }
+
+        private ByteString compactUnsignedKey(long value, char type) {
+            return new ByteStringBuilder().appendInt(type).appendCompactUnsigned(value).toByteString();
+        }
+    }
+
+    /** Indexer for Partial Date and Time Matching rules. */
+    private static final class PartialDateAndTimeIndexer implements Indexer {
+
+        private final PartialDateAndTimeMatchingRuleImpl matchingRule;
+
+        private PartialDateAndTimeIndexer(PartialDateAndTimeMatchingRuleImpl matchingRule) {
+            this.matchingRule = matchingRule;
+        }
+
+        @Override
+        public void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) {
+            matchingRule.timeKeys(value, keys);
+        }
+
+        @Override
+        public String keyToHumanReadableString(ByteSequence key) {
+            return key.toByteString().toHexString();
+        }
+
+        @Override
+        public String getIndexID() {
+            return MR_PARTIAL_DATE_AND_TIME_NAME;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxImpl.java
new file mode 100644
index 0000000..321e4cb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxImpl.java
@@ -0,0 +1,695 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class implements the UTC time attribute syntax. This is very similar to
+ * the generalized time syntax (and actually has been deprecated in favor of
+ * that), but requires that the minute be provided and does not allow for
+ * sub-second times. All matching will be performed using the generalized time
+ * matching rules, and equality, ordering, and substring matching will be
+ * allowed.
+ */
+final class UTCTimeSyntaxImpl extends AbstractSyntaxImpl {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * The lock that will be used to provide threadsafe access to the date
+     * formatter.
+     */
+    private static final Object DATE_FORMAT_LOCK;
+
+    /**
+     * The date formatter that will be used to convert dates into UTC time
+     * values. Note that all interaction with it must be synchronized.
+     */
+    private static final SimpleDateFormat DATE_FORMAT;
+
+    /**
+     * The date formatter needs help converting 2-digit years.
+     */
+    private static Date datum1900;
+    private static Date datum2000;
+
+    /*
+     * Create the date formatter that will be used to construct and parse
+     * normalized UTC time values.
+     */
+    static {
+        DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_UTC_TIME);
+        DATE_FORMAT.setLenient(false);
+        DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(TIME_ZONE_UTC));
+
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(TIME_ZONE_UTC));
+        cal.clear();
+        cal.set(1900, 0, 1);
+        datum1900 = cal.getTime();
+
+        cal.clear();
+        cal.set(2000, 0, 1);
+        datum2000 = cal.getTime();
+
+        DATE_FORMAT_LOCK = new Object();
+    }
+
+    /**
+     * Retrieves an string containing a UTC time representation of the provided
+     * date.
+     *
+     * @param d
+     *            The date for which to retrieve the UTC time value.
+     * @return The attribute value created from the date.
+     */
+    static String createUTCTimeValue(final Date d) {
+        synchronized (DATE_FORMAT_LOCK) {
+            return DATE_FORMAT.format(d);
+        }
+    }
+
+    /**
+     * Decodes the provided normalized value as a UTC time value and retrieves a
+     * Java <CODE>Date</CODE> object containing its representation.
+     *
+     * @param valueString
+     *            The normalized UTC time value to decode to a Java
+     *            <CODE>Date</CODE>.
+     * @return The Java <CODE>Date</CODE> created from the provided UTC time
+     *         value.
+     * @throws DecodeException
+     *             If the provided value cannot be parsed as a valid UTC time
+     *             string.
+     */
+    static Date decodeUTCTimeValue(final String valueString) throws DecodeException {
+        try {
+            synchronized (DATE_FORMAT_LOCK) {
+                // RFC 3280 4.1.2.5.1. defines the datum we need to
+                // set for the parser.
+                switch (valueString.charAt(0)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                    // 00-49
+                    DATE_FORMAT.set2DigitYearStart(datum2000);
+                    break;
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                default:
+                    // 50-99
+                    DATE_FORMAT.set2DigitYearStart(datum1900);
+                    break;
+                }
+                return DATE_FORMAT.parse(valueString);
+            }
+        } catch (final Exception e) {
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE.get(valueString, e);
+            final DecodeException de = DecodeException.error(message, e);
+            logger.debug(LocalizableMessage.raw("%s", de));
+            throw de;
+        }
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_GENERALIZED_TIME_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_UTC_TIME_NAME;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_GENERALIZED_TIME_OID;
+    }
+
+    @Override
+    public String getSubstringMatchingRule() {
+        return SMR_CASE_IGNORE_OID;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return false;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // Get the value as a string and verify that it is at least long
+        // enough for "YYYYMMDDhhmmZ", which is the shortest allowed value.
+        final String valueString = value.toString().toUpperCase();
+        final int length = valueString.length();
+        if (length < 11) {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT.get(valueString);
+            invalidReason.append(message);
+            return false;
+        }
+
+        // The first two characters are the year, and they must be numeric
+        // digits between 0 and 9.
+        for (int i = 0; i < 2; i++) {
+            switch (valueString.charAt(i)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR.get(valueString, String
+                                .valueOf(valueString.charAt(i)));
+                invalidReason.append(message);
+                return false;
+            }
+        }
+
+        // The next two characters are the month, and they must form the
+        // string representation of an integer between 01 and 12.
+        char m1 = valueString.charAt(2);
+        final char m2 = valueString.charAt(3);
+        switch (m1) {
+        case '0':
+            // m2 must be a digit between 1 and 9.
+            switch (m2) {
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH.get(valueString, valueString
+                                .substring(2, 4));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        case '1':
+            // m2 must be a digit between 0 and 2.
+            switch (m2) {
+            case '0':
+            case '1':
+            case '2':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH.get(valueString, valueString
+                                .substring(2, 4));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH.get(valueString, valueString.substring(
+                            2, 4));
+            invalidReason.append(message);
+            return false;
+        }
+
+        // The next two characters should be the day of the month, and they
+        // must form the string representation of an integer between 01 and
+        // 31. This doesn't do any validation against the year or month, so
+        // it will allow dates like April 31, or February 29 in a non-leap
+        // year, but we'll let those slide.
+        final char d1 = valueString.charAt(4);
+        final char d2 = valueString.charAt(5);
+        switch (d1) {
+        case '0':
+            // d2 must be a digit between 1 and 9.
+            switch (d2) {
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY.get(valueString, valueString
+                                .substring(4, 6));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        case '1':
+            // Treated the same as '2'.
+        case '2':
+            // d2 must be a digit between 0 and 9.
+            switch (d2) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY.get(valueString, valueString
+                                .substring(4, 6));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        case '3':
+            // d2 must be either 0 or 1.
+            switch (d2) {
+            case '0':
+            case '1':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY.get(valueString, valueString
+                                .substring(4, 6));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY.get(valueString, valueString.substring(4,
+                            6));
+            invalidReason.append(message);
+            return false;
+        }
+
+        // The next two characters must be the hour, and they must form the
+        // string representation of an integer between 00 and 23.
+        final char h1 = valueString.charAt(6);
+        final char h2 = valueString.charAt(7);
+        switch (h1) {
+        case '0':
+            // This is treated the same as '1'.
+        case '1':
+            switch (h2) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR.get(valueString, valueString
+                                .substring(6, 8));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        case '2':
+            // This must be a digit between 0 and 3.
+            switch (h2) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR.get(valueString, valueString
+                                .substring(6, 8));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR.get(valueString, valueString.substring(6,
+                            8));
+            invalidReason.append(message);
+            return false;
+        }
+
+        // Next, there should be two digits comprising an integer between 00
+        // and 59 for the minute.
+        m1 = valueString.charAt(8);
+        switch (m1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 11) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(m1),
+                                8);
+                invalidReason.append(message);
+                return false;
+            }
+
+            switch (valueString.charAt(9)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE.get(valueString, valueString
+                                .substring(8, 10));
+                invalidReason.append(message);
+                return false;
+            }
+
+            break;
+
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(m1), 8);
+            invalidReason.append(message);
+            return false;
+        }
+
+        // Next, there should be either two digits comprising an integer
+        // between 00 and 60 (for the second, including a possible leap
+        // second), a letter 'Z' (for the UTC specifier), or a plus or minus
+        // sign followed by four digits (for the UTC offset).
+        final char s1 = valueString.charAt(10);
+        switch (s1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 13) {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(s1),
+                                10);
+                invalidReason.append(message);
+                return false;
+            }
+
+            switch (valueString.charAt(11)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND.get(valueString, valueString
+                                .substring(10, 12));
+                invalidReason.append(message);
+                return false;
+            }
+
+            break;
+        case '6':
+            // There must be at least two more characters and the next one
+            // must be a 0.
+            if (length < 13) {
+
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(s1),
+                                10);
+                invalidReason.append(message);
+                return false;
+            }
+
+            if (valueString.charAt(11) != '0') {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND.get(valueString, valueString
+                                .substring(10, 12));
+                invalidReason.append(message);
+                return false;
+            }
+
+            break;
+        case 'Z':
+            // This is fine only if we are at the end of the value.
+            if (length == 11) {
+                return true;
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(s1),
+                                10);
+                invalidReason.append(message);
+                return false;
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly four more digits that
+            // specify a valid offset.
+            if (length == 15) {
+                return hasValidOffset(valueString, 11, invalidReason);
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(s1),
+                                10);
+                invalidReason.append(message);
+                return false;
+            }
+
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String.valueOf(s1), 10);
+            invalidReason.append(message);
+            return false;
+        }
+
+        // The last element should be either a letter 'Z' (for the UTC
+        // specifier), or a plus or minus sign followed by four digits (for
+        // the UTC offset).
+        switch (valueString.charAt(12)) {
+        case 'Z':
+            // This is fine only if we are at the end of the value.
+            if (length == 13) {
+                return true;
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String
+                                .valueOf(valueString.charAt(12)), 12);
+                invalidReason.append(message);
+                return false;
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are four or two more digits that
+            // specify a valid offset.
+            if (length == 17 || length == 15) {
+                return hasValidOffset(valueString, 13, invalidReason);
+            } else {
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String
+                                .valueOf(valueString.charAt(12)), 12);
+                invalidReason.append(message);
+                return false;
+            }
+
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR.get(valueString, String
+                            .valueOf(valueString.charAt(12)), 12);
+            invalidReason.append(message);
+            return false;
+        }
+    }
+
+    /**
+     * Indicates whether the provided string contains a valid set of two or four
+     * UTC offset digits. The provided string must have either two or four
+     * characters from the provided start position to the end of the value.
+     *
+     * @param value
+     *            The whole value, including the offset.
+     * @param startPos
+     *            The position of the first character that is contained in the
+     *            offset.
+     * @param invalidReason
+     *            The buffer to which the invalid reason may be appended if the
+     *            string does not contain a valid set of UTC offset digits.
+     * @return <CODE>true</CODE> if the provided offset string is valid, or
+     *         <CODE>false</CODE> if it is not.
+     */
+    private boolean hasValidOffset(final String value, final int startPos,
+            final LocalizableMessageBuilder invalidReason) {
+        final int offsetLength = value.length() - startPos;
+        if (offsetLength < 2) {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT.get(value);
+            invalidReason.append(message);
+            return false;
+        }
+
+        // The first two characters must be an integer between 00 and 23.
+        switch (value.charAt(startPos)) {
+        case '0':
+        case '1':
+            switch (value.charAt(startPos + 1)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET.get(value, value.substring(
+                                startPos, startPos + offsetLength));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        case '2':
+            switch (value.charAt(startPos + 1)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+                // These are all fine.
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET.get(value, value.substring(
+                                startPos, startPos + offsetLength));
+                invalidReason.append(message);
+                return false;
+            }
+            break;
+        default:
+            final LocalizableMessage message =
+                    ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET.get(value, value.substring(startPos,
+                            startPos + offsetLength));
+            invalidReason.append(message);
+            return false;
+        }
+
+        // If there are two more characters, then they must be an integer
+        // between 00 and 59.
+        if (offsetLength == 4) {
+            switch (value.charAt(startPos + 2)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+                switch (value.charAt(startPos + 3)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    // These are all fine.
+                    break;
+                default:
+                    final LocalizableMessage message =
+                            ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET.get(value, value.substring(
+                                    startPos, startPos + offsetLength));
+                    invalidReason.append(message);
+                    return false;
+                }
+                break;
+            default:
+                final LocalizableMessage message =
+                        ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET.get(value, value.substring(
+                                startPos, startPos + offsetLength));
+                invalidReason.append(message);
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..f1b2b5c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleImpl.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_EXPECTED_DASH;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_EXPECTED_HEX;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_INVALID_LENGTH;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_UUID_NAME;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteSequenceReader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+
+/**
+ * This class defines the uuidMatch matching rule defined in RFC 4530. It will
+ * be used as the default equality matching rule for the UUID syntax.
+ */
+final class UUIDEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
+    /**
+     * An optimized hash based indexer which stores a 4 byte hash of the UUID. Mix the bytes using XOR similar
+     * to UUID.hashCode(): 128 down to 64 then 32.
+     */
+    private static final Collection<? extends Indexer> INDEXERS = Collections.singleton(new Indexer() {
+        @Override
+        public void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) throws DecodeException {
+            keys.add(hash(normalize(value)));
+        }
+
+        @Override
+        public String keyToHumanReadableString(ByteSequence key) {
+            return key.toByteString().toHexString();
+        }
+
+        @Override
+        public String getIndexID() {
+            return EMR_UUID_NAME;
+        }
+    });
+
+    UUIDEqualityMatchingRuleImpl() {
+        // Nothing to do.
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        return normalize(value).toByteString();
+    }
+
+    @Override
+    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
+        final ByteString normalizedAssertionValue = normalizeAttributeValue(schema, assertionValue);
+        return new Assertion() {
+            @Override
+            public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
+                return ConditionResult.valueOf(normalizedAssertionValue.equals(normalizedAttributeValue));
+            }
+
+            @Override
+            public <T> T createIndexQuery(final IndexQueryFactory<T> factory) throws DecodeException {
+                return factory.createExactMatchQuery(EMR_UUID_NAME, hash(normalizedAssertionValue));
+            }
+        };
+    }
+
+    @Override
+    public Collection<? extends Indexer> createIndexers(final IndexingOptions options) {
+        return INDEXERS;
+    }
+
+    // Package private so that it can be reused by ordering matching rule.
+    static ByteSequence normalize(final ByteSequence value) throws DecodeException {
+        if (value.length() != 36) {
+            throw DecodeException.error(WARN_ATTR_SYNTAX_UUID_INVALID_LENGTH.get(value, value.length()));
+        }
+        final ByteStringBuilder builder = new ByteStringBuilder(16);
+        for (int i = 0; i < 36; i++) {
+            // The 9th, 14th, 19th, and 24th characters must be dashes. All others must be hex.
+            switch (i) {
+            case 8:
+            case 13:
+            case 18:
+            case 23:
+                final char c = (char) value.byteAt(i);
+                if (c != '-') {
+                    throw DecodeException.error(WARN_ATTR_SYNTAX_UUID_EXPECTED_DASH.get(value, i, c));
+                }
+                break;
+            default:
+                final int high4Bits = decodeHexByte(value, i++);
+                final int low4Bits = decodeHexByte(value, i);
+                builder.appendByte((high4Bits << 4) | low4Bits);
+                break;
+            }
+        }
+        return builder;
+    }
+
+    private static int decodeHexByte(final ByteSequence value, final int i) throws DecodeException {
+        final char c = (char) value.byteAt(i);
+        switch (c) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+            return c - '0';
+        case 'a':
+        case 'b':
+        case 'c':
+        case 'd':
+        case 'e':
+        case 'f':
+            return c - 'a' + 10;
+        case 'A':
+        case 'B':
+        case 'C':
+        case 'D':
+        case 'E':
+        case 'F':
+            return c - 'A' + 10;
+        default:
+            throw DecodeException.error(WARN_ATTR_SYNTAX_UUID_EXPECTED_HEX.get(value, i, c));
+        }
+    }
+
+    private static ByteString hash(final ByteSequence normalizeAttributeValue) {
+        final ByteSequenceReader uuid128Bytes = normalizeAttributeValue.asReader();
+        final long uuidHigh64 = uuid128Bytes.readLong();
+        final long uuidLow64 = uuid128Bytes.readLong();
+        final long uuid64 = uuidHigh64 ^ uuidLow64;
+        final int hash32 = ((int) (uuid64 >> 32)) ^ (int) uuid64;
+        return ByteString.valueOfInt(hash32);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleImpl.java
new file mode 100644
index 0000000..3374dd7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleImpl.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines the uuidOrderingMatch matching rule defined in RFC 4530.
+ * This will be the default ordering matching rule for the UUID syntax.
+ */
+final class UUIDOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
+    UUIDOrderingMatchingRuleImpl() {
+        // Don't re-use equality matching rule index because it is hash based.
+        super(OMR_UUID_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        return UUIDEqualityMatchingRuleImpl.normalize(value).toByteString();
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxImpl.java
new file mode 100644
index 0000000..29911ad
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxImpl.java
@@ -0,0 +1,122 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_EXPECTED_DASH;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_EXPECTED_HEX;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_UUID_INVALID_LENGTH;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_UUID_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_UUID_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_UUID_NAME;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * This class implements the UUID syntax, which is defined in RFC 4530. Equality
+ * and ordering matching will be allowed by default.
+ */
+final class UUIDSyntaxImpl extends AbstractSyntaxImpl {
+    @Override
+    public String getName() {
+        return SYNTAX_UUID_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_UUID_OID;
+    }
+
+    @Override
+    public String getOrderingMatchingRule() {
+        return OMR_UUID_OID;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We will only accept values that look like valid UUIDs. This means
+        // that all values must be in the form
+        // HHHHHHHH-HHHH-HHHH-HHHH-HHHHHHHHHHHH, where "H" represents a
+        // hexadecimal digit. First, make sure that the value is exactly 36
+        // bytes long.
+        final String valueString = value.toString();
+        if (valueString.length() != 36) {
+            invalidReason.append(WARN_ATTR_SYNTAX_UUID_INVALID_LENGTH.get(valueString, valueString
+                    .length()));
+            return false;
+        }
+
+        // Next, iterate through each character. Make sure that the 9th,
+        // 14th, 19th, and 24th characters are dashes and the rest are hex
+        // digits.
+        for (int i = 0; i < 36; i++) {
+            switch (i) {
+            case 8:
+            case 13:
+            case 18:
+            case 23:
+                if (valueString.charAt(i) != '-') {
+                    invalidReason.append(WARN_ATTR_SYNTAX_UUID_EXPECTED_DASH.get(valueString, i,
+                            String.valueOf(valueString.charAt(i))));
+                    return false;
+                }
+                break;
+            default:
+                switch (valueString.charAt(i)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                case 'a':
+                case 'b':
+                case 'c':
+                case 'd':
+                case 'e':
+                case 'f':
+                case 'A':
+                case 'B':
+                case 'C':
+                case 'D':
+                case 'E':
+                case 'F':
+                    break;
+                default:
+
+                    invalidReason.append(WARN_ATTR_SYNTAX_UUID_EXPECTED_HEX.get(valueString, i,
+                            String.valueOf(valueString.charAt(i))));
+                    return false;
+                }
+            }
+        }
+
+        // If we've gotten here, then the value is acceptable.
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..66fbe16
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleImpl.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class implements the uniqueMemberMatch matching rule defined in X.520
+ * and referenced in RFC 4517. It is based on the name and optional UID syntax,
+ * and will compare values with a distinguished name and optional bit string (uid)
+ * suffix.
+ */
+final class UniqueMemberEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    UniqueMemberEqualityMatchingRuleImpl() {
+        super(EMR_UNIQUE_MEMBER_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
+        // Separate value into normalized DN and "optional uid" portion.
+        final String stringValue = value.toString().trim();
+        int dnEndPosition = stringValue.length();
+        String optionalUid = "";
+        int sharpPosition = -1;
+        if (stringValue.endsWith("'B") || stringValue.endsWith("'b")) {
+            sharpPosition = stringValue.lastIndexOf("#'");
+            if (sharpPosition > 0) {
+                dnEndPosition = sharpPosition;
+                optionalUid = stringValue.substring(sharpPosition);
+            }
+        }
+        try {
+            DN dn = DN.valueOf(stringValue.substring(0, dnEndPosition), schema.asNonStrictSchema());
+            return new ByteStringBuilder()
+                .appendBytes(dn.toNormalizedByteString())
+                .appendUtf8(optionalUid).toByteString();
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UnknownSchemaElementException.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UnknownSchemaElementException.java
new file mode 100644
index 0000000..0be2e73
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UnknownSchemaElementException.java
@@ -0,0 +1,37 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+/**
+ * Thrown when a schema query fails because the requested schema element could
+ * not be found or is ambiguous.
+ */
+@SuppressWarnings("serial")
+public class UnknownSchemaElementException extends LocalizedIllegalArgumentException {
+    /**
+     * Creates a new unknown schema element exception with the provided message.
+     *
+     * @param message
+     *            The message that explains the problem that occurred.
+     */
+    public UnknownSchemaElementException(final LocalizableMessage message) {
+        super(message);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleImpl.java
new file mode 100644
index 0000000..b473888
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleImpl.java
@@ -0,0 +1,68 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * This class implements the userPasswordExactMatch matching rule, which will
+ * simply compare encoded hashed password values to see if they are exactly
+ * equal to each other.
+ */
+final class UserPasswordExactEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
+
+    UserPasswordExactEqualityMatchingRuleImpl() {
+        super(EMR_USER_PASSWORD_EXACT_NAME);
+    }
+
+    @Override
+    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
+            throws DecodeException {
+        // The normalized form of this matching rule is exactly equal to the
+        // non-normalized form, except that the scheme needs to be converted
+        // to lowercase (if there is one).
+
+        if (UserPasswordSyntaxImpl.isEncoded(value)) {
+            final StringBuilder builder = new StringBuilder(value.length());
+            int closingBracePos = -1;
+            for (int i = 1; i < value.length(); i++) {
+                if (value.byteAt(i) == '}') {
+                    closingBracePos = i;
+                    break;
+                }
+            }
+            final ByteSequence seq1 = value.subSequence(0, closingBracePos + 1);
+            final ByteSequence seq2 = value.subSequence(closingBracePos + 1, value.length());
+            StaticUtils.toLowerCase(seq1, builder);
+            builder.append(seq2);
+            return ByteString.valueOfUtf8(builder);
+        } else {
+            return value.toByteString();
+        }
+    }
+
+    @Override
+    public String keyToHumanReadableString(ByteSequence key) {
+        return key.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordSyntaxImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordSyntaxImpl.java
new file mode 100644
index 0000000..155c4b6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/UserPasswordSyntaxImpl.java
@@ -0,0 +1,154 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_USERPW_NO_CLOSING_BRACE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_USERPW_NO_OPENING_BRACE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_USERPW_NO_SCHEME;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_USERPW_NO_VALUE;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_USER_PASSWORD_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_USER_PASSWORD_NAME;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * This class defines an attribute syntax used for storing values that have been
+ * encoded using a password storage scheme. The format for attribute values with
+ * this syntax is the concatenation of the following elements in the given
+ * order: <BR>
+ * <UL>
+ * <LI>An opening curly brace ("{") character.</LI>
+ * <LI>The name of the storage scheme used to encode the value.</LI>
+ * <LI>A closing curly brace ("}") character.</LI>
+ * <LI>The encoded value.</LI>
+ * </UL>
+ */
+final class UserPasswordSyntaxImpl extends AbstractSyntaxImpl {
+    /**
+     * Decodes the provided user password value into its component parts.
+     *
+     * @param userPasswordValue
+     *            The user password value to be decoded.
+     * @return A two-element string array whose elements are the storage scheme
+     *         name (in all lowercase characters) and the encoded value, in that
+     *         order.
+     * @throws DecodeException
+     *             If a problem is encountered while attempting to decode the
+     *             value.
+     */
+    static String[] decodeUserPassword(final String userPasswordValue) throws DecodeException {
+        // Make sure that there actually is a value to decode.
+        if (userPasswordValue == null || userPasswordValue.length() == 0) {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_USERPW_NO_VALUE.get();
+            throw DecodeException.error(message);
+        }
+
+        // The first character of an encoded value must be an opening curly
+        // brace.
+        if (userPasswordValue.charAt(0) != '{') {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_USERPW_NO_OPENING_BRACE.get();
+            throw DecodeException.error(message);
+        }
+
+        // There must be a corresponding closing brace.
+        final int closePos = userPasswordValue.indexOf('}');
+        if (closePos < 0) {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_USERPW_NO_CLOSING_BRACE.get();
+            throw DecodeException.error(message);
+        }
+
+        // Get the storage scheme name and encoded value.
+        final String schemeName = userPasswordValue.substring(1, closePos);
+        final String encodedValue = userPasswordValue.substring(closePos + 1);
+
+        if (schemeName.length() == 0) {
+            final LocalizableMessage message = ERR_ATTR_SYNTAX_USERPW_NO_SCHEME.get();
+            throw DecodeException.error(message);
+        }
+
+        return new String[] { toLowerCase(schemeName), encodedValue };
+    }
+
+    /**
+     * Indicates whether the provided value is encoded using the user password
+     * syntax.
+     *
+     * @param value
+     *            The value for which to make the determination.
+     * @return <CODE>true</CODE> if the value appears to be encoded using the
+     *         user password syntax, or <CODE>false</CODE> if not.
+     */
+    static boolean isEncoded(final ByteSequence value) {
+        // If the value is null or empty, then it's not.
+        if (value == null || value.length() == 0) {
+            return false;
+        }
+
+        // If the value doesn't start with an opening curly brace, then it's
+        // not.
+        if (value.byteAt(0) != '{') {
+            return false;
+        }
+
+        // There must be a corresponding closing curly brace, and there must
+        // be at least one character inside the brace.
+        int closingBracePos = -1;
+        for (int i = 1; i < value.length(); i++) {
+            if (value.byteAt(i) == '}') {
+                closingBracePos = i;
+                break;
+            }
+        }
+
+        if (closingBracePos < 0 || closingBracePos == 1) {
+            return false;
+        }
+
+        // The closing curly brace must not be the last character of the password
+        return closingBracePos != value.length() - 1;
+    }
+
+    @Override
+    public String getEqualityMatchingRule() {
+        return EMR_USER_PASSWORD_EXACT_OID;
+    }
+
+    @Override
+    public String getName() {
+        return SYNTAX_USER_PASSWORD_NAME;
+    }
+
+    @Override
+    public boolean isHumanReadable() {
+        return true;
+    }
+
+    @Override
+    public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
+            final LocalizableMessageBuilder invalidReason) {
+        // We have to accept any value here because in many cases the value
+        // will not have been encoded by the time this method is called.
+        // TODO: Is this correct for client-side use?
+        return true;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/package-info.java
new file mode 100644
index 0000000..4214c69
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for constructing and querying LDAP schemas.
+ */
+package org.forgerock.opendj.ldap.schema;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java
new file mode 100644
index 0000000..9d7416d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.BindClient;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.util.promise.PromiseImpl;
+
+/**
+ * Bind result promise implementation.
+ */
+public final class BindResultLdapPromiseImpl extends ResultLdapPromiseImpl<BindRequest, BindResult> {
+    private final BindClient bindClient;
+
+    BindResultLdapPromiseImpl(
+            final PromiseImpl<BindResult, LdapException> impl,
+            final int requestID,
+            final BindRequest request,
+            final BindClient bindClient,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
+        this.bindClient = bindClient;
+    }
+
+    /**
+     * Returns the bind client.
+     *
+     * @return The bind client.
+     */
+    public BindClient getBindClient() {
+        return bindClient;
+    }
+
+    @Override
+    public boolean isBindOrStartTLS() {
+        return true;
+    }
+
+    @Override
+    BindResult newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
+        return Responses.newBindResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ConnectionState.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ConnectionState.java
new file mode 100644
index 0000000..cec68d9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ConnectionState.java
@@ -0,0 +1,371 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/**
+ * This class can be used to manage the internal state of a connection, ensuring
+ * valid and atomic state transitions, as well as connection event listener
+ * notification. There are 4 states:
+ * <ul>
+ * <li>connection is <b>valid</b> (isClosed()=false, isFailed()=false): can fail
+ * or be closed
+ * <li>connection has failed due to an <b>error</b> (isClosed()=false,
+ * isFailed()=true): can be closed
+ * <li>connection has been <b>closed</b> by the application (isClosed()=true,
+ * isFailed()=false): terminal state
+ * <li>connection has failed due to an <b>error</b> and has been <b>closed</b>
+ * by the application (isClosed()=true, isFailed()=true): terminal state
+ * </ul>
+ * All methods are synchronized and container classes may also synchronize on
+ * the state where needed. The state transition methods,
+ * {@link #notifyConnectionClosed()} and
+ * {@link #notifyConnectionError(boolean, LdapException)}, correspond to
+ * methods in the {@link ConnectionEventListener} interface except that they
+ * return a boolean indicating whether the transition was successful or not.
+ */
+public final class ConnectionState {
+    /*
+     * FIXME: The synchronization in this class has been kept simple for now.
+     * However, ideally we should notify listeners without synchronizing on the
+     * state in case a listener takes a long time to complete.
+     */
+
+    /*
+     * FIXME: This class should be used by connection pool and ldap connection
+     * implementations as well.
+     */
+
+    /**
+     * Use the State design pattern to manage state transitions.
+     */
+    private enum State {
+
+        /**
+         * Connection has not encountered an error nor has it been closed
+         * (initial state).
+         */
+        VALID() {
+            @Override
+            void addConnectionEventListener(final ConnectionState cs,
+                    final ConnectionEventListener listener) {
+                cs.listeners.add(listener);
+            }
+
+            @Override
+            boolean isClosed() {
+                return false;
+            }
+
+            @Override
+            boolean isFailed() {
+                return false;
+            }
+
+            @Override
+            boolean isValid() {
+                return true;
+            }
+
+            @Override
+            boolean notifyConnectionClosed(final ConnectionState cs) {
+                cs.state = CLOSED;
+                for (final ConnectionEventListener listener : cs.listeners) {
+                    listener.handleConnectionClosed();
+                }
+                return true;
+            }
+
+            @Override
+            boolean notifyConnectionError(final ConnectionState cs,
+                    final boolean isDisconnectNotification, final LdapException error) {
+                // Transition from valid to error state.
+                cs.failedDueToDisconnect = isDisconnectNotification;
+                cs.connectionError = error;
+                cs.state = ERROR;
+                /*
+                 * FIXME: a re-entrant close will invoke close listeners before
+                 * error notification has completed.
+                 */
+                for (final ConnectionEventListener listener : cs.listeners) {
+                    // Use the reason provided in the disconnect notification.
+                    listener.handleConnectionError(isDisconnectNotification, error);
+                }
+                return true;
+            }
+
+            @Override
+            void notifyUnsolicitedNotification(final ConnectionState cs,
+                    final ExtendedResult notification) {
+                for (final ConnectionEventListener listener : cs.listeners) {
+                    listener.handleUnsolicitedNotification(notification);
+                }
+            }
+        },
+
+        /**
+         * Connection has encountered an error, but has not been closed.
+         */
+        ERROR() {
+            @Override
+            void addConnectionEventListener(final ConnectionState cs,
+                    final ConnectionEventListener listener) {
+                listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError);
+                cs.listeners.add(listener);
+            }
+
+            @Override
+            boolean isClosed() {
+                return false;
+            }
+
+            @Override
+            boolean isFailed() {
+                return true;
+            }
+
+            @Override
+            boolean isValid() {
+                return false;
+            }
+
+            @Override
+            boolean notifyConnectionClosed(final ConnectionState cs) {
+                cs.state = ERROR_CLOSED;
+                for (final ConnectionEventListener listener : cs.listeners) {
+                    listener.handleConnectionClosed();
+                }
+                return true;
+            }
+        },
+
+        /**
+         * Connection has been closed (terminal state).
+         */
+        CLOSED() {
+            @Override
+            void addConnectionEventListener(final ConnectionState cs,
+                    final ConnectionEventListener listener) {
+                listener.handleConnectionClosed();
+            }
+
+            @Override
+            boolean isClosed() {
+                return true;
+            }
+
+            @Override
+            boolean isFailed() {
+                return false;
+            }
+
+            @Override
+            boolean isValid() {
+                return false;
+            }
+        },
+
+        /**
+         * Connection has encountered an error and has been closed (terminal
+         * state).
+         */
+        ERROR_CLOSED() {
+            @Override
+            void addConnectionEventListener(final ConnectionState cs,
+                    final ConnectionEventListener listener) {
+                listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError);
+                listener.handleConnectionClosed();
+            }
+
+            @Override
+            boolean isClosed() {
+                return true;
+            }
+
+            @Override
+            boolean isFailed() {
+                return true;
+            }
+
+            @Override
+            boolean isValid() {
+                return false;
+            }
+        };
+
+        abstract void addConnectionEventListener(ConnectionState cs,
+                final ConnectionEventListener listener);
+
+        abstract boolean isClosed();
+
+        abstract boolean isFailed();
+
+        abstract boolean isValid();
+
+        boolean notifyConnectionClosed(final ConnectionState cs) {
+            return false;
+        }
+
+        boolean notifyConnectionError(final ConnectionState cs,
+                final boolean isDisconnectNotification, final LdapException error) {
+            return false;
+        }
+
+        void notifyUnsolicitedNotification(final ConnectionState cs,
+                final ExtendedResult notification) {
+            // Do nothing by default.
+        }
+    }
+
+    /**
+     * Non-{@code null} once the connection has failed due to a connection
+     * error. Volatile so that it can be read without synchronization.
+     */
+    private volatile LdapException connectionError;
+
+    /** Whether the connection has failed due to a disconnect notification. */
+    private boolean failedDueToDisconnect;
+
+    /** Registered event listeners. */
+    private final List<ConnectionEventListener> listeners = new LinkedList<>();
+
+    /** Internal state implementation. */
+    private volatile State state = State.VALID;
+
+    /** Creates a new connection state which is initially valid. */
+    public ConnectionState() {
+        // Nothing to do.
+    }
+
+    /**
+     * Registers the provided connection event listener so that it will be
+     * notified when this connection is closed by the application, receives an
+     * unsolicited notification, or experiences a fatal error.
+     *
+     * @param listener
+     *            The listener which wants to be notified when events occur on
+     *            this connection.
+     * @throws IllegalStateException
+     *             If this connection has already been closed, i.e. if
+     *             {@code isClosed() == true}.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     */
+    public synchronized void addConnectionEventListener(final ConnectionEventListener listener) {
+        state.addConnectionEventListener(this, listener);
+    }
+
+    /**
+     * Returns the error that caused the connection to fail, or {@code null} if
+     * the connection has not failed.
+     *
+     * @return The error that caused the connection to fail, or {@code null} if
+     *         the connection has not failed.
+     */
+    public LdapException getConnectionError() {
+        return connectionError;
+    }
+
+    /**
+     * Indicates whether or not this connection has been explicitly closed by
+     * calling {@code close}. This method will not return {@code true} if a
+     * fatal error has occurred on the connection unless {@code close} has been
+     * called.
+     *
+     * @return {@code true} if this connection has been explicitly closed by
+     *         calling {@code close}, or {@code false} otherwise.
+     */
+    public boolean isClosed() {
+        return state.isClosed();
+    }
+
+    /**
+     * Returns {@code true} if the associated connection has not been closed and
+     * no fatal errors have been detected.
+     *
+     * @return {@code true} if this connection is valid, {@code false}
+     *         otherwise.
+     */
+    public boolean isValid() {
+        return state.isValid();
+    }
+
+    /**
+     * Attempts to transition this connection state to closed and invokes event
+     * listeners if successful.
+     *
+     * @return {@code true} if the state changed to closed, or {@code false} if
+     *         the state was already closed.
+     * @see ConnectionEventListener#handleConnectionClosed()
+     */
+    public synchronized boolean notifyConnectionClosed() {
+        return state.notifyConnectionClosed(this);
+    }
+
+    /**
+     * Attempts to transition this connection state to error and invokes event
+     * listeners if successful.
+     *
+     * @param isDisconnectNotification
+     *            {@code true} if the error was triggered by a disconnect
+     *            notification sent by the server, otherwise {@code false}.
+     * @param error
+     *            The exception that is about to be thrown to the application.
+     * @return {@code true} if the state changed to error, or {@code false} if
+     *         the state was already error or closed.
+     * @see ConnectionEventListener#handleConnectionError(boolean,
+     *      LdapException)
+     */
+    public synchronized boolean notifyConnectionError(final boolean isDisconnectNotification,
+            final LdapException error) {
+        return state.notifyConnectionError(this, isDisconnectNotification, error);
+    }
+
+    /**
+     * Notifies event listeners of the provided unsolicited notification if the
+     * state is valid.
+     *
+     * @param notification
+     *            The unsolicited notification.
+     * @see ConnectionEventListener#handleUnsolicitedNotification(ExtendedResult)
+     */
+    public synchronized void notifyUnsolicitedNotification(final ExtendedResult notification) {
+        state.notifyUnsolicitedNotification(this, notification);
+    }
+
+    /**
+     * Removes the provided connection event listener from this connection so
+     * that it will no longer be notified when this connection is closed by the
+     * application, receives an unsolicited notification, or experiences a fatal
+     * error.
+     *
+     * @param listener
+     *            The listener which no longer wants to be notified when events
+     *            occur on this connection.
+     * @throws NullPointerException
+     *             If the {@code listener} was {@code null}.
+     */
+    public synchronized void removeConnectionEventListener(final ConnectionEventListener listener) {
+        listeners.remove(listener);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java
new file mode 100644
index 0000000..e8a956d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.util.promise.PromiseImpl;
+
+/**
+ * Extended result promise implementation.
+ *
+ * @param <S>
+ *         The type of result returned by this promise.
+ */
+public final class ExtendedResultLdapPromiseImpl<S extends ExtendedResult> extends
+        ResultLdapPromiseImpl<ExtendedRequest<S>, S> {
+    ExtendedResultLdapPromiseImpl(
+            final PromiseImpl<S, LdapException> impl,
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Decode an extended result.
+     *
+     * @param result
+     *         Extended result to decode.
+     * @param options
+     *         Decoding options.
+     * @return The decoded extended result.
+     * @throws DecodeException
+     *         If a problem occurs during decoding.
+     */
+    public S decodeResult(final ExtendedResult result, final DecodeOptions options) throws DecodeException {
+        return getRequest().getResultDecoder().decodeExtendedResult(result, options);
+    }
+
+    @Override
+    public boolean isBindOrStartTLS() {
+        return StartTLSExtendedRequest.OID.equals(getRequest().getOID());
+    }
+
+    @Override
+    S newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
+        return getRequest().getResultDecoder().newExtendedErrorResult(resultCode, "", diagnosticMessage);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexQueryFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexQueryFactory.java
new file mode 100644
index 0000000..1dc85e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexQueryFactory.java
@@ -0,0 +1,107 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+
+/**
+ * A factory for creating arbitrarily complex index queries. This
+ * interface is implemented by the underlying backend implementation
+ * and passed to extensible matching rules so that they can construct
+ * arbitrarily complex index queries.
+ *
+ * @param <T>
+ *          The type of query created by this factory.
+ */
+public interface IndexQueryFactory<T> {
+
+    /**
+     * Returns a query requesting an index record matching the provided key.
+     *
+     * @param indexID
+     *          An identifier of the index type.
+     * @param key
+     *          A byte sequence containing the key.
+     * @return A query requesting the index record matching the key.
+     */
+    T createExactMatchQuery(String indexID, ByteSequence key);
+
+    /**
+     * Returns a query requesting all index records. A backend implementation
+     * may choose to return all or no records as part of the optimization.
+     *
+     * @return A query requesting all index records.
+     */
+    T createMatchAllQuery();
+
+    /**
+     * Returns a query requesting all index records in the specified range.
+     *
+     * @param indexID
+     *          An identifier of the index type.
+     * @param lower
+     *          The lower bound of the range. A 0 length byte array indicates no
+     *          lower bound and the range will start from the smallest key.
+     * @param upper
+     *          The upper bound of the range. A 0 length byte array indicates no
+     *          upper bound and the range will end at the largest key.
+     * @param lowerIncluded
+     *          true if a key exactly matching the lower bound is included in
+     *          the range, false if only keys strictly greater than the lower
+     *          bound are included. This value is ignored if the lower bound is
+     *          not specified.
+     * @param upperIncluded
+     *          true if a key exactly matching the upper bound is included in
+     *          the range, false if only keys strictly less than the upper bound
+     *          are included. This value is ignored if the upper bound is not
+     *          specified.
+     * @return A query requesting all index records in the specified range.
+     */
+    T createRangeMatchQuery(String indexID, ByteSequence lower,
+            ByteSequence upper, boolean lowerIncluded, boolean upperIncluded);
+
+    /**
+     * Returns a query which returns the intersection of a collection of
+     * sub-queries.
+     *
+     * @param subqueries
+     *          A collection of sub-queries.
+     * @return A query which returns the intersection of a collection of
+     *         sub-queries.
+     */
+    T createIntersectionQuery(Collection<T> subqueries);
+
+    /**
+     * Returns a query which combines the results of a collection of
+     * sub-queries.
+     *
+     * @param subqueries
+     *          A collection of sub-queries.
+     * @return A query which combines the results of a collection of
+     *         sub-queries.
+     */
+    T createUnionQuery(Collection<T> subqueries);
+
+    /**
+     * Returns the indexing options for this factory.
+     *
+     * @return the indexing options for this factory.
+     */
+    IndexingOptions getIndexingOptions();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Indexer.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Indexer.java
new file mode 100644
index 0000000..2fdbd4f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Indexer.java
@@ -0,0 +1,68 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * This class is registered with a Backend and it provides callbacks
+ * for indexing attribute values. An index implementation will use
+ * this interface to create the keys for an attribute value.
+ */
+public interface Indexer {
+
+    /**
+     * Returns an index identifier associated with this indexer. An identifier
+     * should be selected based on the matching rule type. A unique identifier
+     * will map to a unique index database in the backend implementation. If
+     * multiple matching rules need to share the index database, the
+     * corresponding indexers should always use the same identifier.
+     *
+     * @return index ID A String containing the ID associated with this indexer.
+     */
+    String getIndexID();
+
+    /**
+     * Generates the set of index keys for an attribute.
+     *
+     * @param schema
+     *          The schema in which the associated matching rule is defined.
+     * @param value
+     *          The attribute value for which keys are required.
+     * @param keys
+     *          A collection where to add the created keys.
+     * @throws DecodeException if an error occurs while normalizing the value
+     */
+    void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) throws DecodeException;
+
+    /**
+     * Returns a human readable representation of the key.
+     * Does a best effort conversion from an index key to a string that can be printed, as
+     * used by the diagnostic tools, which are the only users of the method.
+     * It is not necessary for the resulting string to exactly match the value it was
+     * generated from.
+     *
+     * @param key the byte string for the index key.
+     * @return a human readable representation of the key
+     */
+    String keyToHumanReadableString(ByteSequence key);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexingOptions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexingOptions.java
new file mode 100644
index 0000000..7f7a043
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/IndexingOptions.java
@@ -0,0 +1,33 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+/**
+ * Contains options indicating how indexing must be performed.
+ */
+public interface IndexingOptions {
+
+    /**
+     * Returns the maximum size to use when building the keys for the
+     * "substring" index.
+     *
+     * @return the maximum size to use when building the keys for the
+     *         "substring" index.
+     */
+    int substringKeySize();
+
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java
new file mode 100644
index 0000000..a01300e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java
@@ -0,0 +1,71 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.Closeable;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * Interface for all classes that implement {@code LDAPConnectionFactory}.
+ * <p>
+ * An implementation class is provided by a {@code TransportProvider}.
+ * <p>
+ * The implementation can be automatically loaded using the
+ * {@code java.util.ServiceLoader} facility if its provider extending
+ * {@code TransportProvider} is declared in the provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}.
+ */
+public interface LDAPConnectionFactoryImpl extends Closeable {
+
+    /**
+     * Returns the address used by the connections created by this factory.
+     *
+     * @return The address used by the connections.
+     */
+    InetSocketAddress getSocketAddress();
+
+    /**
+     * Returns the hostname used by the connections created by this factory.
+     *
+     * @return The hostname used by the connections.
+     */
+    String getHostName();
+
+    /**
+     * Returns the remote port number used by the connections created by this factory.
+     *
+     * @return The remote port number used by the connections.
+     */
+    int getPort();
+
+    /**
+     * Releases any resources associated with this connection factory implementation.
+     */
+    @Override
+    void close();
+
+    /**
+     * Asynchronously obtains a connection to the Directory Server associated
+     * with this connection factory. The returned {@code Promise} can be used to
+     * retrieve the completed connection.
+     *
+     * @return A promise which can be used to retrieve the connection.
+     */
+    Promise<LDAPConnectionImpl, LdapException> getConnectionAsync();
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java
new file mode 100644
index 0000000..43ddff6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java
@@ -0,0 +1,314 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * LDAP connection interface which implementations of {@link LDAPConnectionFactoryImpl} should implement.
+ */
+public interface LDAPConnectionImpl extends Closeable {
+
+    /**
+     * Abandons the unfinished operation identified in the provided abandon request.
+     *
+     * @param request
+     *         The request identifying the operation to be abandoned.
+     * @return A promise whose result is Void.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support abandon operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#abandonAsync(AbandonRequest)
+     */
+    LdapPromise<Void> abandonAsync(AbandonRequest request);
+
+    /**
+     * Asynchronously adds an entry to the Directory Server using the provided add request.
+     *
+     * @param request
+     *         The add request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support add operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#addAsync(AddRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Registers the provided connection event listener so that it will be notified when this connection is closed by
+     * the application, receives an unsolicited notification, or experiences a fatal error.
+     *
+     * @param listener
+     *         The listener which wants to be notified when events occur on this connection.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If the {@code listener} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#addConnectionEventListener(ConnectionEventListener)
+     */
+    void addConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Asynchronously authenticates to the Directory Server using the provided bind request.
+     *
+     * @param request
+     *         The bind request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#bindAsync(BindRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<BindResult> bindAsync(BindRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Releases any resources associated with this connection.
+     * <p/>
+     * Calling {@code close} on a connection that is already closed has no effect.
+     * @see org.forgerock.opendj.ldap.Connection#close()
+     */
+    @Override
+    void close();
+
+    /**
+     * Releases any resources associated with this connection.
+     * <p/>
+     * Calling {@code close} on a connection that is already closed has no effect.
+     *
+     * @param request
+     *         The unbind request to use in the case where a physical connection is closed.
+     * @param reason
+     *         A reason describing why the connection was closed.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#close(UnbindRequest, String)
+     */
+    void close(UnbindRequest request, String reason);
+
+    /**
+     * Asynchronously compares an entry in the Directory Server using the provided compare request.
+     *
+     * @param request
+     *         The compare request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#compareAsync(CompareRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<CompareResult> compareAsync(
+            CompareRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Asynchronously deletes an entry from the Directory Server using the provided delete request.
+     *
+     * @param request
+     *         The delete request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#deleteAsync(DeleteRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> deleteAsync(DeleteRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Installs the TLS/SSL security layer on the underlying connection. The TLS/SSL security layer will be installed
+     * beneath any existing connection security layers and can only be installed at most once.
+     *
+     * @param sslContext
+     *         The {@code SSLContext} which should be used to secure the
+     * @param protocols
+     *         Names of all the protocols to enable or {@code null} to use the default protocols.
+     * @param suites
+     *         Names of all the suites to enable or {@code null} to use the default cipher suites.
+     * @return A promise which will complete once the SSL handshake has completed.
+     * @throws IllegalStateException
+     *         If the TLS/SSL security layer has already been installed.
+     */
+    Promise<Void, LdapException> enableTLS(SSLContext sslContext, List<String> protocols, List<String> suites);
+
+    /**
+     * Asynchronously performs the provided extended request in the Directory Server.
+     *
+     * @param <R>
+     *         The type of result returned by the extended request.
+     * @param request
+     *         The extended request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#extendedRequestAsync(ExtendedRequest, IntermediateResponseHandler)
+     */
+    <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(
+            ExtendedRequest<R> request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Indicates whether or not this connection has been explicitly closed by calling {@code close}. This method will
+     * not return {@code true} if a fatal error has occurred on the connection unless {@code close} has been called.
+     *
+     * @return {@code true} if this connection has been explicitly closed by calling {@code close}, or {@code false}
+     * otherwise.
+     * @see org.forgerock.opendj.ldap.Connection#isClosed()
+     */
+    boolean isClosed();
+
+    /**
+     * Returns {@code true} if this connection has not been closed and no fatal errors have been detected. This method
+     * is guaranteed to return {@code false} only when it is called after the method {@code close} has been called.
+     *
+     * @return {@code true} if this connection is valid, {@code false} otherwise.
+     * @see org.forgerock.opendj.ldap.Connection#isValid()
+     */
+    boolean isValid();
+
+    /**
+     * Asynchronously modifies an entry in the Directory Server using the provided modify request.
+     *
+     * @param request
+     *         The modify request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support modify operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#modifyAsync(ModifyRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> modifyAsync(ModifyRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Asynchronously renames an entry in the Directory Server using the provided modify DN request.
+     *
+     * @param request
+     *         The modify DN request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#modifyDNAsync(ModifyDNRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> modifyDNAsync(
+            ModifyDNRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Removes the provided connection event listener from this connection so that it will no longer be notified when
+     * this connection is closed by the application, receives an unsolicited notification, or experiences a fatal
+     * error.
+     *
+     * @param listener
+     *         The listener which no longer wants to be notified when events occur on this connection.
+     * @throws NullPointerException
+     *         If the {@code listener} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#removeConnectionEventListener(ConnectionEventListener)
+     */
+    void removeConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Asynchronously searches the Directory Server using the provided search request.
+     *
+     * @param request
+     *         The search request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @param entryHandler
+     *         A search result handler which can be used to asynchronously process the search result entries and
+     *         references as they are received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support search operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#searchAsync(SearchRequest, IntermediateResponseHandler,
+     * SearchResultHandler)
+     */
+    LdapPromise<Result> searchAsync(
+            SearchRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            SearchResultHandler entryHandler);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPListenerImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPListenerImpl.java
new file mode 100644
index 0000000..673ec69
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPListenerImpl.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.Closeable;
+import java.net.InetSocketAddress;
+
+/**
+ * Interface for all classes that actually implement {@code LDAPListener}.
+ * <p>
+ * An implementation class is provided by a {@code TransportProvider}.
+ * <p>
+ * The implementation can be automatically loaded using the
+ * {@code java.util.ServiceLoader} facility if its provider extending
+ * {@code TransportProvider} is declared in the provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}.
+ */
+public interface LDAPListenerImpl extends Closeable {
+
+    /**
+     * Returns the address that this LDAP listener is listening on.
+     *
+     * @return The address that this LDAP listener is listening on.
+     */
+    InetSocketAddress getSocketAddress();
+
+    /**
+     * Closes this stream and releases any system resources associated
+     * with it. If the stream is already closed then invoking this
+     * method has no effect.
+     */
+    @Override
+    void close();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseImpl.java
new file mode 100644
index 0000000..06643bd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseImpl.java
@@ -0,0 +1,82 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+
+/**
+ * This class provides an implementation of the {@link LdapPromise}.
+ *
+ * @param <S>
+ *            The type of result returned by this promise.
+ * @see Promise
+ * @see Promises
+ * @see LdapPromise
+ */
+public class LdapPromiseImpl<S> extends LdapPromiseWrapper<S, PromiseImpl<S, LdapException>> implements
+        LdapPromise<S>, LdapResultHandler<S> {
+
+    /**
+     * Creates a new {@link LdapPromiseImpl} from a wrapped existing {@link PromiseImpl}.
+     *
+     * @param wrappedPromise
+     *      The {@link Promise} to wrap.
+     * @param requestID
+     *      Identifier of the request.
+     */
+    protected LdapPromiseImpl(PromiseImpl<S, LdapException> wrappedPromise, int requestID) {
+        super(wrappedPromise, requestID);
+    }
+
+    /**
+     * Creates a new {@link LdapPromiseImpl}.
+     *
+     * @param <S>
+     *            The type of result of the promise.
+     * @return a new {@link LdapPromiseImpl}
+     */
+    public static <S> LdapPromiseImpl<S> newLdapPromiseImpl() {
+        return newLdapPromiseImpl(-1);
+    }
+
+    /**
+     * Creates a new {@link LdapPromiseImpl} with a requestID.
+     *
+     * @param <S>
+     *            The type of result of the promise.
+     * @param requestID
+     *            Identifier of the request.
+     * @return a new {@link LdapPromiseImpl}
+     */
+    public static <S> LdapPromiseImpl<S> newLdapPromiseImpl(int requestID) {
+        return new LdapPromiseImpl<>(PromiseImpl.<S, LdapException> create(), requestID);
+    }
+
+    @Override
+    public void handleException(LdapException exception) {
+        getWrappedPromise().handleException(exception);
+    }
+
+    @Override
+    public void handleResult(S result) {
+        getWrappedPromise().handleResult(result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java
new file mode 100644
index 0000000..43eaead
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java
@@ -0,0 +1,193 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.promise.RuntimeExceptionHandler;
+
+/**
+ * Provides a {@link Promise} wrapper and a {@link LdapPromise} implementation.
+ *
+ *
+ * This wrapper allows client code to return {@link LdapPromise} instance when
+ * using {@link Promise} chaining methods (e.g thenOnResult(), then(), thenAsync()).
+ * Wrapping is specially needed with {@link Promise} method which are not returning the original promise.
+ *
+ * @param <R>
+ *       The type of the task's result.
+ * @param <P>
+ *       The wrapped promise.
+ */
+class LdapPromiseWrapper<R, P extends Promise<R, LdapException>> implements LdapPromise<R> {
+    private final P wrappedPromise;
+    private final int requestID;
+
+    public LdapPromiseWrapper(P wrappedPromise, int requestID) {
+        this.wrappedPromise = wrappedPromise;
+        this.requestID = requestID;
+    }
+
+    @Override
+    public int getRequestID() {
+        return wrappedPromise instanceof LdapPromise ? ((LdapPromise<R>) wrappedPromise).getRequestID()
+                : requestID;
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return wrappedPromise.cancel(mayInterruptIfRunning);
+    }
+
+    @Override
+    public R get() throws ExecutionException, InterruptedException {
+        return wrappedPromise.get();
+    }
+
+    @Override
+    public R get(long timeout, TimeUnit unit) throws ExecutionException, TimeoutException, InterruptedException {
+        return wrappedPromise.get(timeout, unit);
+    }
+
+    @Override
+    public R getOrThrow() throws InterruptedException, LdapException {
+        return wrappedPromise.getOrThrow();
+    }
+
+    @Override
+    public R getOrThrow(long timeout, TimeUnit unit) throws InterruptedException, LdapException, TimeoutException {
+        return wrappedPromise.getOrThrow(timeout, unit);
+    }
+
+    @Override
+    public R getOrThrowUninterruptibly() throws LdapException {
+        return wrappedPromise.getOrThrowUninterruptibly();
+    }
+
+    @Override
+    public R getOrThrowUninterruptibly(long timeout, TimeUnit unit) throws LdapException, TimeoutException {
+        return wrappedPromise.getOrThrowUninterruptibly(timeout, unit);
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return wrappedPromise.isCancelled();
+    }
+
+    @Override
+    public boolean isDone() {
+        return wrappedPromise.isDone();
+    }
+
+    @Override
+    public LdapPromise<R> thenOnException(ExceptionHandler<? super LdapException> onException) {
+        wrappedPromise.thenOnException(onException);
+        return this;
+    }
+
+    @Override
+    public LdapPromise<R> thenOnRuntimeException(RuntimeExceptionHandler onRuntimeException) {
+        wrappedPromise.thenOnRuntimeException(onRuntimeException);
+        return this;
+    }
+
+    @Override
+    public LdapPromise<R> thenOnResult(ResultHandler<? super R> onResult) {
+        wrappedPromise.thenOnResult(onResult);
+        return this;
+    }
+
+    @Override
+    public LdapPromise<R> thenOnResultOrException(Runnable onResultOrException) {
+        wrappedPromise.thenOnResultOrException(onResultOrException);
+        return this;
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <VOUT> LdapPromise<VOUT> then(Function<? super R, VOUT, LdapException> onResult) {
+        return wrap(wrappedPromise.then(onResult), getRequestID());
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <VOUT, EOUT extends Exception> Promise<VOUT, EOUT> then(Function<? super R, VOUT, EOUT> onResult,
+            Function<? super LdapException, VOUT, EOUT> onException) {
+        return wrappedPromise.then(onResult, onException);
+    }
+
+    @Override
+    public LdapPromise<R> thenOnResultOrException(ResultHandler<? super R> onResult,
+                                                  ExceptionHandler<? super LdapException> onException) {
+        wrappedPromise.thenOnResultOrException(onResult, onException);
+        return this;
+    }
+
+    @Override
+    public LdapPromise<R> thenAlways(Runnable onResultOrException) {
+        wrappedPromise.thenAlways(onResultOrException);
+        return this;
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <VOUT> LdapPromise<VOUT> thenAsync(AsyncFunction<? super R, VOUT, LdapException> onResult) {
+        return wrap(wrappedPromise.thenAsync(onResult), getRequestID());
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <VOUT, EOUT extends Exception> Promise<VOUT, EOUT> thenAsync(
+            AsyncFunction<? super R, VOUT, EOUT> onResult,
+            AsyncFunction<? super LdapException, VOUT, EOUT> onException) {
+        return wrappedPromise.thenAsync(onResult, onException);
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <EOUT extends Exception> Promise<R, EOUT> thenCatch(Function<? super LdapException, R, EOUT> onException) {
+        return wrappedPromise.thenCatch(onException);
+    }
+
+    @Override
+    public LdapPromise<R> thenFinally(Runnable onResultOrException) {
+        wrappedPromise.thenFinally(onResultOrException);
+        return this;
+    }
+
+    @Override
+    // @Checkstyle:ignore
+    public <EOUT extends Exception> Promise<R, EOUT> thenCatchAsync(
+            AsyncFunction<? super LdapException, R, EOUT> onException) {
+        return wrappedPromise.thenCatchAsync(onException);
+    }
+
+    public P getWrappedPromise() {
+        return wrappedPromise;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java
new file mode 100644
index 0000000..dd55d10
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java
@@ -0,0 +1,440 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.requests.BindClient;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+
+/** Utility methods for creating and composing {@link LdapPromise}s. */
+public final class LdapPromises {
+    private LdapPromises() {
+    }
+
+    /**
+     * Converts a {@link Promise} to a {@link LdapPromise}.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param wrappedPromise
+     *         The {@link Promise} to wrap.
+     * @return A {@link LdapPromise} representing the same asynchronous task as the {@link Promise} provided.
+     */
+    public static <R> LdapPromise<R> asPromise(Promise<R, LdapException> wrappedPromise) {
+        return wrap(wrappedPromise, -1);
+    }
+
+    /**
+     * Creates a new bind {@link BindResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The bind request sent to server.
+     * @param bindClient
+     *         Client that binds to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @return The new {@link BindResultLdapPromiseImpl}.
+     */
+    public static BindResultLdapPromiseImpl newBindLdapPromise(
+            final int requestID,
+            final BindRequest request,
+            final BindClient bindClient,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return new BindResultLdapPromiseImpl(LdapPromises.<BindResult>newInnerBindOrStartTLSPromise(),
+                                             requestID,
+                                             request,
+                                             bindClient,
+                                             intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new compare {@link ResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The compare request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        final PromiseImpl<CompareResult, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newCompareLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new compare {@link ResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The compare request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<CompareResult, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newCompareLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    private static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final PromiseImpl<CompareResult, LdapException> innerPromise,
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return new ResultLdapPromiseImpl<CompareRequest, CompareResult>(innerPromise,
+                                                                        requestID,
+                                                                        request,
+                                                                        intermediateResponseHandler) {
+            @Override
+            protected CompareResult newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause) {
+                return newCompareResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
+            }
+        };
+    }
+
+    /**
+     * Creates a new extended {@link ExtendedResultLdapPromiseImpl}.
+     *
+     * @param <S>
+     *         The type of result returned by this promise.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The extended request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ExtendedResultLdapPromiseImpl}.
+     */
+    public static <S extends ExtendedResult> ExtendedResultLdapPromiseImpl<S> newExtendedLdapPromise(
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        final PromiseImpl<S, LdapException> innerPromise;
+        if (!StartTLSExtendedRequest.OID.equals(request.getOID())) {
+            innerPromise = newInnerBindOrStartTLSPromise();
+        } else {
+            innerPromise = newInnerPromise(connection, requestID);
+        }
+        return new ExtendedResultLdapPromiseImpl<>(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new extended {@link ExtendedResultLdapPromiseImpl}.
+     *
+     * @param <S>
+     *         The type of result returned by this promise.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The extended request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ExtendedResultLdapPromiseImpl}.
+     */
+    public static <S extends ExtendedResult> ExtendedResultLdapPromiseImpl<S> newExtendedLdapPromise(
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<S, LdapException> innerPromise;
+        if (!StartTLSExtendedRequest.OID.equals(request.getOID())) {
+            innerPromise = newInnerBindOrStartTLSPromise();
+        } else {
+            innerPromise = newInnerPromise(connection, requestID);
+        }
+        return new ExtendedResultLdapPromiseImpl<>(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new {@link ResultLdapPromiseImpl} to handle  a standard request (add, delete, modify and modidyDN).
+     *
+     * @param <R>
+     *         The type of the task's request.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        final PromiseImpl<Result, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newResultLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new {@link ResultLdapPromiseImpl} to handle  a standard request (add, delete, modify and modidyDN).
+     *
+     * @param <R>
+     *         The type of the task's request.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<Result, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newResultLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    private static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final PromiseImpl<Result, LdapException> innerPromise,
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return new ResultLdapPromiseImpl<R, Result>(innerPromise, requestID, request, intermediateResponseHandler) {
+            @Override
+            protected Result newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause) {
+                return newResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
+            }
+        };
+    }
+
+    /**
+     * Creates a new search {@link SearchResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The search request sent to the server.
+     * @param resultHandler
+     *         Handler that consumes search result.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link SearchResultLdapPromiseImpl}.
+     */
+    public static SearchResultLdapPromiseImpl newSearchLdapPromise(
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        return new SearchResultLdapPromiseImpl(newInnerPromise(connection, requestID),
+                                               requestID,
+                                               request,
+                                               resultHandler,
+                                               intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new search {@link SearchResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The search request sent to the server.
+     * @param resultHandler
+     *         Handler that consumes search result.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link SearchResultLdapPromiseImpl}.
+     */
+    public static SearchResultLdapPromiseImpl newSearchLdapPromise(
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        return new SearchResultLdapPromiseImpl(newInnerPromise(connection, requestID),
+                                               requestID,
+                                               request,
+                                               resultHandler,
+                                               intermediateResponseHandler);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task which has already failed with the provided
+     * error.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param <E>
+     *         The type of the exception thrown by the task if it fails.
+     * @param error
+     *         The exception indicating why the asynchronous task has failed.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already failed with the provided error.
+     */
+    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error) {
+        return wrap(Promises.<R, LdapException>newExceptionPromise(error), -1);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task, identified by the provided requestID, which has
+     * already failed with the provided error.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param <E>
+     *         The type of the exception thrown by the task if it fails.
+     * @param error
+     *         The exception indicating why the asynchronous task has failed.
+     * @param requestID
+     *         The request ID of the failed task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already failed with the provided error.
+     */
+    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error, int requestID) {
+        return wrap(Promises.<R, LdapException>newExceptionPromise(error), requestID);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result. Attempts to get the result will immediately return the result.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param result
+     *         The result of the asynchronous task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result.
+     */
+    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result) {
+        return wrap(Promises.<R, LdapException>newResultPromise(result), -1);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task, identified by the provided requestID, which has
+     * already succeeded with the provided result. Attempts to get the result will immediately return the result.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param result
+     *         The result of the asynchronous task.
+     * @param requestID
+     *         The request ID of the succeeded task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result.
+     */
+    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result, int requestID) {
+        return wrap(Promises.<R, LdapException>newResultPromise(result), requestID);
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerBindOrStartTLSPromise() {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected LdapException tryCancel(boolean mayInterruptIfRunning) {
+                /*
+                 * No other operations can be performed while a bind or StartTLS is active. Therefore it is not
+                 * possible to cancel bind or StartTLS requests, since doing so will leave the connection in a
+                 * state which prevents other operations from being performed.
+                 */
+                return null;
+            }
+        };
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerPromise(
+            final Connection connection, final int requestID) {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected final LdapException tryCancel(final boolean mayInterruptIfRunning) {
+                /*
+                 * This will abandon the request, but will also recursively cancel this future. There is no risk of
+                 * an infinite loop because the state of this future has already been changed.
+                 */
+                connection.abandonAsync(Requests.newAbandonRequest(requestID));
+                return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
+            }
+        };
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerPromise(
+            final LDAPConnectionImpl connection, final int requestID) {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected final LdapException tryCancel(final boolean mayInterruptIfRunning) {
+                /*
+                 * This will abandon the request, but will also recursively cancel this future. There is no risk of
+                 * an infinite loop because the state of this future has already been changed.
+                 */
+                connection.abandonAsync(Requests.newAbandonRequest(requestID));
+                return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
+            }
+        };
+    }
+
+    static <R> LdapPromise<R> wrap(Promise<R, LdapException> wrappedPromise, int requestID) {
+        return new LdapPromiseWrapper<>(wrappedPromise, requestID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Provider.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Provider.java
new file mode 100644
index 0000000..cd4e01a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/Provider.java
@@ -0,0 +1,35 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+/**
+ * Interface for providers, which provide an implementation of one or more interfaces.
+ * <p>
+ * A provider must be declared in the provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.<ProviderClass>}
+ * in order to allow automatic loading of the implementation classes using the
+ * {@code java.util.ServiceLoader} facility.
+ */
+public interface Provider {
+
+    /**
+     * Returns the name of this provider.
+     *
+     * @return name of provider
+     */
+    String getName();
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java
new file mode 100644
index 0000000..504f045
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+
+/**
+ * This class provides an implementation of the {@link LdapPromise}.
+ *
+ * @param <R>
+ *            The type of the associated {@link Request}.
+ * @param <S>
+ *            The type of result returned by this promise.
+ * @see Promise
+ * @see Promises
+ * @see LdapPromise
+ */
+public abstract class ResultLdapPromiseImpl<R extends Request, S extends Result> extends LdapPromiseImpl<S>
+        implements LdapPromise<S>, IntermediateResponseHandler {
+    private final R request;
+    private IntermediateResponseHandler intermediateResponseHandler;
+    private volatile long timestamp;
+
+    ResultLdapPromiseImpl(final PromiseImpl<S, LdapException> impl, final int requestID, final R request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID);
+        this.request = request;
+        this.intermediateResponseHandler = intermediateResponseHandler;
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    @Override
+    public final boolean handleIntermediateResponse(final IntermediateResponse response) {
+        // FIXME: there's a potential race condition here - the promise could
+        // get cancelled between the isDone() call and the handler
+        // invocation. We'd need to add support for intermediate handlers in
+        // the synchronizer.
+        if (!isDone()) {
+            updateTimestamp();
+            if (intermediateResponseHandler != null
+                    && !intermediateResponseHandler.handleIntermediateResponse(response)) {
+                intermediateResponseHandler = null;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns {@code true} if this promise represents the result of a bind or
+     * StartTLS request. The default implementation is to return {@code false}.
+     *
+     * @return {@code true} if this promise represents the result of a bind or
+     *         StartTLS request.
+     */
+    public boolean isBindOrStartTLS() {
+        return false;
+    }
+
+    /**
+     * Returns a string representation of this promise's state.
+     *
+     * @return String representation of this promise's state.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("( requestID = ");
+        sb.append(getRequestID());
+        sb.append(" timestamp = ");
+        sb.append(timestamp);
+        sb.append(" request = ");
+        sb.append(getRequest());
+        sb.append(" )");
+        return sb.toString();
+    }
+
+    /**
+     * Sets the result associated to this promise as an error result.
+     *
+     * @param result
+     *            result of an operation
+     */
+    public final void adaptErrorResult(final Result result) {
+        final S errorResult = newErrorResult(result.getResultCode(), result.getDiagnosticMessage(), result.getCause());
+        setResultOrError(errorResult);
+    }
+
+    /**
+     * Returns the creation time of this promise.
+     *
+     * @return the timestamp indicating creation time of this promise
+     */
+    public final long getTimestamp() {
+        return timestamp;
+    }
+
+    abstract S newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause);
+
+    /**
+     * Sets the result associated to this promise.
+     *
+     * @param result
+     *            the result of operation
+     */
+    public final void setResultOrError(final S result) {
+        if (result.getResultCode().isExceptional()) {
+            getWrappedPromise().handleException(newLdapException(result));
+        } else {
+            getWrappedPromise().handleResult(result);
+        }
+    }
+
+    /**
+     * Returns the attached request.
+     *
+     * @return The request.
+     */
+    public R getRequest() {
+        return request;
+    }
+
+    final void updateTimestamp() {
+        timestamp = System.currentTimeMillis();
+    }
+
+    /**
+     * Returns {@code true} if this request should be canceled once the timeout
+     * period expires. The default implementation is to return {@code true}
+     * which will be appropriate for nearly all requests, the one obvious
+     * exception being persistent searches.
+     *
+     * @return {@code true} if this request should be canceled once the timeout
+     *         period expires.
+     */
+    public boolean checkForTimeout() {
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java
new file mode 100644
index 0000000..d8a9c81
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java
@@ -0,0 +1,90 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.controls.ADNotificationRequestControl;
+import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.promise.PromiseImpl;
+
+/** Search result promise implementation. */
+public final class SearchResultLdapPromiseImpl extends ResultLdapPromiseImpl<SearchRequest, Result> implements
+        SearchResultHandler {
+    private SearchResultHandler searchResultHandler;
+    private final boolean isPersistentSearch;
+
+    SearchResultLdapPromiseImpl(
+            final PromiseImpl<Result, LdapException> impl,
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
+        this.searchResultHandler = resultHandler;
+        this.isPersistentSearch = request.containsControl(PersistentSearchRequestControl.OID)
+                || request.containsControl(ADNotificationRequestControl.OID);
+    }
+
+    @Override
+    public boolean handleEntry(final SearchResultEntry entry) {
+        // FIXME: there's a potential race condition here - the promise could
+        // get cancelled between the isDone() call and the handler
+        // invocation. We'd need to add support for intermediate handlers in
+        // the synchronizer.
+        if (!isDone()) {
+            updateTimestamp();
+            if (searchResultHandler != null && !searchResultHandler.handleEntry(entry)) {
+                searchResultHandler = null;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean handleReference(final SearchResultReference reference) {
+        // FIXME: there's a potential race condition here - the promise could
+        // get cancelled between the isDone() call and the handler
+        // invocation. We'd need to add support for intermediate handlers in
+        // the synchronizer.
+        if (!isDone()) {
+            updateTimestamp();
+            if (searchResultHandler != null && !searchResultHandler.handleReference(reference)) {
+                searchResultHandler = null;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    Result newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
+        return Responses.newResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
+    }
+
+    @Override
+    public boolean checkForTimeout() {
+        // Persistent searches should not time out.
+        return !isPersistentSearch;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/TransportProvider.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/TransportProvider.java
new file mode 100644
index 0000000..ddfe3c5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/TransportProvider.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.util.Options;
+
+/**
+ * Interface for transport providers, which provide implementation
+ * for {@code LDAPConnectionFactory} and {@code LDAPListener} classes,
+ * using a specific transport.
+ * <p>
+ * A transport provider must be declared in the provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}
+ * in order to allow automatic loading of the implementation classes using the
+ * {@code java.util.ServiceLoader} facility.
+ */
+public interface TransportProvider extends Provider {
+
+    /**
+     * Returns an implementation of {@code LDAPConnectionFactory}. The address
+     * will be resolved each time a new connection is returned.
+     *
+     * @param host
+     *            The hostname of the Directory Server to connect to.
+     * @param port
+     *            The port number of the Directory Server to connect to.
+     * @param options
+     *            The LDAP options to use when creating connections.
+     * @return an implementation of {@code LDAPConnectionFactory}
+     */
+    LDAPConnectionFactoryImpl getLDAPConnectionFactory(String host, int port, Options options);
+
+  /**
+     * Returns an implementation of {@code LDAPListener}.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @return an implementation of {@code LDAPListener}
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     */
+    LDAPListenerImpl getLDAPListener(InetSocketAddress address,
+            ServerConnectionFactory<LDAPClientContext, Integer> factory, Options options)
+            throws IOException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/package-info.java
new file mode 100644
index 0000000..cd7c098
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+/**
+ * Interfaces and classes for service providers.
+ */
+package org.forgerock.opendj.ldap.spi;
+
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
new file mode 100644
index 0000000..9e861ef
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
@@ -0,0 +1,698 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_COULD_NOT_BASE64_DECODE_DN;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_INVALID_DN;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_INVALID_LEADING_SPACE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_INVALID_URL;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_MALFORMED_ATTRIBUTE_NAME;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_NO_ATTR_NAME;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_NO_DN;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_UNEXPECTED_BINARY_OPTION;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_LDIF_URL_IO_ERROR;
+import static com.forgerock.opendj.ldap.CoreMessages.WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldap.schema.Syntax;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Common LDIF reader functionality.
+ */
+abstract class AbstractLDIFReader extends AbstractLDIFStream {
+    static final class KeyValuePair {
+        String key;
+        String value;
+    }
+
+    /**
+     * LDIF reader implementation interface.
+     */
+    interface LDIFReaderImpl {
+
+        /**
+         * Closes any resources associated with this LDIF reader implementation.
+         *
+         * @throws IOException
+         *             If an error occurs while closing.
+         */
+        void close() throws IOException;
+
+        /**
+         * Reads the next line of LDIF from the underlying LDIF source.
+         * Implementations must remove trailing line delimiters.
+         *
+         * @return The next line of LDIF, or {@code null} if the end of the LDIF
+         *         source has been reached.
+         * @throws IOException
+         *             If an error occurs while reading from the LDIF source.
+         */
+        String readLine() throws IOException;
+    }
+
+    static final class LDIFRecord {
+        final Iterator<String> iterator;
+        final LinkedList<String> ldifLines;
+        final long lineNumber;
+
+        private LDIFRecord(final long lineNumber, final LinkedList<String> ldifLines) {
+            this.lineNumber = lineNumber;
+            this.ldifLines = ldifLines;
+            this.iterator = ldifLines.iterator();
+        }
+    }
+
+    /**
+     * LDIF output stream writer implementation.
+     */
+    private static final class LDIFReaderInputStreamImpl implements LDIFReaderImpl {
+        private BufferedReader reader;
+
+        LDIFReaderInputStreamImpl(final Reader reader) {
+            this.reader =
+                    reader instanceof BufferedReader ? (BufferedReader) reader
+                            : new BufferedReader(reader);
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (reader != null) {
+                reader.close();
+                reader = null;
+            }
+        }
+
+        @Override
+        public String readLine() throws IOException {
+            String line = null;
+            if (reader != null) {
+                line = reader.readLine();
+                if (line == null) {
+                    // Automatically close.
+                    close();
+                }
+            }
+            return line;
+        }
+    }
+
+    /**
+     * LDIF output stream writer implementation.
+     */
+    private static final class LDIFReaderListImpl implements LDIFReaderImpl {
+        private final Iterator<String> iterator;
+
+        LDIFReaderListImpl(final List<String> ldifLines) {
+            this.iterator = ldifLines.iterator();
+        }
+
+        @Override
+        public void close() throws IOException {
+            // Nothing to do.
+        }
+
+        @Override
+        public String readLine() throws IOException {
+            if (iterator.hasNext()) {
+                return iterator.next();
+            } else {
+                return null;
+            }
+        }
+    }
+
+    RejectedLDIFListener rejectedRecordListener = RejectedLDIFListener.FAIL_FAST;
+    Schema schema = Schema.getDefaultSchema().asNonStrictSchema();
+    SchemaValidationPolicy schemaValidationPolicy = SchemaValidationPolicy.ignoreAll();
+    private final LDIFReaderImpl impl;
+    private long lineNumber;
+
+    AbstractLDIFReader(final InputStream in) {
+        this(new InputStreamReader(in));
+    }
+
+    AbstractLDIFReader(final List<String> ldifLines) {
+        Reject.ifNull(ldifLines);
+        this.impl = new LDIFReaderListImpl(ldifLines);
+    }
+
+    AbstractLDIFReader(final Reader reader) {
+        this.impl = new LDIFReaderInputStreamImpl(reader);
+    }
+
+    final void close0() throws IOException {
+        impl.close();
+    }
+
+    final void handleMalformedRecord(final LDIFRecord record, final LocalizableMessage message)
+            throws DecodeException {
+        rejectedRecordListener.handleMalformedRecord(record.lineNumber, record.ldifLines, message);
+    }
+
+    final void handleSchemaValidationFailure(final LDIFRecord record,
+            final List<LocalizableMessage> messages) throws DecodeException {
+        rejectedRecordListener.handleSchemaValidationFailure(record.lineNumber, record.ldifLines,
+                messages);
+    }
+
+    final void handleSchemaValidationWarning(final LDIFRecord record,
+            final List<LocalizableMessage> messages) throws DecodeException {
+        rejectedRecordListener.handleSchemaValidationWarning(record.lineNumber, record.ldifLines,
+                messages);
+    }
+
+    final void handleSkippedRecord(final LDIFRecord record, final LocalizableMessage message)
+            throws DecodeException {
+        rejectedRecordListener.handleSkippedRecord(record.lineNumber, record.ldifLines, message);
+    }
+
+    final int parseColonPosition(final LDIFRecord record, final String ldifLine)
+            throws DecodeException {
+        final int colonPos = ldifLine.indexOf(":");
+        if (colonPos <= 0) {
+            final LocalizableMessage message =
+                    ERR_LDIF_NO_ATTR_NAME.get(record.lineNumber, ldifLine);
+            throw DecodeException.error(message);
+        }
+        return colonPos;
+    }
+
+    final ByteString parseSingleValue(final LDIFRecord record, final String ldifLine,
+            final DN entryDN, final int colonPos, final String attrName) throws DecodeException {
+
+        /*
+         * Look at the character immediately after the colon. If there is none,
+         * then assume an attribute with an empty value. If it is another colon,
+         * then the value must be base64-encoded. If it is a less-than sign,
+         * then assume that it is a URL. Otherwise, it is a regular value.
+         */
+        final int length = ldifLine.length();
+        ByteString value;
+        if (colonPos == length - 1) {
+            value = ByteString.empty();
+        } else {
+            final char c = ldifLine.charAt(colonPos + 1);
+            if (c == ':') {
+                /*
+                 * The value is base64-encoded. Find the first non-blank
+                 * character, take the rest of the line, and base64-decode it.
+                 */
+                int pos = colonPos + 2;
+                while (pos < length && ldifLine.charAt(pos) == ' ') {
+                    pos++;
+                }
+
+                try {
+                    value = ByteString.valueOfBase64(ldifLine.substring(pos));
+                } catch (final LocalizedIllegalArgumentException e) {
+                    /*
+                     * The value did not have a valid base64-encoding.
+                     */
+                    final LocalizableMessage message =
+                            ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR.get(entryDN.toString(),
+                                    record.lineNumber, ldifLine, e.getMessageObject());
+                    throw DecodeException.error(message);
+                }
+            } else if (c == '<') {
+                /*
+                 * Find the first non-blank character, decode the rest of the
+                 * line as a URL, and read its contents.
+                 */
+                int pos = colonPos + 2;
+                while (pos < length && ldifLine.charAt(pos) == ' ') {
+                    pos++;
+                }
+
+                URL contentURL;
+                try {
+                    contentURL = new URL(ldifLine.substring(pos));
+                } catch (final Exception e) {
+                    // The URL was malformed or had an invalid protocol.
+                    final LocalizableMessage message =
+                            ERR_LDIF_INVALID_URL.get(entryDN.toString(), record.lineNumber,
+                                    attrName, String.valueOf(e));
+                    throw DecodeException.error(message);
+                }
+
+                ByteStringBuilder builder = null;
+                try (InputStream inputStream = contentURL.openConnection().getInputStream()) {
+                    builder = new ByteStringBuilder();
+
+                    int bytesRead;
+                    final byte[] buffer = new byte[4096];
+                    while ((bytesRead = inputStream.read(buffer)) > 0) {
+                        builder.appendBytes(buffer, 0, bytesRead);
+                    }
+
+                    value = builder.toByteString();
+                } catch (final Exception e) {
+                    /*
+                     * We were unable to read the contents of that URL for some
+                     * reason.
+                     */
+                    final LocalizableMessage message =
+                            ERR_LDIF_URL_IO_ERROR.get(entryDN.toString(), record.lineNumber,
+                                    attrName, String.valueOf(contentURL), String.valueOf(e));
+                    throw DecodeException.error(message);
+                }
+            } else {
+                /*
+                 * The rest of the line should be the value. Skip over any
+                 * spaces and take the rest of the line as the value.
+                 */
+                int pos = colonPos + 1;
+                while (pos < length && ldifLine.charAt(pos) == ' ') {
+                    pos++;
+                }
+
+                value = ByteString.valueOfUtf8(ldifLine.substring(pos));
+            }
+        }
+        return value;
+    }
+
+    final LDIFRecord readLDIFRecord() throws IOException {
+        // Read the entry lines into a buffer.
+        final StringBuilder lastLineBuilder = new StringBuilder();
+        final LinkedList<String> ldifLines = new LinkedList<>();
+        long recordLineNumber = 0;
+        final int stateStart = 0;
+        final int stateStartCommentLine = 1;
+        final int stateGotLDIFLine = 2;
+        final int stateGotCommentLine = 3;
+        final int appendingLDIFLine = 4;
+        int state = stateStart;
+
+        while (true) {
+            final String line = readLine();
+
+            switch (state) {
+            case stateStart:
+                if (line == null) {
+                    // We have reached the end of the LDIF source.
+                    return null;
+                } else if (line.length() == 0) {
+                    // Skip leading blank lines.
+                } else if (line.charAt(0) == '#') {
+                    // This is a comment at the start of the LDIF record.
+                    state = stateStartCommentLine;
+                } else if (isContinuationLine(line)) {
+                    /*
+                     * Fatal: got a continuation line at the start of the
+                     * record.
+                     */
+                    final LocalizableMessage message =
+                            ERR_LDIF_INVALID_LEADING_SPACE.get(lineNumber, line);
+                    throw DecodeException.fatalError(message);
+                } else {
+                    // Got the first line of LDIF.
+                    ldifLines.add(line);
+                    recordLineNumber = lineNumber;
+                    state = stateGotLDIFLine;
+                }
+                break;
+            case stateStartCommentLine:
+                if (line == null) {
+                    // We have reached the end of the LDIF source.
+                    return null;
+                } else if (line.length() == 0) {
+                    // Skip leading blank lines and comments.
+                    state = stateStart;
+                } else if (line.charAt(0) == '#') {
+                    // This is another comment at the start of the LDIF record.
+                } else if (isContinuationLine(line)) {
+                    // Skip comment continuation lines.
+                } else {
+                    // Got the first line of LDIF.
+                    ldifLines.add(line);
+                    recordLineNumber = lineNumber;
+                    state = stateGotLDIFLine;
+                }
+                break;
+            case stateGotLDIFLine:
+                if (line == null) {
+                    // We have reached the end of the LDIF source.
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.length() == 0) {
+                    // We have reached the end of the LDIF record.
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.charAt(0) == '#') {
+                    // This is a comment.
+                    state = stateGotCommentLine;
+                } else if (isContinuationLine(line)) {
+                    // Got a continuation line for the previous line.
+                    lastLineBuilder.setLength(0);
+                    lastLineBuilder.append(ldifLines.removeLast());
+                    lastLineBuilder.append(line.substring(1));
+                    state = appendingLDIFLine;
+                } else {
+                    // Got the next line of LDIF.
+                    ldifLines.add(line);
+                    state = stateGotLDIFLine;
+                }
+                break;
+            case stateGotCommentLine:
+                if (line == null) {
+                    // We have reached the end of the LDIF source.
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.length() == 0) {
+                    // We have reached the end of the LDIF record.
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.charAt(0) == '#') {
+                    // This is another comment.
+                    state = stateGotCommentLine;
+                } else if (isContinuationLine(line)) {
+                    // Skip comment continuation lines.
+                } else {
+                    // Got the next line of LDIF.
+                    ldifLines.add(line);
+                    state = stateGotLDIFLine;
+                }
+                break;
+            default: // appendingLDIFLine:
+                if (line == null) {
+                    // We have reached the end of the LDIF source.
+                    ldifLines.add(lastLineBuilder.toString());
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.length() == 0) {
+                    // We have reached the end of the LDIF record.
+                    ldifLines.add(lastLineBuilder.toString());
+                    return new LDIFRecord(recordLineNumber, ldifLines);
+                } else if (line.charAt(0) == '#') {
+                    // This is a comment.
+                    ldifLines.add(lastLineBuilder.toString());
+                    state = stateGotCommentLine;
+                } else if (isContinuationLine(line)) {
+                    // Got another continuation line for the previous line.
+                    lastLineBuilder.append(line.substring(1));
+                } else {
+                    // Got the next line of LDIF.
+                    ldifLines.add(lastLineBuilder.toString());
+                    ldifLines.add(line);
+                    state = stateGotLDIFLine;
+                }
+                break;
+            }
+        }
+    }
+
+    final boolean readLDIFRecordAttributeValue(final LDIFRecord record, final String ldifLine,
+            final Entry entry, final List<LocalizableMessage> schemaErrors) throws DecodeException {
+        // Parse the attribute description.
+        final int colonPos = parseColonPosition(record, ldifLine);
+        final String attrDescr = ldifLine.substring(0, colonPos);
+
+        AttributeDescription attributeDescription;
+        try {
+            attributeDescription = AttributeDescription.valueOf(attrDescr, schema);
+        } catch (final UnknownSchemaElementException e) {
+            final LocalizableMessage message =
+                    ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE.get(record.lineNumber, entry.getName()
+                            .toString(), attrDescr);
+            switch (schemaValidationPolicy.checkAttributesAndObjectClasses()) {
+            case REJECT:
+                schemaErrors.add(message);
+                return false;
+            case WARN:
+                schemaErrors.add(message);
+                return true;
+            default: // Ignore
+                /*
+                 * This should not happen: we should be using a non-strict
+                 * schema for this policy.
+                 */
+                throw new IllegalStateException("Schema is not consistent with policy", e);
+            }
+        } catch (final LocalizedIllegalArgumentException e) {
+            final LocalizableMessage message =
+                    ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(record.lineNumber, entry.getName()
+                            .toString(), attrDescr);
+            throw DecodeException.error(message);
+        }
+
+        // Now parse the attribute value.
+        final ByteString value =
+                parseSingleValue(record, ldifLine, entry.getName(), colonPos, attrDescr);
+
+        /*
+         * Skip the attribute if requested before performing any schema
+         * checking: the attribute may have been excluded because it is known to
+         * violate the schema.
+         */
+        if (isAttributeExcluded(attributeDescription)) {
+            return true;
+        }
+
+        final Syntax syntax = attributeDescription.getAttributeType().getSyntax();
+
+        // Ensure that the binary option is present if required.
+        if (!syntax.isBEREncodingRequired()) {
+            if (schemaValidationPolicy.checkAttributeValues().needsChecking()
+                    && attributeDescription.hasOption("binary")) {
+                final LocalizableMessage message =
+                        ERR_LDIF_UNEXPECTED_BINARY_OPTION.get(record.lineNumber, entry.getName()
+                                .toString(), attrDescr);
+                schemaErrors.add(message);
+                return !schemaValidationPolicy.checkAttributeValues().isReject();
+            }
+        } else {
+            attributeDescription = attributeDescription.withOption("binary");
+        }
+
+        final boolean checkAttributeValues =
+                schemaValidationPolicy.checkAttributeValues().needsChecking();
+        if (checkAttributeValues) {
+            final LocalizableMessageBuilder builder = new LocalizableMessageBuilder();
+            if (!syntax.valueIsAcceptable(value, builder)) {
+                schemaErrors.add(builder.toMessage());
+                if (schemaValidationPolicy.checkAttributeValues().isReject()) {
+                    return false;
+                }
+            }
+        }
+
+        Attribute attribute = entry.getAttribute(attributeDescription);
+        if (attribute == null) {
+            attribute = new LinkedAttribute(attributeDescription, value);
+            entry.addAttribute(attribute);
+        } else if (checkAttributeValues) {
+            if (!attribute.add(value)) {
+                final LocalizableMessage message =
+                        WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE.get(record.lineNumber, entry.getName()
+                                .toString(), attrDescr, value.toString());
+                schemaErrors.add(message);
+                if (schemaValidationPolicy.checkAttributeValues().isReject()) {
+                    return false;
+                }
+            } else if (attributeDescription.getAttributeType().isSingleValue()) {
+                final LocalizableMessage message =
+                        ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE.get(record.lineNumber, entry
+                                .getName().toString(), attrDescr);
+                schemaErrors.add(message);
+                if (schemaValidationPolicy.checkAttributeValues().isReject()) {
+                    return false;
+                }
+            }
+        } else {
+            attribute.add(value);
+        }
+
+        return true;
+    }
+
+    final DN readLDIFRecordDN(final LDIFRecord record) throws DecodeException {
+        String ldifLine = record.iterator.next();
+        int colonPos = ldifLine.indexOf(":");
+        if (colonPos <= 0) {
+            throw DecodeException.error(ERR_LDIF_NO_ATTR_NAME.get(record.lineNumber, ldifLine));
+        }
+
+        String attrName = toLowerCase(ldifLine.substring(0, colonPos));
+        if ("version".equals(attrName)) {
+            // This is the version line, try the next line if there is one.
+            if (!record.iterator.hasNext()) {
+                return null;
+            }
+
+            ldifLine = record.iterator.next();
+            colonPos = ldifLine.indexOf(":");
+            if (colonPos <= 0) {
+                throw DecodeException.error(ERR_LDIF_NO_ATTR_NAME.get(record.lineNumber, ldifLine));
+            }
+
+            attrName = toLowerCase(ldifLine.substring(0, colonPos));
+        }
+
+        if (!"dn".equals(attrName)) {
+            throw DecodeException.error(ERR_LDIF_NO_DN.get(record.lineNumber, ldifLine));
+        }
+
+        /*
+         * Look at the character immediately after the colon. If there is none,
+         * then assume the null DN. If it is another colon, then the DN must be
+         * base64-encoded. Otherwise, it may be one or more spaces.
+         */
+        final int length = ldifLine.length();
+        if (colonPos == length - 1) {
+            return DN.rootDN();
+        }
+
+        String dnString = null;
+
+        if (ldifLine.charAt(colonPos + 1) == ':') {
+            /*
+             * The DN is base64-encoded. Find the first non-blank character and
+             * take the rest of the line and base64-decode it.
+             */
+            int pos = colonPos + 2;
+            while (pos < length && ldifLine.charAt(pos) == ' ') {
+                pos++;
+            }
+
+            final String base64DN = ldifLine.substring(pos);
+            try {
+                dnString = ByteString.valueOfBase64(base64DN).toString();
+            } catch (final LocalizedIllegalArgumentException e) {
+                // The value did not have a valid base64-encoding.
+                final LocalizableMessage message =
+                        ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get(record.lineNumber, ldifLine, e
+                                .getMessageObject());
+                throw DecodeException.error(message);
+            }
+        } else {
+            /*
+             * The rest of the value should be the DN. Skip over any spaces and
+             * attempt to decode the rest of the line as the DN.
+             */
+            int pos = colonPos + 1;
+            while (pos < length && ldifLine.charAt(pos) == ' ') {
+                pos++;
+            }
+
+            dnString = ldifLine.substring(pos);
+        }
+
+        try {
+            return DN.valueOf(dnString, schema);
+        } catch (final LocalizedIllegalArgumentException e) {
+            final LocalizableMessage message =
+                    ERR_LDIF_INVALID_DN.get(record.lineNumber, ldifLine, e.getMessageObject());
+            throw DecodeException.error(message);
+        }
+    }
+
+    final String readLDIFRecordKeyValuePair(final LDIFRecord record, final KeyValuePair pair,
+            final boolean allowBase64) {
+        final String ldifLine = record.iterator.next();
+        final int colonPos = ldifLine.indexOf(":");
+        if (colonPos <= 0) {
+            pair.key = null;
+            return ldifLine;
+        }
+        pair.key = ldifLine.substring(0, colonPos);
+
+        /*
+         * Look at the character immediately after the colon. If there is none,
+         * then no value was specified. Throw an exception
+         */
+        final int length = ldifLine.length();
+        if (colonPos == length - 1) {
+            pair.key = null;
+            return ldifLine;
+        }
+
+        if (allowBase64 && ldifLine.charAt(colonPos + 1) == ':') {
+            /*
+             * The value is base64-encoded. Find the first non-blank character,
+             * take the rest of the line, and base64-decode it.
+             */
+            int pos = colonPos + 2;
+            while (pos < length && ldifLine.charAt(pos) == ' ') {
+                pos++;
+            }
+
+            try {
+                pair.value = ByteString.valueOfBase64(ldifLine.substring(pos)).toString();
+            } catch (final LocalizedIllegalArgumentException e) {
+                pair.key = null;
+                return ldifLine;
+            }
+        } else {
+            /*
+             * The rest of the value should be the changetype. Skip over any
+             * spaces and attempt to decode the rest of the line as the
+             * changetype string.
+             */
+            int pos = colonPos + 1;
+            while (pos < length && ldifLine.charAt(pos) == ' ') {
+                pos++;
+            }
+
+            pair.value = ldifLine.substring(pos);
+        }
+
+        return ldifLine;
+    }
+
+    /**
+     * Determines whether the provided line is a continuation line. Note that
+     * while RFC 2849 technically only allows a space in this position, both
+     * OpenLDAP and the Sun Java System Directory Server allow a tab as well, so
+     * we will too for compatibility reasons. See issue #852 for details.
+     */
+    private boolean isContinuationLine(final String line) {
+        return line.charAt(0) == ' ' || line.charAt(0) == '\t';
+    }
+
+    private String readLine() throws IOException {
+        final String line = impl.readLine();
+        if (line != null) {
+            lineNumber++;
+        }
+        return line;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java
new file mode 100644
index 0000000..a00ff43
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java
@@ -0,0 +1,101 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Matcher;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+
+/**
+ * Common LDIF reader/writer functionality.
+ */
+abstract class AbstractLDIFStream {
+    final Set<AttributeDescription> excludeAttributes = new HashSet<>();
+    final Set<DN> excludeBranches = new HashSet<>();
+    final List<Matcher> excludeFilters = new LinkedList<>();
+    boolean excludeOperationalAttributes;
+    boolean excludeUserAttributes;
+    final Set<AttributeDescription> includeAttributes = new HashSet<>();
+    final Set<DN> includeBranches = new HashSet<>();
+    final List<Matcher> includeFilters = new LinkedList<>();
+
+    AbstractLDIFStream() {
+        // Nothing to do.
+    }
+
+    final boolean isAttributeExcluded(final AttributeDescription attributeDescription) {
+        // Let explicit include override more general exclude.
+        if (!excludeAttributes.isEmpty() && excludeAttributes.contains(attributeDescription)) {
+            return true;
+        } else if (!includeAttributes.isEmpty()) {
+            return !includeAttributes.contains(attributeDescription);
+        } else {
+            final AttributeType type = attributeDescription.getAttributeType();
+            return (excludeOperationalAttributes && type.isOperational())
+                    || (excludeUserAttributes && !type.isOperational());
+        }
+    }
+
+    final boolean isBranchExcluded(final DN dn) {
+        if (!excludeBranches.isEmpty()) {
+            for (final DN excludeBranch : excludeBranches) {
+                if (excludeBranch.isSuperiorOrEqualTo(dn)) {
+                    return true;
+                }
+            }
+        }
+        if (!includeBranches.isEmpty()) {
+            for (final DN includeBranch : includeBranches) {
+                if (includeBranch.isSuperiorOrEqualTo(dn)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    final boolean isEntryExcluded(final Entry entry) {
+        if (!excludeFilters.isEmpty()) {
+            for (final Matcher excludeFilter : excludeFilters) {
+                if (excludeFilter.matches(entry).toBoolean()) {
+                    return true;
+                }
+            }
+        }
+        if (!includeFilters.isEmpty()) {
+            for (final Matcher includeFilter : includeFilters) {
+                if (includeFilter.matches(entry).toBoolean()) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java
new file mode 100644
index 0000000..2a6c774
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java
@@ -0,0 +1,373 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.controls.Control;
+
+import org.forgerock.util.Reject;
+
+/**
+ * Common LDIF writer functionality.
+ */
+abstract class AbstractLDIFWriter extends AbstractLDIFStream {
+
+    /**
+     * LDIF writer implementation interface.
+     */
+    interface LDIFWriterImpl {
+
+        /**
+         * Closes any resources associated with this LDIF writer implementation.
+         *
+         * @throws IOException
+         *             If an error occurs while closing.
+         */
+        void close() throws IOException;
+
+        /**
+         * Flushes this LDIF writer implementation so that any buffered data is
+         * written immediately to underlying stream, flushing the stream if it
+         * is also {@code Flushable}.
+         * <p>
+         * If the intended destination of this stream is an abstraction provided
+         * by the underlying operating system, for example a file, then flushing
+         * the stream guarantees only that bytes previously written to the
+         * stream are passed to the operating system for writing; it does not
+         * guarantee that they are actually written to a physical device such as
+         * a disk drive.
+         *
+         * @throws IOException
+         *             If an error occurs while flushing.
+         */
+        void flush() throws IOException;
+
+        /**
+         * Prints the provided {@code CharSequence}. Implementations must not
+         * add a new-line character sequence.
+         *
+         * @param s
+         *            The {@code CharSequence} to be printed.
+         * @throws IOException
+         *             If an error occurs while printing {@code s}.
+         */
+        void print(CharSequence s) throws IOException;
+
+        /**
+         * Prints a new-line character sequence.
+         *
+         * @throws IOException
+         *             If an error occurs while printing the new-line character
+         *             sequence.
+         */
+        void println() throws IOException;
+    }
+
+    /**
+     * LDIF string list writer implementation.
+     */
+    private static final class LDIFWriterListImpl implements LDIFWriterImpl {
+        private final StringBuilder builder = new StringBuilder();
+        private final List<String> ldifLines;
+
+        LDIFWriterListImpl(final List<String> ldifLines) {
+            this.ldifLines = ldifLines;
+        }
+
+        @Override
+        public void close() throws IOException {
+            // Nothing to do.
+        }
+
+        @Override
+        public void flush() throws IOException {
+            // Nothing to do.
+        }
+
+        @Override
+        public void print(final CharSequence s) throws IOException {
+            builder.append(s);
+        }
+
+        @Override
+        public void println() throws IOException {
+            ldifLines.add(builder.toString());
+            builder.setLength(0);
+        }
+    }
+
+    /**
+     * LDIF output stream writer implementation.
+     */
+    private static final class LDIFWriterOutputStreamImpl implements LDIFWriterImpl {
+        private final BufferedWriter writer;
+
+        LDIFWriterOutputStreamImpl(final Writer writer) {
+            this.writer =
+                    writer instanceof BufferedWriter ? (BufferedWriter) writer
+                            : new BufferedWriter(writer);
+        }
+
+        @Override
+        public void close() throws IOException {
+            writer.close();
+        }
+
+        @Override
+        public void flush() throws IOException {
+            writer.flush();
+        }
+
+        @Override
+        public void print(final CharSequence s) throws IOException {
+            writer.append(s);
+        }
+
+        @Override
+        public void println() throws IOException {
+            writer.newLine();
+        }
+    }
+
+    /** Regular expression used for splitting comments on line-breaks. */
+    private static final Pattern SPLIT_NEWLINE = Pattern.compile("\\r?\\n");
+    boolean addUserFriendlyComments;
+    final LDIFWriterImpl impl;
+    int wrapColumn;
+    private final StringBuilder builder = new StringBuilder(80);
+
+    AbstractLDIFWriter(final List<String> ldifLines) {
+        this.impl = new LDIFWriterListImpl(ldifLines);
+    }
+
+    AbstractLDIFWriter(final OutputStream out) {
+        this(new OutputStreamWriter(out));
+    }
+
+    AbstractLDIFWriter(final Writer writer) {
+        this.impl = new LDIFWriterOutputStreamImpl(writer);
+    }
+
+    final void close0() throws IOException {
+        flush0();
+        impl.close();
+    }
+
+    final void flush0() throws IOException {
+        impl.flush();
+    }
+
+    final void writeComment0(final CharSequence comment) throws IOException {
+        Reject.ifNull(comment);
+
+        /*
+         * First, break up the comment into multiple lines to preserve the
+         * original spacing that it contained.
+         */
+        final String[] lines = SPLIT_NEWLINE.split(comment);
+
+        /*
+         * Now iterate through the lines and write them out, prefixing and
+         * wrapping them as necessary.
+         */
+        for (final String line : lines) {
+            if (!shouldWrap()) {
+                impl.print("# ");
+                impl.print(line);
+                impl.println();
+            } else {
+                final int breakColumn = wrapColumn - 2;
+                if (line.length() <= breakColumn) {
+                    impl.print("# ");
+                    impl.print(line);
+                    impl.println();
+                } else {
+                    int startPos = 0;
+                outerLoop:
+                    while (startPos < line.length()) {
+                        if (startPos + breakColumn >= line.length()) {
+                            impl.print("# ");
+                            impl.print(line.substring(startPos));
+                            impl.println();
+                            startPos = line.length();
+                        } else {
+                            final int endPos = startPos + breakColumn;
+                            int i = endPos - 1;
+                            while (i > startPos) {
+                                if (line.charAt(i) == ' ') {
+                                    impl.print("# ");
+                                    impl.print(line.substring(startPos, i));
+                                    impl.println();
+
+                                    startPos = i + 1;
+                                    continue outerLoop;
+                                }
+                                i--;
+                            }
+
+                            /*
+                             * If we've gotten here, then there are no spaces on
+                             * the entire line. If that happens, then we'll have
+                             * to break in the middle of a word.
+                             */
+                            impl.print("# ");
+                            impl.print(line.substring(startPos, endPos));
+                            impl.println();
+                            startPos = endPos;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    final void writeControls(final List<Control> controls) throws IOException {
+        for (final Control control : controls) {
+            final StringBuilder key = new StringBuilder("control: ");
+            key.append(control.getOID());
+            key.append(control.isCritical() ? " true" : " false");
+            if (control.hasValue()) {
+                writeKeyAndValue(key, control.getValue());
+            } else {
+                writeLine(key);
+            }
+        }
+    }
+
+    final void writeKeyAndValue(final CharSequence key, final ByteSequence value)
+            throws IOException {
+        builder.setLength(0);
+
+        /*
+         * If the value is empty, then just append a single colon and a single
+         * space.
+         */
+        if (value.length() == 0) {
+            builder.append(key);
+            builder.append(": ");
+        } else if (needsBase64Encoding(value)) {
+            if (addUserFriendlyComments) {
+                /*
+                 * TODO: Only display comments for valid UTF-8 values, not
+                 * binary values.
+                 */
+            }
+            builder.setLength(0);
+            builder.append(key);
+            builder.append(":: ");
+            builder.append(value.toBase64String());
+        } else {
+            builder.append(key);
+            builder.append(": ");
+            builder.append(value.toString());
+        }
+        writeLine(builder);
+    }
+
+    final void writeKeyAndValue(final CharSequence key, final CharSequence value)
+            throws IOException {
+        writeKeyAndValue(key, ByteString.valueOfUtf8(value));
+    }
+
+    final void writeLine(final CharSequence line) throws IOException {
+        final int length = line.length();
+        if (shouldWrap() && length > wrapColumn) {
+            impl.print(line.subSequence(0, wrapColumn));
+            impl.println();
+            int pos = wrapColumn;
+            while (pos < length) {
+                final int writeLength = Math.min(wrapColumn - 1, length - pos);
+                impl.print(" ");
+                impl.print(line.subSequence(pos, pos + writeLength));
+                impl.println();
+                pos += wrapColumn - 1;
+            }
+        } else {
+            impl.print(line);
+            impl.println();
+        }
+    }
+
+    private boolean needsBase64Encoding(final ByteSequence bytes) {
+        final int length = bytes.length();
+        if (length == 0) {
+            return false;
+        }
+
+        /*
+         * If the value starts with a space, colon, or less than, then it needs
+         * to be base64 encoded.
+         */
+        switch (bytes.byteAt(0)) {
+        case 0x20: // Space
+        case 0x3A: // Colon
+        case 0x3C: // Less-than
+            return true;
+        }
+
+        /*
+         * If the value ends with a space, then it needs to be base64 encoded.
+         */
+        if (length > 1 && bytes.byteAt(length - 1) == 0x20) {
+            return true;
+        }
+
+        /*
+         * If the value contains a null, newline, or return character, then it
+         * needs to be base64 encoded.
+         */
+        for (int i = 0; i < bytes.length(); i++) {
+            final byte b = bytes.byteAt(i);
+            if (b < 0) {
+                return true;
+            }
+
+            switch (b) {
+            case 0x00: // Null
+            case 0x0A: // New line
+            case 0x0D: // Carriage return
+                return true;
+            }
+        }
+
+        /* If we've made it here, then there's no reason to base64 encode. */
+        return false;
+    }
+
+    private boolean shouldWrap() {
+        return wrapColumn > 1;
+    }
+
+    @SuppressWarnings("unused")
+    private void writeKeyAndURL(final CharSequence key, final CharSequence url) throws IOException {
+        builder.setLength(0);
+        builder.append(key);
+        builder.append(":: ");
+        builder.append(url);
+        writeLine(builder);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java
new file mode 100644
index 0000000..a291d20
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java
@@ -0,0 +1,99 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.requests.Request;
+
+/**
+ * A request to modify the content of the Directory in some way. A change record
+ * represents one of the following operations:
+ * <ul>
+ * <li>An {@code Add} operation.
+ * <li>An {@code Delete} operation.
+ * <li>An {@code Modify} operation.
+ * <li>An {@code ModifyDN} operation.
+ * </ul>
+ */
+public interface ChangeRecord extends Request {
+    /**
+     * Applies a {@code ChangeRecordVisitor} to this {@code ChangeRecord}.
+     *
+     * @param <R>
+     *            The return type of the visitor's methods.
+     * @param <P>
+     *            The type of the additional parameters to the visitor's
+     *            methods.
+     * @param v
+     *            The change record visitor.
+     * @param p
+     *            Optional additional visitor parameter.
+     * @return A result as specified by the visitor.
+     */
+    <R, P> R accept(ChangeRecordVisitor<R, P> v, P p);
+
+    /**
+     * Returns the distinguished name of the entry being modified by this
+     * {@code ChangeRecord}.
+     *
+     * @return The distinguished name of the entry being modified.
+     */
+    DN getName();
+
+
+    /*
+     * Uncomment both setName methods when we require JDK7 since JDK6 fails
+     * cannot deal with multiple inheritance of covariant return types
+     * (AddRequest inherits from both ChangeRecord and Entry).
+     *
+     * See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6970851
+     */
+
+    /**
+     * Sets the distinguished name of the entry to be updated. The server shall
+     * not perform any alias dereferencing in determining the object to be
+     * updated.
+     *
+     * @param dn
+     *            The the distinguished name of the entry to be updated.
+     * @return This change record.
+     * @throws UnsupportedOperationException
+     *             If this change record does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    // ChangeRecord setName(DN dn);
+
+    /**
+     * Sets the distinguished name of the entry to be updated. The server shall
+     * not perform any alias dereferencing in determining the object to be
+     * updated.
+     *
+     * @param dn
+     *            The the distinguished name of the entry to be updated.
+     * @return This change record.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code dn} could not be decoded using the default schema.
+     * @throws UnsupportedOperationException
+     *             If this change record does not permit the distinguished name
+     *             to be set.
+     * @throws NullPointerException
+     *             If {@code dn} was {@code null}.
+     */
+    // ChangeRecord setName(String dn);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordReader.java
new file mode 100644
index 0000000..628b7c4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordReader.java
@@ -0,0 +1,72 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+/**
+ * An interface for reading change records from a data source, typically an LDIF
+ * file.
+ * <p>
+ * Implementations must specify the following:
+ * <ul>
+ * <li>Whether or not it is possible for the implementation to encounter
+ * malformed change records and, if it is possible, how they are handled.
+ * <li>Any synchronization limitations.
+ * </ul>
+ */
+public interface ChangeRecordReader extends Closeable {
+
+    /**
+     * Closes this change record reader if it not already closed. Note that this
+     * method does not need to be called if a previous call of
+     * {@link #readChangeRecord()} has returned {@code null}.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Returns {@code true} if this reader contains another change record,
+     * blocking if necessary until either the next change record is available or
+     * the end of the stream is reached.
+     *
+     * @return {@code true} if this reader contains another change record.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean hasNext() throws IOException;
+
+    /**
+     * Reads the next change record, blocking if necessary until a change record
+     * is available. If the next change record does not contain a change type
+     * then it will be treated as an {@code Add} change record.
+     *
+     * @return The next change record.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the change
+     *             record.
+     * @throws NoSuchElementException
+     *             If this reader does not contain any more change records.
+     */
+    ChangeRecord readChangeRecord() throws IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitor.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitor.java
new file mode 100644
index 0000000..1f046f2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitor.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+/**
+ * A visitor of {@code ChangeRecord}s, in the style of the visitor design
+ * pattern.
+ * <p>
+ * Classes implementing this interface can query change records in a type-safe
+ * manner. When a visitor is passed to a change record's accept method, the
+ * corresponding visit method most applicable to that change record is invoked.
+ *
+ * @param <R>
+ *            The return type of this visitor's methods. Use
+ *            {@link java.lang.Void} for visitors that do not need to return
+ *            results.
+ * @param <P>
+ *            The type of the additional parameter to this visitor's methods.
+ *            Use {@link java.lang.Void} for visitors that do not need an
+ *            additional parameter.
+ */
+public interface ChangeRecordVisitor<R, P> {
+
+    /**
+     * Visits an {@code Add} change record.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param change
+     *            The {@code Add} change record.
+     * @return Returns a visitor specified result.
+     */
+    R visitChangeRecord(P p, AddRequest change);
+
+    /**
+     * Visits an {@code Delete} change record.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param change
+     *            The {@code Delete} change record.
+     * @return Returns a visitor specified result.
+     */
+    R visitChangeRecord(P p, DeleteRequest change);
+
+    /**
+     * Visits an {@code ModifyDN} change record.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param change
+     *            The {@code ModifyDN} change record.
+     * @return Returns a visitor specified result.
+     */
+    R visitChangeRecord(P p, ModifyDNRequest change);
+
+    /**
+     * Visits an {@code Modify} change record.
+     *
+     * @param p
+     *            A visitor specified parameter.
+     * @param change
+     *            The {@code Modify} change record.
+     * @return Returns a visitor specified result.
+     */
+    R visitChangeRecord(P p, ModifyRequest change);
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitorWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitorWriter.java
new file mode 100644
index 0000000..793cb2a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordVisitorWriter.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+/**
+ * A visitor which can be used to write generic change records.
+ */
+final class ChangeRecordVisitorWriter implements
+        ChangeRecordVisitor<IOException, ChangeRecordWriter> {
+    /** Visitor used for writing generic change records. */
+    private static final ChangeRecordVisitorWriter VISITOR = new ChangeRecordVisitorWriter();
+
+    /**
+     * Returns the singleton instance.
+     *
+     * @return The instance.
+     */
+    static ChangeRecordVisitorWriter getInstance() {
+        return VISITOR;
+    }
+
+    private ChangeRecordVisitorWriter() {
+        // Nothing to do.
+    }
+
+    @Override
+    public IOException visitChangeRecord(final ChangeRecordWriter p, final AddRequest change) {
+        try {
+            p.writeChangeRecord(change);
+            return null;
+        } catch (final IOException e) {
+            return e;
+        }
+    }
+
+    @Override
+    public IOException visitChangeRecord(final ChangeRecordWriter p, final DeleteRequest change) {
+        try {
+            p.writeChangeRecord(change);
+            return null;
+        } catch (final IOException e) {
+            return e;
+        }
+    }
+
+    @Override
+    public IOException visitChangeRecord(final ChangeRecordWriter p, final ModifyDNRequest change) {
+        try {
+            p.writeChangeRecord(change);
+            return null;
+        } catch (final IOException e) {
+            return e;
+        }
+    }
+
+    @Override
+    public IOException visitChangeRecord(final ChangeRecordWriter p, final ModifyRequest change) {
+        try {
+            p.writeChangeRecord(change);
+            return null;
+        } catch (final IOException e) {
+            return e;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java
new file mode 100644
index 0000000..2e1639a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java
@@ -0,0 +1,148 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+/**
+ * An interface for writing change records to a data source, typically an LDIF
+ * file.
+ */
+public interface ChangeRecordWriter extends Closeable, Flushable {
+    /**
+     * Closes this change record writer, flushing it first. Closing a previously
+     * closed change record writer has no effect.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Flushes this change record writer so that any buffered data is written
+     * immediately to underlying stream, flushing the stream if it is also
+     * {@code Flushable}.
+     * <p>
+     * If the intended destination of this stream is an abstraction provided by
+     * the underlying operating system, for example a file, then flushing the
+     * stream guarantees only that bytes previously written to the stream are
+     * passed to the operating system for writing; it does not guarantee that
+     * they are actually written to a physical device such as a disk drive.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while flushing.
+     */
+    @Override
+    void flush() throws IOException;
+
+    /**
+     * Writes an {@code Add} change record.
+     *
+     * @param change
+     *            The {@code AddRequest} to be written as an {@code Add} change
+     *            record.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the change
+     *             record.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    ChangeRecordWriter writeChangeRecord(AddRequest change) throws IOException;
+
+    /**
+     * Writes a change record.
+     *
+     * @param change
+     *            The {@code ChangeRecord} to be written.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the change
+     *             record.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    ChangeRecordWriter writeChangeRecord(ChangeRecord change) throws IOException;
+
+    /**
+     * Writes a {@code Delete} change record.
+     *
+     * @param change
+     *            The {@code DeleteRequest} to be written as an {@code Delete}
+     *            change record.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the change
+     *             record.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    ChangeRecordWriter writeChangeRecord(DeleteRequest change) throws IOException;
+
+    /**
+     * Writes a {@code ModifyDN} change record.
+     *
+     * @param change
+     *            The {@code ModifyDNRequest} to be written as an
+     *            {@code ModifyDN} change record.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the change
+     *             record.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    ChangeRecordWriter writeChangeRecord(ModifyDNRequest change) throws IOException;
+
+    /**
+     * Writes a {@code Modify} change record.
+     *
+     * @param change
+     *            The {@code ModifyRequest} to be written as an {@code Modify}
+     *            change record.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the change
+     *             record.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    ChangeRecordWriter writeChangeRecord(ModifyRequest change) throws IOException;
+
+    /**
+     * Writes a comment.
+     *
+     * @param comment
+     *            The {@code CharSequence} to be written as a comment.
+     * @return A reference to this change record writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the comment.
+     * @throws NullPointerException
+     *             If {@code comment} was {@code null}.
+     */
+    ChangeRecordWriter writeComment(CharSequence comment) throws IOException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriter.java
new file mode 100644
index 0000000..71078a5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriter.java
@@ -0,0 +1,210 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A {@code ConnectionChangeRecordWriter} is a bridge from {@code Connection}s
+ * to {@code ChangeRecordWriter}s. A connection change record writer writes
+ * change records by sending appropriate update requests (Add, Delete, Modify,
+ * or ModifyDN) to an underlying connection.
+ * <p>
+ * All update requests are performed synchronously, blocking until an update
+ * result is received. If an update result indicates that an update request has
+ * failed for some reason then the error result is propagated to the caller
+ * using an {@code LdapException}.
+ * <p>
+ * <b>Note:</b> comments are not supported by connection change record writers.
+ * Attempts to write comments will be ignored.
+ */
+public final class ConnectionChangeRecordWriter implements ChangeRecordWriter {
+    private final Connection connection;
+
+    /**
+     * Creates a new connection change record writer whose destination is the
+     * provided connection.
+     *
+     * @param connection
+     *            The connection to use.
+     * @throws NullPointerException
+     *             If {@code connection} was {@code null}.
+     */
+    public ConnectionChangeRecordWriter(final Connection connection) {
+        Reject.ifNull(connection);
+        this.connection = connection;
+    }
+
+    /**
+     * Closes this connection change record writer, including the underlying
+     * connection. Closing a previously closed change record writer has no
+     * effect.
+     */
+    @Override
+    public void close() {
+        connection.close();
+    }
+
+    /**
+     * Connection change record writers do not require flushing, so this method
+     * has no effect.
+     */
+    @Override
+    public void flush() {
+        // Do nothing.
+    }
+
+    /**
+     * Writes the provided Add request to the underlying connection, blocking
+     * until the request completes.
+     *
+     * @param change
+     *            The {@code AddRequest} to be written.
+     * @return A reference to this connection change record writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeChangeRecord(final AddRequest change) throws LdapException {
+        Reject.ifNull(change);
+        connection.add(change);
+        return this;
+    }
+
+    /**
+     * Writes the provided change record to the underlying connection, blocking
+     * until the request completes.
+     *
+     * @param change
+     *            The change record to be written.
+     * @return A reference to this connection change record writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeChangeRecord(final ChangeRecord change) throws LdapException {
+        Reject.ifNull(change);
+
+        final IOException e = change.accept(ChangeRecordVisitorWriter.getInstance(), this);
+        try {
+            if (e != null) {
+                throw e;
+            }
+        } catch (final LdapException e1) {
+            throw e1;
+        } catch (final IOException e1) {
+            // Should not happen.
+            throw new RuntimeException(e1);
+        }
+        return this;
+    }
+
+    /**
+     * Writes the provided Delete request to the underlying connection, blocking
+     * until the request completes.
+     *
+     * @param change
+     *            The {@code DeleteRequest} to be written.
+     * @return A reference to this connection change record writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeChangeRecord(final DeleteRequest change) throws LdapException {
+        Reject.ifNull(change);
+        connection.delete(change);
+        return this;
+    }
+
+    /**
+     * Writes the provided ModifyDN request to the underlying connection,
+     * blocking until the request completes.
+     *
+     * @param change
+     *            The {@code ModifyDNRequest} to be written.
+     * @return A reference to this connection change record writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeChangeRecord(final ModifyDNRequest change) throws LdapException {
+        Reject.ifNull(change);
+        connection.modifyDN(change);
+        return this;
+    }
+
+    /**
+     * Writes the provided Modify request to the underlying connection, blocking
+     * until the request completes.
+     *
+     * @param change
+     *            The {@code ModifyRequest} to be written.
+     * @return A reference to this connection change record writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code change} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeChangeRecord(final ModifyRequest change) throws LdapException {
+        Reject.ifNull(change);
+        connection.modify(change);
+        return this;
+    }
+
+    /**
+     * Connection change record writers do not support comments, so the provided
+     * comment will be ignored.
+     *
+     * @param comment
+     *            The {@code CharSequence} to be written as a comment.
+     * @return A reference to this connection change record writer.
+     * @throws NullPointerException
+     *             If {@code comment} was {@code null}.
+     */
+    @Override
+    public ConnectionChangeRecordWriter writeComment(final CharSequence comment) {
+        Reject.ifNull(comment);
+
+        // Do nothing.
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryReader.java
new file mode 100644
index 0000000..fcb5672
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryReader.java
@@ -0,0 +1,394 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Response;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.Reject;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+
+/**
+ * A {@code ConnectionEntryReader} is a bridge from {@code Connection}s to
+ * {@code EntryReader}s. A connection entry reader allows applications to
+ * iterate over search results as they are returned from the server during a
+ * search operation.
+ * <p>
+ * The Search operation is performed synchronously, blocking until a search
+ * result entry is received. If a search result indicates that the search
+ * operation has failed for some reason then the error result is propagated to
+ * the caller using an {@code LdapException}. If a search result
+ * reference is returned then it is propagated to the caller using a
+ * {@code SearchResultReferenceIOException}.
+ * <p>
+ * The following code illustrates how a {@code ConnectionEntryReader} may be
+ * used:
+ *
+ * <pre>
+ * Connection connection = ...;
+ * ConnectionEntryReader reader = connection.search(&quot;dc=example,dc=com&quot;,
+ *     SearchScope.WHOLE_SUBTREE, &quot;(objectClass=person)&quot;);
+ * try
+ * {
+ *   while (reader.hasNext())
+ *   {
+ *     if (reader.isEntry())
+ *     {
+ *       SearchResultEntry entry = reader.readEntry();
+ *
+ *       // Handle entry...
+ *     }
+ *     else
+ *     {
+ *       SearchResultReference ref = reader.readReference();
+ *
+ *       // Handle continuation reference...
+ *     }
+ *   }
+ *
+ *   Result result = reader.readResult();
+ *   // Handle controls included with the search result...
+ * }
+ * catch (IOException e)
+ * {
+ *   // Handle exceptions...
+ * }
+ * finally
+ * {
+ *   reader.close();
+ * }
+ * </pre>
+ *
+ * <b>NOTE:</b> although this class is non-final, sub-classing is not supported
+ * except when creating mock objects for unit tests. This class has been
+ * selected specifically because it is the only aspect of the {@code Connection}
+ * interface which is not mockable.
+ */
+public class ConnectionEntryReader implements EntryReader {
+    /* See OPENDJ-1124 for more discussion about why this class is non-final. */
+
+    /** Result handler that places all responses in a queue. */
+    private static final class BufferHandler implements SearchResultHandler, LdapResultHandler<Result> {
+        private final BlockingQueue<Response> responses;
+        private volatile boolean isInterrupted;
+
+        private BufferHandler(final BlockingQueue<Response> responses) {
+            this.responses = responses;
+        }
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            try {
+                responses.put(entry);
+                return true;
+            } catch (final InterruptedException e) {
+                // Prevent the reader from waiting for a result that will never
+                // arrive.
+                isInterrupted = true;
+                Thread.currentThread().interrupt();
+                return false;
+            }
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            try {
+                responses.put(error.getResult());
+            } catch (final InterruptedException e) {
+                // Prevent the reader from waiting for a result that will never
+                // arrive.
+                isInterrupted = true;
+                Thread.currentThread().interrupt();
+            }
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            try {
+                responses.put(reference);
+                return true;
+            } catch (final InterruptedException e) {
+                // Prevent the reader from waiting for a result that will never
+                // arrive.
+                isInterrupted = true;
+                Thread.currentThread().interrupt();
+                return false;
+            }
+        }
+
+        @Override
+        public void handleResult(final Result result) {
+            try {
+                responses.put(result);
+            } catch (final InterruptedException e) {
+                // Prevent the reader from waiting for a result that will never
+                // arrive.
+                isInterrupted = true;
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    private final BufferHandler buffer;
+    private final LdapPromise<Result> promise;
+    private Response nextResponse;
+
+    /**
+     * Creates a new connection entry reader whose destination is the provided
+     * connection using an unbounded {@code LinkedBlockingQueue}.
+     *
+     * @param connection
+     *            The connection to use.
+     * @param searchRequest
+     *            The search request to retrieve entries with.
+     * @throws NullPointerException
+     *             If {@code connection} was {@code null}.
+     */
+    public ConnectionEntryReader(final Connection connection, final SearchRequest searchRequest) {
+        this(connection, searchRequest, new LinkedBlockingQueue<Response>());
+    }
+
+    /**
+     * Creates a new connection entry reader whose destination is the provided
+     * connection.
+     *
+     * @param connection
+     *            The connection to use.
+     * @param searchRequest
+     *            The search request to retrieve entries with.
+     * @param entries
+     *            The {@code BlockingQueue} implementation to use when queuing
+     *            the returned entries.
+     * @throws NullPointerException
+     *             If {@code connection} was {@code null}.
+     */
+    public ConnectionEntryReader(final Connection connection, final SearchRequest searchRequest,
+        final BlockingQueue<Response> entries) {
+        Reject.ifNull(connection);
+        buffer = new BufferHandler(entries);
+        promise = connection.searchAsync(searchRequest, buffer).thenOnResult(buffer).thenOnException(buffer);
+    }
+
+    /** Closes this connection entry reader, canceling the search request if it is still active. */
+    @Override
+    public void close() {
+        // Cancel the search if it is still running.
+        promise.cancel(true);
+    }
+
+    @Override
+    public boolean hasNext() throws LdapException {
+        // Poll for the next response if needed.
+        final Response r = getNextResponse();
+        if (!(r instanceof Result)) {
+            // Entry or reference.
+            return true;
+        }
+
+        // Final result.
+        final Result result = (Result) r;
+        if (result.isSuccess()) {
+            return false;
+        }
+
+        throw newLdapException(result);
+    }
+
+    /**
+     * Waits for the next search result entry or reference to become available
+     * and returns {@code true} if it is an entry, or {@code false} if it is a
+     * reference.
+     *
+     * @return {@code true} if the next search result is an entry, or
+     *         {@code false} if it is a reference.
+     * @throws LdapException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             failed for some reason.
+     * @throws NoSuchElementException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             succeeded.
+     */
+    public boolean isEntry() throws LdapException {
+        // Throws LdapException if search returned error.
+        if (!hasNext()) {
+            // Search has completed successfully.
+            throw new NoSuchElementException();
+        }
+
+        // Entry or reference?
+        final Response r = nextResponse;
+        if (r instanceof SearchResultEntry) {
+            return true;
+        } else if (r instanceof SearchResultReference) {
+            return false;
+        } else {
+            throw new RuntimeException("Unexpected response type: " + r.getClass());
+        }
+    }
+
+    /**
+     * Waits for the next search result entry or reference to become available
+     * and returns {@code true} if it is a reference, or {@code false} if it is
+     * an entry.
+     *
+     * @return {@code true} if the next search result is a reference, or
+     *         {@code false} if it is an entry.
+     * @throws LdapException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             failed for some reason.
+     * @throws NoSuchElementException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             succeeded.
+     */
+    public boolean isReference() throws LdapException {
+        return !isEntry();
+    }
+
+    /**
+     * Waits for the next search result entry or reference to become available
+     * and, if it is an entry, returns it as a {@code SearchResultEntry}. If the
+     * next search response is a reference then this method will throw a
+     * {@code SearchResultReferenceIOException}.
+     *
+     * @return The next search result entry.
+     * @throws SearchResultReferenceIOException
+     *             If the next search response was a search result reference.
+     *             This connection entry reader may still contain remaining
+     *             search results and references which can be retrieved using
+     *             additional calls to this method.
+     * @throws LdapException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             failed for some reason.
+     * @throws NoSuchElementException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             succeeded.
+     */
+    @Override
+    public SearchResultEntry readEntry() throws SearchResultReferenceIOException, LdapException {
+        if (isEntry()) {
+            final SearchResultEntry entry = (SearchResultEntry) nextResponse;
+            nextResponse = null;
+            return entry;
+        } else {
+            final SearchResultReference reference = (SearchResultReference) nextResponse;
+            nextResponse = null;
+            throw new SearchResultReferenceIOException(reference);
+        }
+    }
+
+    /**
+     * Waits for the next search result entry or reference to become available
+     * and, if it is a reference, returns it as a {@code SearchResultReference}.
+     * If the next search response is an entry then this method will return
+     * {@code null}.
+     *
+     * @return The next search result reference, or {@code null} if the next
+     *         response was a search result entry.
+     * @throws LdapException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             failed for some reason.
+     * @throws NoSuchElementException
+     *             If there are no more search result entries or references and
+     *             the search result code indicates that the search operation
+     *             succeeded.
+     */
+    public SearchResultReference readReference() throws LdapException {
+        if (isReference()) {
+            final SearchResultReference reference = (SearchResultReference) nextResponse;
+            nextResponse = null;
+            return reference;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Waits for the next search response to become available and returns it if
+     * it is a search result indicating that the search completed successfully.
+     * If the search result indicates that the search failed then an
+     * {@link LdapException} is thrown. Otherwise, if the search
+     * response represents an entry or reference then an
+     * {@code IllegalStateException} is thrown.
+     * <p>
+     * This method should only be called if {@link #hasNext()} has, or will,
+     * return {@code false}.
+     * <p>
+     * It is not necessary to call this method once all search result entries
+     * have been processed, but it may be useful to do so in order to inspect
+     * any controls which were included with the result. For example, this
+     * method may be called in order to obtain the next paged results cookie
+     * once the current page of results has been processed.
+     *
+     * @return The search result indicating success.
+     * @throws LdapException
+     *             If the search result indicates that the search operation
+     *             failed for some reason.
+     * @throws IllegalStateException
+     *             If there are remaining search result entries or references to
+     *             be processed. In other words, if {@link #hasNext()} would
+     *             return {@code true}.
+     */
+    public Result readResult() throws LdapException {
+        if (hasNext()) {
+            throw new IllegalStateException();
+        } else {
+            return (Result) nextResponse;
+        }
+    }
+
+    private Response getNextResponse() throws LdapException {
+        while (nextResponse == null) {
+            try {
+                nextResponse = buffer.responses.poll(50, TimeUnit.MILLISECONDS);
+            } catch (final InterruptedException e) {
+                throw newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
+            }
+
+            if (nextResponse == null && buffer.isInterrupted) {
+                // The worker thread processing the result was interrupted so no
+                // result will ever arrive. We don't want to hang this thread
+                // forever while we wait, so terminate now.
+                nextResponse = Responses.newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR);
+                break;
+            }
+        }
+        return nextResponse;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryWriter.java
new file mode 100644
index 0000000..38ec1c3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/ConnectionEntryWriter.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A {@code ConnectionEntryWriter} is a bridge from {@code Connection}s to
+ * {@code EntryWriter}s. A connection entry writer writes entries by sending Add
+ * requests to an underlying connection.
+ * <p>
+ * All Add requests are performed synchronously, blocking until an Add result is
+ * received. If an Add result indicates that an Add request has failed for some
+ * reason then the error result is propagated to the caller using an
+ * {@code LdapException}.
+ * <p>
+ * <b>Note:</b> comments are not supported by connection change record writers.
+ * Attempts to write comments will be ignored.
+ */
+public final class ConnectionEntryWriter implements EntryWriter {
+    private final Connection connection;
+
+    /**
+     * Creates a new connection entry writer whose destination is the provided
+     * connection.
+     *
+     * @param connection
+     *            The connection to use.
+     * @throws NullPointerException
+     *             If {@code connection} was {@code null}.
+     */
+    public ConnectionEntryWriter(final Connection connection) {
+        Reject.ifNull(connection);
+        this.connection = connection;
+    }
+
+    /**
+     * Closes this connection entry writer, including the underlying connection.
+     * Closing a previously closed entry writer has no effect.
+     */
+    @Override
+    public void close() {
+        connection.close();
+    }
+
+    /**
+     * Connection entry writers do not require flushing, so this method has no
+     * effect.
+     */
+    @Override
+    public void flush() {
+        // Do nothing.
+    }
+
+    /**
+     * Connection entry writers do not support comments, so the provided comment
+     * will be ignored.
+     *
+     * @param comment
+     *            The {@code CharSequence} to be written as a comment.
+     * @return A reference to this connection entry writer.
+     * @throws NullPointerException
+     *             If {@code comment} was {@code null}.
+     */
+    @Override
+    public ConnectionEntryWriter writeComment(final CharSequence comment) {
+        Reject.ifNull(comment);
+
+        // Do nothing.
+        return this;
+    }
+
+    /**
+     * Writes an entry to the underlying connection using an Add request,
+     * blocking until the request completes.
+     *
+     * @param entry
+     *            The {@code Entry} to be written.
+     * @return A reference to this connection entry writer.
+     * @throws LdapException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    @Override
+    public ConnectionEntryWriter writeEntry(final Entry entry) throws LdapException {
+        Reject.ifNull(entry);
+        connection.add(entry);
+        return this;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java
new file mode 100644
index 0000000..a78eff2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java
@@ -0,0 +1,316 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Random;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A template driven entry generator, as used by the make-ldif tool.
+ * <p>
+ * To build a generator with default values, including default template file,
+ * use the empty constructor:
+ *
+ * <pre>
+ * generator = new EntryGenerator();
+ * </pre>
+ * <p>
+ * To build a generator with some custom values, use the non-empty constructor
+ * and the <code>set</code> methods:
+ *
+ * <pre>
+ * generator = new EntryGenerator(templatePath).setResourcePath(path).setSchema(schema)
+ * </pre>
+ */
+public final class EntryGenerator implements EntryReader {
+
+    /** Template file that contains directives for generation of entries. */
+    private TemplateFile templateFile;
+
+    /** Warnings issued by the parsing of the template file. */
+    private final List<LocalizableMessage> warnings = new LinkedList<>();
+
+    /** Indicates if the generator is closed. */
+    private boolean isClosed;
+
+    /** Indicates if the generator is initialized, which means template file has been parsed. */
+    private boolean isInitialized;
+
+    /** Random seed is used to generate random data. */
+    private Random random = new Random();
+
+    /**
+     * Path to the directory that may contain additional resource files needed
+     * during the generation process. It may be {@code null}.
+     */
+    private String resourcePath;
+
+    /**
+     * Schema is used to create attributes. If not provided, the default schema
+     * is used.
+     */
+    private Schema schema;
+
+    /**
+     * Path of template file, can be {@code null} if template file has been
+     * provided through another way.
+     */
+    private String templatePath;
+
+    /**
+     * Lines of template file, can be {@code null} if template file has been
+     * provided through another way.
+     */
+    private String[] templateLines;
+
+    /**
+     * Input stream containing template file, can be {@code null} if template
+     * file has been provided through another way.
+     */
+    private InputStream templateStream;
+
+    /** Indicates whether branch entries should be generated.
+     *
+     *  Default is {@code true}
+     */
+    private boolean generateBranches = true;
+
+    /** Dictionary of constants to use in the template file. */
+    private Map<String, String> constants = new HashMap<>();
+
+    /**
+     * Creates a generator using default values.
+     * <p>
+     * The default template file will be used to generate entries.
+     */
+    public EntryGenerator() {
+        // nothing to do
+    }
+
+    /**
+     * Creates a generator from the provided template path.
+     *
+     * @param templatePath
+     *            Path of the template file.
+     */
+    public EntryGenerator(final String  templatePath) {
+        Reject.ifNull(templatePath);
+        this.templatePath = templatePath;
+    }
+
+    /**
+     * Creates a generator from the provided template lines.
+     *
+     * @param templateLines
+     *            Lines defining the template file.
+     */
+    public EntryGenerator(final String... templateLines) {
+        Reject.ifNull(templateLines);
+        this.templateLines = templateLines;
+    }
+
+    /**
+     * Creates a generator from the provided template lines.
+     *
+     * @param templateLines
+     *            Lines defining the template file.
+     */
+    public EntryGenerator(final List<String> templateLines) {
+        Reject.ifNull(templateLines);
+        this.templateLines = templateLines.toArray(new String[templateLines.size()]);
+    }
+
+    /**
+     * Creates a generator from the provided input stream.
+     *
+     * @param templateStream
+     *            Input stream to read the template file.
+     */
+    public EntryGenerator(final InputStream templateStream) {
+        Reject.ifNull(templateStream);
+        this.templateStream = templateStream;
+    }
+
+    /**
+     * Sets the random seed to use when generating entries.
+     *
+     * @param seed
+     *            The random seed to use.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setRandomSeed(final int seed) {
+        random = new Random(seed);
+        return this;
+    }
+
+    /**
+     * Sets the resource path, used to looks for resources files like first
+     * names, last names, or other custom resources.
+     *
+     * @param path
+     *            The resource path.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setResourcePath(final String path) {
+        Reject.ifNull(path);
+        resourcePath = path;
+        return this;
+    }
+
+    /**
+     * Sets the schema which should be when generating entries. The default
+     * schema is used if no other is specified.
+     *
+     * @param schema
+     *            The schema which should be used for generating entries.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setSchema(final Schema schema) {
+        this.schema = schema;
+        return this;
+    }
+
+    /**
+     * Sets a constant to use in template file. It overrides the constant set in
+     * the template file.
+     *
+     * @param name
+     *            The name of the constant.
+     * @param value
+     *            The value of the constant.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setConstant(String name, Object value) {
+        constants.put(name, value.toString());
+        return this;
+    }
+
+    /**
+     * Sets the flag which indicates whether branch entries should be generated.
+     *
+     * The default is {@code true}.
+     *
+     * @param generateBranches
+     *              Indicates whether or not the branches DN entries has to be generated.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setGenerateBranches(boolean generateBranches) {
+        this.generateBranches = generateBranches;
+        return this;
+    }
+
+    /**
+     * Checks if there are some warning(s) after parsing the template file.
+     * <p>
+     * Warnings are available only after the first call to {@code hasNext()} or
+     * {@code readEntry()} methods.
+     *
+     * @return {@code true} if there is at least one warning.
+     */
+    public boolean hasWarnings() {
+        return !warnings.isEmpty();
+    }
+
+    /**
+     * Returns the warnings generated by the parsing of template file.
+     * <p>
+     * Warnings are available only after the first call to {@code hasNext()} or
+     * {@code readEntry()} methods.
+     *
+     * @return The list of warnings, which is empty if there are no warnings.
+     */
+    public List<LocalizableMessage> getWarnings() {
+        return Collections.unmodifiableList(warnings);
+    }
+
+    @Override
+    public void close() {
+        isClosed = true;
+    }
+
+    @Override
+    public boolean hasNext() throws IOException {
+        if (isClosed) {
+            return false;
+        }
+        ensureGeneratorIsInitialized();
+        return templateFile.hasNext();
+    }
+
+    @Override
+    public Entry readEntry() throws IOException {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        } else {
+            return templateFile.nextEntry();
+        }
+    }
+
+    /**
+     * Check that generator is initialized, and initialize it
+     * if it has not been initialized.
+     */
+    private void ensureGeneratorIsInitialized() throws IOException {
+        if (!isInitialized) {
+            isInitialized = true;
+            initialize();
+        }
+    }
+
+    /**
+     * Initializes the generator, by retrieving template file and parsing it.
+     */
+    private void initialize() throws IOException {
+        if (schema == null) {
+            schema = Schema.getDefaultSchema();
+        }
+        templateFile = new TemplateFile(schema, constants, resourcePath, random, generateBranches);
+        try {
+            if (templatePath != null) {
+                templateFile.parse(templatePath, warnings);
+            } else if (templateLines != null) {
+                templateFile.parse(templateLines, warnings);
+            } else if (templateStream != null) {
+                templateFile.parse(templateStream, warnings);
+            } else {
+                // use default template file
+                templateFile.parse(warnings);
+            }
+        } catch (IOException e) {
+            throw e;
+        } catch (Exception e) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_EXCEPTION_DURING_PARSE.get(e.getMessage()), e);
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryReader.java
new file mode 100644
index 0000000..e4ed1c1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryReader.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.NoSuchElementException;
+
+import org.forgerock.opendj.ldap.Entry;
+
+/**
+ * An interface for reading entries from a data source, typically an LDIF file.
+ * <p>
+ * Implementations must specify the following:
+ * <ul>
+ * <li>Whether or not it is possible for the implementation to encounter
+ * malformed change records and, if it is possible, how they are handled.
+ * <li>Any synchronization limitations.
+ * </ul>
+ */
+public interface EntryReader extends Closeable {
+
+    /**
+     * Closes this entry reader if it is not already closed. Note that this
+     * method does not need to be called if a previous call of
+     * {@link #readEntry()} has returned {@code null}.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Returns {@code true} if this reader contains another entry, blocking if
+     * necessary until either the next entry is available or the end of the
+     * stream is reached.
+     *
+     * @return {@code true} if this reader contains another entry.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    boolean hasNext() throws IOException;
+
+    /**
+     * Reads the next entry, blocking if necessary until an entry is available.
+     *
+     * @return The next entry.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entry.
+     * @throws NoSuchElementException
+     *             If this reader does not contain any more entries.
+     */
+    Entry readEntry() throws IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java
new file mode 100644
index 0000000..dad41fa
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.Entry;
+
+/**
+ * An interface for writing entries to a data source, typically an LDIF file.
+ */
+public interface EntryWriter extends Closeable, Flushable {
+    /**
+     * Closes this entry writer, flushing it first. Closing a previously closed
+     * entry writer has no effect.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while closing.
+     */
+    @Override
+    void close() throws IOException;
+
+    /**
+     * Flushes this entry writer so that any buffered data is written
+     * immediately to underlying stream, flushing the stream if it is also
+     * {@code Flushable}.
+     * <p>
+     * If the intended destination of this stream is an abstraction provided by
+     * the underlying operating system, for example a file, then flushing the
+     * stream guarantees only that bytes previously written to the stream are
+     * passed to the operating system for writing; it does not guarantee that
+     * they are actually written to a physical device such as a disk drive.
+     *
+     * @throws IOException
+     *             If an unexpected IO error occurred while flushing.
+     */
+    @Override
+    void flush() throws IOException;
+
+    /**
+     * Writes a comment.
+     *
+     * @param comment
+     *            The {@code CharSequence} to be written as a comment.
+     * @return A reference to this entry writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the comment.
+     * @throws NullPointerException
+     *             If {@code comment} was {@code null}.
+     */
+    EntryWriter writeComment(CharSequence comment) throws IOException;
+
+    /**
+     * Writes an entry.
+     *
+     * @param entry
+     *            The {@code Entry} to be written.
+     * @return A reference to this entry writer.
+     * @throws IOException
+     *             If an unexpected IO error occurred while writing the entry.
+     * @throws NullPointerException
+     *             If {@code entry} was {@code null}.
+     */
+    EntryWriter writeEntry(Entry entry) throws IOException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java
new file mode 100644
index 0000000..6b6fba0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java
@@ -0,0 +1,848 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.AVA;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Matcher;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.schema.AttributeUsage;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * This class contains common utility methods for creating and manipulating
+ * readers and writers.
+ */
+public final class LDIF {
+    // @formatter:off
+    private static final class EntryIteratorReader implements EntryReader {
+        private final Iterator<Entry> iterator;
+        private EntryIteratorReader(final Iterator<Entry> iterator) { this.iterator = iterator; }
+        @Override
+        public void close()      { }
+        @Override
+        public boolean hasNext() { return iterator.hasNext(); }
+        @Override
+        public Entry readEntry() { return iterator.next(); }
+    }
+    // @formatter:on
+
+    /**
+     * Comparator ordering the DN ASC.
+     */
+    private static final Comparator<byte[][]> DN_ORDER2 = new Comparator<byte[][]>() {
+        @Override
+        public int compare(byte[][] b1, byte[][] b2) {
+            return DN_ORDER.compare(b1[0], b2[0]);
+        }
+    };
+
+    /**
+     * Comparator ordering the DN ASC.
+     */
+    private static final Comparator<byte[]> DN_ORDER = new Comparator<byte[]>() {
+        @Override
+        public int compare(byte[] b1, byte[] b2) {
+            final ByteString bs = ByteString.valueOfBytes(b1);
+            final ByteString bs2 = ByteString.valueOfBytes(b2);
+            return bs.compareTo(bs2);
+        }
+    };
+
+    /**
+     * Copies the content of {@code input} to {@code output}. This method does
+     * not close {@code input} or {@code output}.
+     *
+     * @param input
+     *            The input change record reader.
+     * @param output
+     *            The output change record reader.
+     * @return The output change record reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public static ChangeRecordWriter copyTo(final ChangeRecordReader input,
+            final ChangeRecordWriter output) throws IOException {
+        while (input.hasNext()) {
+            output.writeChangeRecord(input.readChangeRecord());
+        }
+        return output;
+    }
+
+    /**
+     * Copies the content of {@code input} to {@code output}. This method does
+     * not close {@code input} or {@code output}.
+     *
+     * @param input
+     *            The input entry reader.
+     * @param output
+     *            The output entry reader.
+     * @return The output entry reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public static EntryWriter copyTo(final EntryReader input, final EntryWriter output)
+            throws IOException {
+        while (input.hasNext()) {
+            output.writeEntry(input.readEntry());
+        }
+        return output;
+    }
+
+    /**
+     * Compares the content of {@code source} to the content of {@code target}
+     * and returns the differences in a change record reader. Closing the
+     * returned reader will cause {@code source} and {@code target} to be closed
+     * as well.
+     * <p>
+     * <b>NOTE:</b> this method reads the content of {@code source} and
+     * {@code target} into memory before calculating the differences, and is
+     * therefore not suited for use in cases where a very large number of
+     * entries are to be compared.
+     *
+     * @param source
+     *            The entry reader containing the source entries to be compared.
+     * @param target
+     *            The entry reader containing the target entries to be compared.
+     * @return A change record reader containing the differences.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public static ChangeRecordReader diff(final EntryReader source, final EntryReader target)
+            throws IOException {
+
+        final List<byte[][]> source2 = readEntriesAsList(source);
+        final List<byte[][]> target2 = readEntriesAsList(target);
+        final Iterator<byte[][]> sourceIterator = source2.iterator();
+        final Iterator<byte[][]> targetIterator = target2.iterator();
+
+        return new ChangeRecordReader() {
+            private Entry sourceEntry = nextEntry(sourceIterator);
+            private Entry targetEntry = nextEntry(targetIterator);
+
+            @Override
+            public void close() throws IOException {
+                try {
+                    source.close();
+                } finally {
+                    target.close();
+                }
+            }
+
+            @Override
+            public boolean hasNext() {
+                return sourceEntry != null || targetEntry != null;
+            }
+
+            @Override
+            public ChangeRecord readChangeRecord() throws IOException {
+                if (sourceEntry != null && targetEntry != null) {
+                    final DN sourceDN = sourceEntry.getName();
+                    final DN targetDN = targetEntry.getName();
+                    final int cmp = sourceDN.compareTo(targetDN);
+
+                    if (cmp == 0) {
+                        // Modify record: entry in both source and target.
+                        final ModifyRequest request =
+                                Requests.newModifyRequest(sourceEntry, targetEntry);
+                        sourceEntry = nextEntry(sourceIterator);
+                        targetEntry = nextEntry(targetIterator);
+                        return request;
+                    } else if (cmp < 0) {
+                        // Delete record: entry in source but not in target.
+                        final DeleteRequest request =
+                                Requests.newDeleteRequest(sourceEntry.getName());
+                        sourceEntry = nextEntry(sourceIterator);
+                        return request;
+                    } else {
+                        // Add record: entry in target but not in source.
+                        final AddRequest request = Requests.newAddRequest(targetEntry);
+                        targetEntry = nextEntry(targetIterator);
+                        return request;
+                    }
+                } else if (sourceEntry != null) {
+                    // Delete remaining source records.
+                    final DeleteRequest request = Requests.newDeleteRequest(sourceEntry.getName());
+                    sourceEntry = nextEntry(sourceIterator);
+                    return request;
+                } else if (targetEntry != null) {
+                    // Add remaining target records.
+                    final AddRequest request = Requests.newAddRequest(targetEntry);
+                    targetEntry = nextEntry(targetIterator);
+                    return request;
+                } else {
+                    throw new NoSuchElementException();
+                }
+            }
+
+            private Entry nextEntry(final Iterator<byte[][]> i) {
+                if (i.hasNext()) {
+                    return decodeEntry(i.next()[1]);
+                }
+                return null;
+            }
+        };
+    }
+
+    /**
+     * Builds an entry from the provided lines of LDIF.
+     * <p>
+     * Sample usage:
+     * <pre>
+     * Entry john = makeEntry(
+     *   "dn: cn=John Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: John Smith",
+     *   "sn: Smith",
+     *   "givenname: John");
+     * </pre>
+     *
+     * @param ldifLines
+     *          LDIF lines that contains entry definition.
+     * @return an entry
+     * @throws LocalizedIllegalArgumentException
+     *            If {@code ldifLines} did not contain an LDIF entry, or
+     *            contained multiple entries, or contained malformed LDIF, or
+     *            if the entry could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static Entry makeEntry(String... ldifLines) {
+        // returns a non-empty list
+        List<Entry> entries = makeEntries(ldifLines);
+        if (entries.size() > 1) {
+            throw new LocalizedIllegalArgumentException(
+                WARN_READ_LDIF_ENTRY_MULTIPLE_ENTRIES_FOUND.get(entries.size()));
+        }
+        return entries.get(0);
+    }
+
+    /**
+     * Builds an entry from the provided lines of LDIF.
+     *
+     * @param ldifLines
+     *            LDIF lines that contains entry definition.
+     * @return an entry
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain an LDIF entry, or
+     *             contained multiple entries, or contained malformed LDIF, or
+     *             if the entry could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     * @see LDIF#makeEntry(String...)
+     */
+    public static Entry makeEntry(List<String> ldifLines) {
+        return makeEntry(ldifLines.toArray(new String[ldifLines.size()]));
+    }
+
+    /**
+     * Builds a list of entries from the provided lines of LDIF.
+     * <p>
+     * Sample usage:
+     * <pre>
+     * List<Entry> smiths = TestCaseUtils.makeEntries(
+     *   "dn: cn=John Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: John Smith",
+     *   "sn: Smith",
+     *   "givenname: John",
+     *   "",
+     *   "dn: cn=Jane Smith,dc=example,dc=com",
+     *   "objectclass: inetorgperson",
+     *   "cn: Jane Smith",
+     *   "sn: Smith",
+     *   "givenname: Jane");
+     * </pre>
+     * @param ldifLines
+     *          LDIF lines that contains entries definition.
+     *          Entries are separated by an empty string: {@code ""}.
+     * @return a non empty list of entries
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain LDIF entries,
+     *             or contained malformed LDIF, or if the entries
+     *             could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static List<Entry> makeEntries(String... ldifLines) {
+        List<Entry> entries = new ArrayList<>();
+        try (LDIFEntryReader reader = new LDIFEntryReader(ldifLines)) {
+            while (reader.hasNext()) {
+                entries.add(reader.readEntry());
+            }
+        } catch (final DecodeException e) {
+            // Badly formed LDIF.
+            throw new LocalizedIllegalArgumentException(e.getMessageObject());
+        } catch (final IOException e) {
+            // This should never happen for a String based reader.
+            throw new LocalizedIllegalArgumentException(WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage()));
+        }
+        if (entries.isEmpty()) {
+            throw new LocalizedIllegalArgumentException(WARN_READ_LDIF_ENTRY_NO_ENTRY_FOUND.get());
+        }
+        return entries;
+    }
+
+    /**
+     * Builds a list of entries from the provided lines of LDIF.
+     *
+     * @param ldifLines
+     *            LDIF lines that contains entries definition. Entries are
+     *            separated by an empty string: {@code ""}.
+     * @return a non empty list of entries
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain LDIF entries, or
+     *             contained malformed LDIF, or if the entries could not be
+     *             decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     * @see LDIF#makeEntries(String...)
+     */
+    public static List<Entry> makeEntries(List<String> ldifLines) {
+        return makeEntries(ldifLines.toArray(new String[ldifLines.size()]));
+    }
+
+    /**
+     * Returns an entry reader over the provided entry collection.
+     *
+     * @param entries
+     *            The entry collection.
+     * @return An entry reader over the provided entry collection.
+     */
+    public static EntryReader newEntryCollectionReader(final Collection<Entry> entries) {
+        return new EntryIteratorReader(entries.iterator());
+    }
+
+    /**
+     * Returns an entry reader over the provided entry iterator.
+     *
+     * @param entries
+     *            The entry iterator.
+     * @return An entry reader over the provided entry iterator.
+     */
+    public static EntryReader newEntryIteratorReader(final Iterator<Entry> entries) {
+        return new EntryIteratorReader(entries);
+    }
+
+    /**
+     * Applies the set of changes contained in {@code patch} to the content of
+     * {@code input} and returns the result in an entry reader. This method
+     * ignores missing entries, and overwrites existing entries. Closing the
+     * returned reader will cause {@code input} and {@code patch} to be closed
+     * as well.
+     * <p>
+     * <b>NOTE:</b> this method reads the content of {@code input} into memory
+     * before applying the changes, and is therefore not suited for use in cases
+     * where a very large number of entries are to be patched.
+     * <p>
+     * <b>NOTE:</b> this method will not perform modifications required in order
+     * to maintain referential integrity. In particular, if an entry references
+     * another entry using a DN valued attribute and the referenced entry is
+     * deleted, then the DN reference will not be removed. The same applies to
+     * renamed entries and their references.
+     *
+     * @param input
+     *            The entry reader containing the set of entries to be patched.
+     * @param patch
+     *            The change record reader containing the set of changes to be
+     *            applied.
+     * @return An entry reader containing the patched entries.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public static EntryReader patch(final EntryReader input, final ChangeRecordReader patch)
+            throws IOException {
+        return patch(input, patch, RejectedChangeRecordListener.OVERWRITE);
+    }
+
+    /**
+     * Applies the set of changes contained in {@code patch} to the content of
+     * {@code input} and returns the result in an entry reader. Closing the
+     * returned reader will cause {@code input} and {@code patch} to be closed
+     * as well.
+     * <p>
+     * <b>NOTE:</b> this method reads the content of {@code input} into memory
+     * before applying the changes, and is therefore not suited for use in cases
+     * where a very large number of entries are to be patched.
+     * <p>
+     * <b>NOTE:</b> this method will not perform modifications required in order
+     * to maintain referential integrity. In particular, if an entry references
+     * another entry using a DN valued attribute and the referenced entry is
+     * deleted, then the DN reference will not be removed. The same applies to
+     * renamed entries and their references.
+     *
+     * @param input
+     *            The entry reader containing the set of entries to be patched.
+     * @param patch
+     *            The change record reader containing the set of changes to be
+     *            applied.
+     * @param listener
+     *            The rejected change listener.
+     * @return An entry reader containing the patched entries.
+     * @throws IOException
+     *             If an unexpected IO error occurred.
+     */
+    public static EntryReader patch(final EntryReader input, final ChangeRecordReader patch,
+            final RejectedChangeRecordListener listener) throws IOException {
+        final SortedMap<byte[], byte[]> entries = readEntriesAsMap(input);
+
+        while (patch.hasNext()) {
+            final ChangeRecord change = patch.readChangeRecord();
+            final DN changeDN = change.getName();
+            final byte[] changeNormDN = toNormalizedByteArray(change.getName());
+
+            final DecodeException de =
+                    change.accept(new ChangeRecordVisitor<DecodeException, Void>() {
+
+                        @Override
+                        public DecodeException visitChangeRecord(final Void p,
+                                final AddRequest change) {
+
+                            if (entries.get(changeNormDN) != null) {
+                                final Entry existingEntry = decodeEntry(entries.get(changeNormDN));
+                                try {
+                                    final Entry entry =
+                                            listener.handleDuplicateEntry(change, existingEntry);
+                                    entries.put(toNormalizedByteArray(entry.getName()), encodeEntry(entry)[1]);
+                                } catch (final DecodeException e) {
+                                    return e;
+                                }
+                            } else {
+                                entries.put(changeNormDN, encodeEntry(change)[1]);
+                            }
+                            return null;
+                        }
+
+                        @Override
+                        public DecodeException visitChangeRecord(final Void p,
+                                final DeleteRequest change) {
+                            if (entries.get(changeNormDN) == null) {
+                                try {
+                                    listener.handleRejectedChangeRecord(change,
+                                            REJECTED_CHANGE_FAIL_DELETE.get(change.getName()
+                                                    .toString()));
+                                } catch (final DecodeException e) {
+                                    return e;
+                                }
+                            } else {
+                                try {
+                                    if (change.getControl(SubtreeDeleteRequestControl.DECODER,
+                                            new DecodeOptions()) != null) {
+                                        entries.subMap(
+                                            toNormalizedByteArray(change.getName()),
+                                            toNormalizedByteArray(change.getName().child(RDN.maxValue()))).clear();
+                                    } else {
+                                        entries.remove(changeNormDN);
+                                    }
+                                } catch (final DecodeException e) {
+                                    return e;
+                                }
+
+                            }
+                            return null;
+                        }
+
+                        @Override
+                        public DecodeException visitChangeRecord(final Void p,
+                                final ModifyDNRequest change) {
+                            if (entries.get(changeNormDN) == null) {
+                                try {
+                                    listener.handleRejectedChangeRecord(change,
+                                            REJECTED_CHANGE_FAIL_MODIFYDN.get(change.getName()
+                                                    .toString()));
+                                } catch (final DecodeException e) {
+                                    return e;
+                                }
+                            } else {
+                                // Calculate the old and new DN.
+                                final DN oldDN = changeDN;
+
+                                DN newSuperior = change.getNewSuperior();
+                                if (newSuperior == null) {
+                                    newSuperior = change.getName().parent();
+                                    if (newSuperior == null) {
+                                        newSuperior = DN.rootDN();
+                                    }
+                                }
+                                final DN newDN = newSuperior.child(change.getNewRDN());
+
+                                // Move the renamed entries into a separate map
+                                // in order to avoid cases where the renamed subtree overlaps.
+                                final SortedMap<byte[], byte[]> renamedEntries = new TreeMap<>(DN_ORDER);
+
+                                // @formatter:off
+                                final Iterator<Map.Entry<byte[], byte[]>> i =
+                                    entries.subMap(changeNormDN,
+                                        toNormalizedByteArray(changeDN.child(RDN.maxValue()))).entrySet().iterator();
+                                // @formatter:on
+
+                                while (i.hasNext()) {
+                                    final Map.Entry<byte[], byte[]> e = i.next();
+                                    final Entry entry = decodeEntry(e.getValue());
+                                    final DN renamedDN = entry.getName().rename(oldDN, newDN);
+                                    entry.setName(renamedDN);
+                                    renamedEntries.put(toNormalizedByteArray(renamedDN), encodeEntry(entry)[1]);
+                                    i.remove();
+                                }
+
+                                // Modify target entry
+                                final Entry targetEntry =
+                                        decodeEntry(renamedEntries.values().iterator().next());
+
+                                if (change.isDeleteOldRDN()) {
+                                    for (final AVA ava : oldDN.rdn()) {
+                                        targetEntry.removeAttribute(ava.toAttribute(), null);
+                                    }
+                                }
+                                for (final AVA ava : newDN.rdn()) {
+                                    targetEntry.addAttribute(ava.toAttribute());
+                                }
+
+                                renamedEntries.remove(toNormalizedByteArray(targetEntry.getName()));
+                                renamedEntries.put(toNormalizedByteArray(targetEntry.getName()),
+                                        encodeEntry(targetEntry)[1]);
+
+                                // Add the renamed entries.
+                                final Iterator<byte[]> j = renamedEntries.values().iterator();
+                                while (j.hasNext()) {
+                                    final Entry renamedEntry = decodeEntry(j.next());
+                                    final byte[] existingEntryDn =
+                                            entries.get(toNormalizedByteArray(renamedEntry.getName()));
+
+                                    if (existingEntryDn != null) {
+                                        final Entry existingEntry = decodeEntry(existingEntryDn);
+                                        try {
+                                            final Entry tmp =
+                                                    listener.handleDuplicateEntry(change,
+                                                            existingEntry, renamedEntry);
+                                            entries.put(toNormalizedByteArray(tmp.getName()), encodeEntry(tmp)[1]);
+                                        } catch (final DecodeException e) {
+                                            return e;
+                                        }
+                                    } else {
+                                        entries.put(toNormalizedByteArray(renamedEntry.getName()),
+                                                encodeEntry(renamedEntry)[1]);
+                                    }
+                                }
+                                renamedEntries.clear();
+                            }
+                            return null;
+                        }
+
+                        @Override
+                        public DecodeException visitChangeRecord(final Void p,
+                                final ModifyRequest change) {
+                            if (entries.get(changeNormDN) == null) {
+                                try {
+                                    listener.handleRejectedChangeRecord(change,
+                                            REJECTED_CHANGE_FAIL_MODIFY.get(change.getName()
+                                                    .toString()));
+                                } catch (final DecodeException e) {
+                                    return e;
+                                }
+                            } else {
+                                final Entry entry = decodeEntry(entries.get(changeNormDN));
+                                for (final Modification modification : change.getModifications()) {
+                                    final ModificationType modType =
+                                            modification.getModificationType();
+                                    if (modType.equals(ModificationType.ADD)) {
+                                        entry.addAttribute(modification.getAttribute(), null);
+                                    } else if (modType.equals(ModificationType.DELETE)) {
+                                        entry.removeAttribute(modification.getAttribute(), null);
+                                    } else if (modType.equals(ModificationType.REPLACE)) {
+                                        entry.replaceAttribute(modification.getAttribute());
+                                    } else {
+                                        System.err.println("Unable to apply \"" + modType
+                                                + "\" modification to entry \"" + change.getName()
+                                                + "\": modification type not supported");
+                                    }
+                                }
+                                entries.put(changeNormDN, encodeEntry(entry)[1]);
+                            }
+                            return null;
+                        }
+
+                    }, null);
+
+            if (de != null) {
+                throw de;
+            }
+        }
+
+        return new EntryReader() {
+            private final Iterator<byte[]> iterator = entries.values().iterator();
+
+            @Override
+            public void close() throws IOException {
+                try {
+                    input.close();
+                } finally {
+                    patch.close();
+                }
+            }
+
+            @Override
+            public boolean hasNext() throws IOException {
+                return iterator.hasNext();
+            }
+
+            @Override
+            public Entry readEntry() throws IOException {
+                return decodeEntry(iterator.next());
+            }
+        };
+    }
+
+    /**
+     * Returns a filtered view of {@code input} containing only those entries
+     * which match the search base DN, scope, and filtered defined in
+     * {@code search}. In addition, returned entries will be filtered according
+     * to any attribute filtering criteria defined in the search request.
+     * <p>
+     * The filter and attribute descriptions will be decoded using the default
+     * schema.
+     *
+     * @param input
+     *            The entry reader containing the set of entries to be filtered.
+     * @param search
+     *            The search request defining the filtering criteria.
+     * @return A filtered view of {@code input} containing only those entries
+     *         which match the provided search request.
+     */
+    public static EntryReader search(final EntryReader input, final SearchRequest search) {
+        return search(input, search, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns a filtered view of {@code input} containing only those entries
+     * which match the search base DN, scope, and filtered defined in
+     * {@code search}. In addition, returned entries will be filtered according
+     * to any attribute filtering criteria defined in the search request.
+     * <p>
+     * The filter and attribute descriptions will be decoded using the provided
+     * schema.
+     *
+     * @param input
+     *            The entry reader containing the set of entries to be filtered.
+     * @param search
+     *            The search request defining the filtering criteria.
+     * @param schema
+     *            The schema which should be used to decode the search filter
+     *            and attribute descriptions.
+     * @return A filtered view of {@code input} containing only those entries
+     *         which match the provided search request.
+     */
+    public static EntryReader search(final EntryReader input, final SearchRequest search,
+            final Schema schema) {
+        final Matcher matcher = search.getFilter().matcher(schema);
+
+        return new EntryReader() {
+            private Entry nextEntry = null;
+            private int entryCount = 0;
+
+            @Override
+            public void close() throws IOException {
+                input.close();
+            }
+
+            @Override
+            public boolean hasNext() throws IOException {
+                if (nextEntry == null) {
+                    final int sizeLimit = search.getSizeLimit();
+                    if (sizeLimit == 0 || entryCount < sizeLimit) {
+                        final DN baseDN = search.getName();
+                        final SearchScope scope = search.getScope();
+                        while (input.hasNext()) {
+                            final Entry entry = input.readEntry();
+                            if (entry.getName().isInScopeOf(baseDN, scope)
+                                    && matcher.matches(entry).toBoolean()) {
+                                nextEntry = filterEntry(entry);
+                                break;
+                            }
+                        }
+                    }
+                }
+                return nextEntry != null;
+            }
+
+            @Override
+            public Entry readEntry() throws IOException {
+                if (hasNext()) {
+                    final Entry entry = nextEntry;
+                    nextEntry = null;
+                    entryCount++;
+                    return entry;
+                } else {
+                    throw new NoSuchElementException();
+                }
+            }
+
+            private Entry filterEntry(final Entry entry) {
+                // TODO: rename attributes; move functionality to Entries.
+                if (search.getAttributes().isEmpty()) {
+                    if (search.isTypesOnly()) {
+                        final Entry filteredEntry = new LinkedHashMapEntry(entry.getName());
+                        for (final Attribute attribute : entry.getAllAttributes()) {
+                            filteredEntry.addAttribute(Attributes.emptyAttribute(attribute
+                                    .getAttributeDescription()));
+                        }
+                        return filteredEntry;
+                    } else {
+                        return entry;
+                    }
+                } else {
+                    final Entry filteredEntry = new LinkedHashMapEntry(entry.getName());
+                    for (final String atd : search.getAttributes()) {
+                        if ("*".equals(atd)) {
+                            for (final Attribute attribute : entry.getAllAttributes()) {
+                                if (attribute.getAttributeDescription().getAttributeType()
+                                        .getUsage() == AttributeUsage.USER_APPLICATIONS) {
+                                    if (search.isTypesOnly()) {
+                                        filteredEntry
+                                                .addAttribute(Attributes.emptyAttribute(attribute
+                                                        .getAttributeDescription()));
+                                    } else {
+                                        filteredEntry.addAttribute(attribute);
+                                    }
+                                }
+                            }
+                        } else if ("+".equals(atd)) {
+                            for (final Attribute attribute : entry.getAllAttributes()) {
+                                if (attribute.getAttributeDescription().getAttributeType()
+                                        .getUsage() != AttributeUsage.USER_APPLICATIONS) {
+                                    if (search.isTypesOnly()) {
+                                        filteredEntry
+                                                .addAttribute(Attributes.emptyAttribute(attribute
+                                                        .getAttributeDescription()));
+                                    } else {
+                                        filteredEntry.addAttribute(attribute);
+                                    }
+                                }
+                            }
+                        } else {
+                            final AttributeDescription ad =
+                                    AttributeDescription.valueOf(atd, schema);
+                            for (final Attribute attribute : entry.getAllAttributes(ad)) {
+                                if (search.isTypesOnly()) {
+                                    filteredEntry.addAttribute(Attributes.emptyAttribute(attribute
+                                            .getAttributeDescription()));
+                                } else {
+                                    filteredEntry.addAttribute(attribute);
+                                }
+                            }
+                        }
+                    }
+                    return filteredEntry;
+                }
+            }
+
+        };
+    }
+
+    private static List<byte[][]> readEntriesAsList(final EntryReader reader) throws IOException {
+        final List<byte[][]> entries = new ArrayList<>();
+
+        while (reader.hasNext()) {
+            final Entry entry = reader.readEntry();
+            entries.add(encodeEntry(entry));
+        }
+        // Sorting the list by DN
+        Collections.sort(entries, DN_ORDER2);
+
+        return entries;
+    }
+
+    private static TreeMap<byte[], byte[]> readEntriesAsMap(final EntryReader reader)
+            throws IOException {
+        final TreeMap<byte[], byte[]> entries = new TreeMap<>(DN_ORDER);
+
+        while (reader.hasNext()) {
+            final Entry entry = reader.readEntry();
+            final byte[][] bEntry = encodeEntry(entry);
+            entries.put(bEntry[0], bEntry[1]);
+        }
+
+        return entries;
+    }
+
+    private static Entry decodeEntry(final byte[] asn1EntryFormat) {
+        try {
+            return LDAP.readEntry(ASN1.getReader(asn1EntryFormat), new DecodeOptions());
+        } catch (IOException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static byte[] toNormalizedByteArray(DN dn) {
+        return dn.toNormalizedByteString().toByteArray();
+    }
+
+    private static byte[][] encodeEntry(final Entry entry) {
+        final byte[][] bEntry = new byte[2][];
+        // Store normalized DN
+        bEntry[0] = toNormalizedByteArray(entry.getName());
+        try {
+            // Store ASN1 representation of the entry.
+            final ByteStringBuilder bsb = new ByteStringBuilder();
+            LDAP.writeEntry(ASN1.getWriter(bsb), entry);
+            bEntry[1] = bsb.toByteArray();
+            return bEntry;
+        } catch (final IOException ioe) {
+            throw new IllegalStateException(ioe);
+        }
+    }
+
+    /** Prevent instantiation. */
+    private LDIF() {
+        // Do nothing.
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
new file mode 100644
index 0000000..0ba1cb1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
@@ -0,0 +1,740 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldap.schema.Syntax;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
+import org.forgerock.util.Reject;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+/**
+ * An LDIF change record reader reads change records using the LDAP Data
+ * Interchange Format (LDIF) from a user defined source.
+ * <p>
+ * The following example reads changes from LDIF, and writes the changes to the
+ * directory server.
+ *
+ * <pre>
+ * InputStream ldif = ...;
+ * LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldif);
+ *
+ * Connection connection = ...;
+ * connection.bind(...);
+ *
+ * ConnectionChangeRecordWriter writer =
+ *         new ConnectionChangeRecordWriter(connection);
+ * while (reader.hasNext()) {
+ *     ChangeRecord changeRecord = reader.readChangeRecord();
+ *     writer.writeChangeRecord(changeRecord);
+ * }
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data
+ *      Interchange Format (LDIF) - Technical Specification </a>
+ */
+public final class LDIFChangeRecordReader extends AbstractLDIFReader implements ChangeRecordReader {
+    private static final Pattern CONTROL_REGEX = Pattern
+            .compile("^\\s*(\\d+(.\\d+)*)(\\s+((true)|(false)))?\\s*(:(:)?\\s*?\\S+)?\\s*$");
+
+    /** Poison used to indicate end of LDIF. */
+    private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
+
+    /**
+     * Parses the provided array of LDIF lines as a single LDIF change record.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be parsed.
+     * @return The parsed LDIF change record.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain an LDIF change record,
+     *             if it contained multiple change records, if contained
+     *             malformed LDIF, or if the change record could not be decoded
+     *             using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static ChangeRecord valueOfLDIFChangeRecord(final String... ldifLines) {
+        // LDIF change record reader is tolerant to missing change types.
+        try (final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldifLines)) {
+            if (!reader.hasNext()) {
+                // No change record found.
+                final LocalizableMessage message =
+                        WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND.get();
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            final ChangeRecord record = reader.readChangeRecord();
+
+            if (reader.hasNext()) {
+                // Multiple change records found.
+                final LocalizableMessage message =
+                        WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND.get();
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            return record;
+        } catch (final DecodeException e) {
+            // Badly formed LDIF.
+            throw new LocalizedIllegalArgumentException(e.getMessageObject());
+        } catch (final IOException e) {
+            // This should never happen for a String based reader.
+            final LocalizableMessage message =
+                    WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    private ChangeRecord nextChangeRecord;
+
+    /**
+     * Creates a new LDIF change record reader whose source is the provided
+     * input stream.
+     *
+     * @param in
+     *            The input stream to use.
+     * @throws NullPointerException
+     *             If {@code in} was {@code null}.
+     */
+    public LDIFChangeRecordReader(final InputStream in) {
+        super(in);
+    }
+
+    /**
+     * Creates a new LDIF change record reader which will read lines of LDIF
+     * from the provided list of LDIF lines.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be read.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public LDIFChangeRecordReader(final List<String> ldifLines) {
+        super(ldifLines);
+    }
+
+    /**
+     * Creates a new LDIF change record reader whose source is the provided
+     * character stream reader.
+     *
+     * @param reader
+     *            The character stream reader to use.
+     * @throws NullPointerException
+     *             If {@code reader} was {@code null}.
+     */
+    public LDIFChangeRecordReader(final Reader reader) {
+        super(reader);
+    }
+
+    /**
+     * Creates a new LDIF change record reader which will read lines of LDIF
+     * from the provided array of LDIF lines.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be read.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public LDIFChangeRecordReader(final String... ldifLines) {
+        super(Arrays.asList(ldifLines));
+    }
+
+    @Override
+    public void close() throws IOException {
+        close0();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws DecodeException
+     *             If the change record could not be decoded because it was
+     *             malformed.
+     */
+    @Override
+    public boolean hasNext() throws DecodeException, IOException {
+        return getNextChangeRecord() != EOF;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws DecodeException
+     *             If the entry could not be decoded because it was malformed.
+     */
+    @Override
+    public ChangeRecord readChangeRecord() throws DecodeException, IOException {
+        if (!hasNext()) {
+            // LDIF reader has completed successfully.
+            throw new NoSuchElementException();
+        }
+
+        final ChangeRecord changeRecord = nextChangeRecord;
+        nextChangeRecord = null;
+        return changeRecord;
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be excluded
+     * from any change records that are read from LDIF. The default is
+     * {@code false}.
+     *
+     * @param excludeOperationalAttributes
+     *            {@code true} if all operational attributes should be excluded,
+     *            or {@code false} otherwise.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setExcludeAllOperationalAttributes(
+            final boolean excludeOperationalAttributes) {
+        this.excludeOperationalAttributes = excludeOperationalAttributes;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be excluded from any
+     * change records that are read from LDIF. The default is {@code false}.
+     *
+     * @param excludeUserAttributes
+     *            {@code true} if all user attributes should be excluded, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setExcludeAllUserAttributes(final boolean excludeUserAttributes) {
+        this.excludeUserAttributes = excludeUserAttributes;
+        return this;
+    }
+
+    /**
+     * Excludes the named attribute from any change records that are read from
+     * LDIF. By default all attributes are included unless explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be excluded.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setExcludeAttribute(
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        excludeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Excludes all change records which target entries beneath the named entry
+     * (inclusive) from being read from LDIF. By default all change records are
+     * read unless explicitly excluded or included.
+     *
+     * @param excludeBranch
+     *            The distinguished name of the branch to be excluded.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setExcludeBranch(final DN excludeBranch) {
+        Reject.ifNull(excludeBranch);
+        excludeBranches.add(excludeBranch);
+        return this;
+    }
+
+    /**
+     * Ensures that the named attribute is not excluded from any change records
+     * that are read from LDIF. By default all attributes are included unless
+     * explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setIncludeAttribute(
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        includeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Ensures that all change records which target entries beneath the named
+     * entry (inclusive) are read from LDIF. By default all change records are
+     * read unless explicitly excluded or included.
+     *
+     * @param includeBranch
+     *            The distinguished name of the branch to be included.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setIncludeBranch(final DN includeBranch) {
+        Reject.ifNull(includeBranch);
+        includeBranches.add(includeBranch);
+        return this;
+    }
+
+    /**
+     * Sets the rejected record listener which should be notified whenever an
+     * LDIF record is skipped, malformed, or fails schema validation.
+     * <p>
+     * By default the {@link RejectedLDIFListener#FAIL_FAST} listener is used.
+     *
+     * @param listener
+     *            The rejected record listener.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setRejectedLDIFListener(final RejectedLDIFListener listener) {
+        this.rejectedRecordListener = listener;
+        return this;
+    }
+
+    /**
+     * Sets the schema which should be used for decoding change records that are
+     * read from LDIF. The default schema is used if no other is specified.
+     *
+     * @param schema
+     *            The schema which should be used for decoding change records
+     *            that are read from LDIF.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setSchema(final Schema schema) {
+        Reject.ifNull(schema);
+        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
+        return this;
+    }
+
+    /**
+     * Specifies the schema validation which should be used when reading LDIF
+     * change records. If attribute value validation is enabled then all checks
+     * will be performed.
+     * <p>
+     * Schema validation is disabled by default.
+     * <p>
+     * <b>NOTE:</b> this method copies the provided policy so changes made to it
+     * after this method has been called will have no effect.
+     *
+     * @param policy
+     *            The schema validation which should be used when reading LDIF
+     *            change records.
+     * @return A reference to this {@code LDIFChangeRecordReader}.
+     */
+    public LDIFChangeRecordReader setSchemaValidationPolicy(final SchemaValidationPolicy policy) {
+        this.schemaValidationPolicy = SchemaValidationPolicy.copyOf(policy);
+        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
+        return this;
+    }
+
+    private ChangeRecord getNextChangeRecord() throws DecodeException, IOException {
+        while (nextChangeRecord == null) {
+            // Read the set of lines that make up the next entry.
+            final LDIFRecord record = readLDIFRecord();
+            if (record == null) {
+                nextChangeRecord = EOF;
+                break;
+            }
+
+            try {
+                /* Read the DN of the entry and see if it is one that should be included in the import. */
+                final DN entryDN = readLDIFRecordDN(record);
+                if (entryDN == null) {
+                    // Skip version record.
+                    continue;
+                }
+
+                // Skip if branch containing the entry DN is excluded.
+                if (isBranchExcluded(entryDN)) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_CHANGE_EXCLUDED_BY_DN.get(record.lineNumber, entryDN);
+                    handleSkippedRecord(record, message);
+                    continue;
+                }
+
+                KeyValuePair pair;
+                String ldifLine;
+                List<Control> controls = null;
+                while (true) {
+                    if (!record.iterator.hasNext()) {
+                        throw DecodeException.error(
+                                ERR_LDIF_NO_CHANGE_TYPE.get(record.lineNumber, entryDN));
+                    }
+
+                    pair = new KeyValuePair();
+                    ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
+                    if (pair.key == null) {
+                        throw DecodeException.error(
+                                ERR_LDIF_MALFORMED_CHANGE_TYPE.get(record.lineNumber, entryDN, ldifLine));
+                    }
+
+                    if (!"control".equals(toLowerCase(pair.key))) {
+                        break;
+                    }
+
+                    if (controls == null) {
+                        controls = new LinkedList<>();
+                    }
+
+                    controls.add(parseControl(entryDN, record, ldifLine, pair.value));
+                }
+
+                if (!"changetype".equals(toLowerCase(pair.key))) {
+                    // Default to add change record.
+                    nextChangeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
+                } else {
+                    final String changeType = toLowerCase(pair.value);
+                    if ("add".equals(changeType)) {
+                        nextChangeRecord = parseAddChangeRecordEntry(entryDN, null, record);
+                    } else if ("delete".equals(changeType)) {
+                        nextChangeRecord = parseDeleteChangeRecordEntry(entryDN, record);
+                    } else if ("modify".equals(changeType)) {
+                        nextChangeRecord = parseModifyChangeRecordEntry(entryDN, record);
+                    } else if ("modrdn".equals(changeType)) {
+                        nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+                    } else if ("moddn".equals(changeType)) {
+                        nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+                    } else {
+                        throw DecodeException.error(
+                                ERR_LDIF_BAD_CHANGE_TYPE.get(record.lineNumber, entryDN, pair.value));
+                    }
+
+                    // Add the controls to the record.
+                    if (controls != null) {
+                        for (final Control control : controls) {
+                            nextChangeRecord.addControl(control);
+                        }
+                    }
+                }
+            } catch (final DecodeException e) {
+                handleMalformedRecord(record, e.getMessageObject());
+                continue;
+            }
+        }
+        return nextChangeRecord;
+    }
+
+    private ChangeRecord parseAddChangeRecordEntry(final DN entryDN, final String lastLDIFLine,
+            final LDIFRecord record) throws DecodeException {
+        // Use an Entry for the AttributeSequence.
+        final Entry entry = new LinkedHashMapEntry(entryDN);
+        boolean schemaValidationFailure = false;
+        final List<LocalizableMessage> schemaErrors = new LinkedList<>();
+
+        if (lastLDIFLine != null
+                // This line was read when looking for the change type.
+                && !readLDIFRecordAttributeValue(record, lastLDIFLine, entry, schemaErrors)) {
+            schemaValidationFailure = true;
+        }
+
+        while (record.iterator.hasNext()) {
+            final String ldifLine = record.iterator.next();
+            if (!readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors)) {
+                schemaValidationFailure = true;
+            }
+        }
+
+        if (!schema.validateEntry(entry, schemaValidationPolicy, schemaErrors)) {
+            schemaValidationFailure = true;
+        }
+
+        if (schemaValidationFailure) {
+            handleSchemaValidationFailure(record, schemaErrors);
+            return null;
+        }
+
+        if (!schemaErrors.isEmpty()) {
+            handleSchemaValidationWarning(record, schemaErrors);
+        }
+        return Requests.newAddRequest(entry);
+    }
+
+    private Control parseControl(final DN entryDN, final LDIFRecord record, final String ldifLine,
+            final String value) throws DecodeException {
+        final Matcher matcher = CONTROL_REGEX.matcher(value);
+        if (!matcher.matches()) {
+            throw DecodeException.error(ERR_LDIF_MALFORMED_CONTROL.get(record.lineNumber, entryDN, ldifLine));
+        }
+        final String oid = matcher.group(1);
+        final boolean isCritical = matcher.group(5) != null;
+        final String controlValueString = matcher.group(7);
+        ByteString controlValue = null;
+        if (controlValueString != null) {
+            controlValue =
+                    parseSingleValue(record, ldifLine, entryDN, ldifLine.indexOf(':', 8), oid);
+        }
+        return GenericControl.newControl(oid, isCritical, controlValue);
+    }
+
+    private ChangeRecord parseDeleteChangeRecordEntry(final DN entryDN, final LDIFRecord record)
+            throws DecodeException {
+        if (record.iterator.hasNext()) {
+            throw DecodeException.error(ERR_LDIF_MALFORMED_DELETE.get(record.lineNumber, entryDN));
+        }
+        return Requests.newDeleteRequest(entryDN);
+    }
+
+    private ChangeRecord parseModifyChangeRecordEntry(final DN entryDN, final LDIFRecord record)
+            throws DecodeException {
+        final ModifyRequest modifyRequest = Requests.newModifyRequest(entryDN);
+        final KeyValuePair pair = new KeyValuePair();
+        final List<ByteString> attributeValues = new ArrayList<>();
+        boolean schemaValidationFailure = false;
+        final List<LocalizableMessage> schemaErrors = new LinkedList<>();
+
+        while (record.iterator.hasNext()) {
+            String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
+            if (pair.key == null) {
+                throw DecodeException.error(
+                        ERR_LDIF_MALFORMED_MODIFICATION_TYPE.get(record.lineNumber, entryDN, ldifLine));
+            }
+
+            final String changeType = toLowerCase(pair.key);
+
+            ModificationType modType;
+            if ("add".equals(changeType)) {
+                modType = ModificationType.ADD;
+            } else if ("delete".equals(changeType)) {
+                modType = ModificationType.DELETE;
+            } else if ("replace".equals(changeType)) {
+                modType = ModificationType.REPLACE;
+            } else if ("increment".equals(changeType)) {
+                modType = ModificationType.INCREMENT;
+            } else {
+                throw DecodeException.error(
+                        ERR_LDIF_BAD_MODIFICATION_TYPE.get(record.lineNumber, entryDN, pair.key));
+            }
+
+            AttributeDescription attributeDescription;
+            try {
+                attributeDescription = AttributeDescription.valueOf(pair.value, schema);
+            } catch (final UnknownSchemaElementException e) {
+                final LocalizableMessage message =
+                        ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE.get(record.lineNumber, entryDN, pair.value);
+                switch (schemaValidationPolicy.checkAttributesAndObjectClasses()) {
+                case REJECT:
+                    schemaValidationFailure = true;
+                    schemaErrors.add(message);
+                    continue;
+                case WARN:
+                    schemaErrors.add(message);
+                    continue;
+                default: // Ignore
+                    /* This should not happen: we should be using a non-strict schema for this policy. */
+                    throw new IllegalStateException("Schema is not consistent with policy", e);
+                }
+            } catch (final LocalizedIllegalArgumentException e) {
+                throw DecodeException.error(
+                        ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(record.lineNumber, entryDN, pair.value));
+            }
+
+            /*
+             * Skip the attribute if requested before performing any schema
+             * checking: the attribute may have been excluded because it is
+             * known to violate the schema.
+             */
+            if (isAttributeExcluded(attributeDescription)) {
+                continue;
+            }
+
+            final Syntax syntax = attributeDescription.getAttributeType().getSyntax();
+
+            // Ensure that the binary option is present if required.
+            if (!syntax.isBEREncodingRequired()) {
+                if (schemaValidationPolicy.checkAttributeValues().needsChecking()
+                        && attributeDescription.hasOption("binary")) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_UNEXPECTED_BINARY_OPTION.get(record.lineNumber, entryDN, pair.value);
+                    if (schemaValidationPolicy.checkAttributeValues().isReject()) {
+                        schemaValidationFailure = true;
+                    }
+                    schemaErrors.add(message);
+                    continue;
+                }
+            } else {
+                attributeDescription = attributeDescription.withOption("binary");
+            }
+
+            /* Now go through the rest of the attributes until the "-" line is reached. */
+            attributeValues.clear();
+            while (record.iterator.hasNext()) {
+                ldifLine = record.iterator.next();
+                if ("-".equals(ldifLine)) {
+                    break;
+                }
+
+                // Parse the attribute description.
+                final int colonPos = parseColonPosition(record, ldifLine);
+                final String attrDescr = ldifLine.substring(0, colonPos);
+
+                AttributeDescription attributeDescription2;
+                try {
+                    attributeDescription2 = AttributeDescription.valueOf(attrDescr, schema);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    /*
+                     * No need to catch schema exception here because it implies
+                     * that the attribute name is wrong and the record is
+                     * malformed.
+                     */
+                    throw DecodeException.error(
+                            ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(record.lineNumber, entryDN, attrDescr));
+                }
+
+                // Ensure that the binary option is present if required.
+                if (attributeDescription.getAttributeType().getSyntax().isBEREncodingRequired()) {
+                    attributeDescription2 = attributeDescription2.withOption("binary");
+                }
+
+                if (!attributeDescription2.equals(attributeDescription)) {
+                    // Malformed record.
+                    throw DecodeException.error(ERR_LDIF_ATTRIBUTE_NAME_MISMATCH.get(
+                            record.lineNumber, entryDN, attributeDescription2, attributeDescription));
+                }
+
+                // Parse the attribute value and check it if needed.
+                final ByteString value =
+                        parseSingleValue(record, ldifLine, entryDN, colonPos, attrDescr);
+                if (schemaValidationPolicy.checkAttributeValues().needsChecking()) {
+                    final LocalizableMessageBuilder builder = new LocalizableMessageBuilder();
+                    if (!syntax.valueIsAcceptable(value, builder)) {
+                        /*
+                         * Just log a message, but don't skip the value since
+                         * this could change the semantics of the modification
+                         * (e.g. if all values in a delete are skipped then this
+                         * implies that the whole attribute should be removed).
+                         */
+                        if (schemaValidationPolicy.checkAttributeValues().isReject()) {
+                            schemaValidationFailure = true;
+                        }
+                        schemaErrors.add(builder.toMessage());
+                    }
+                }
+                attributeValues.add(value);
+            }
+
+            final Modification change =
+                    new Modification(modType, new LinkedAttribute(attributeDescription,
+                            attributeValues));
+            modifyRequest.addModification(change);
+        }
+
+        if (schemaValidationFailure) {
+            handleSchemaValidationFailure(record, schemaErrors);
+            return null;
+        }
+
+        if (!schemaErrors.isEmpty()) {
+            handleSchemaValidationWarning(record, schemaErrors);
+        }
+
+        return modifyRequest;
+    }
+
+    private ChangeRecord parseModifyDNChangeRecordEntry(final DN entryDN, final LDIFRecord record)
+            throws DecodeException {
+        // Parse the newrdn.
+        if (!record.iterator.hasNext()) {
+            throw DecodeException.error(ERR_LDIF_NO_NEW_RDN.get(record.lineNumber, entryDN));
+        }
+
+        final KeyValuePair pair = new KeyValuePair();
+        String ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+
+        if (pair.key == null || !"newrdn".equals(toLowerCase(pair.key))) {
+            throw DecodeException.error(
+                    ERR_LDIF_MALFORMED_NEW_RDN.get(record.lineNumber, entryDN, ldifLine));
+        }
+
+        final ModifyDNRequest modifyDNRequest;
+        try {
+            final RDN newRDN = RDN.valueOf(pair.value, schema);
+            modifyDNRequest = Requests.newModifyDNRequest(entryDN, newRDN);
+        } catch (final LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(
+                    ERR_LDIF_MALFORMED_NEW_RDN.get(record.lineNumber, entryDN, pair.value));
+        }
+
+        // Parse the deleteoldrdn.
+        if (!record.iterator.hasNext()) {
+            final LocalizableMessage message =
+                    ERR_LDIF_NO_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString());
+            throw DecodeException.error(message);
+        }
+
+        ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+        if (pair.key == null || !"deleteoldrdn".equals(toLowerCase(pair.key))) {
+            final LocalizableMessage message =
+                    ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString(),
+                            ldifLine);
+            throw DecodeException.error(message);
+        }
+
+        final String delStr = toLowerCase(pair.value);
+        if ("false".equals(delStr) || "no".equals(delStr) || "0".equals(delStr)) {
+            modifyDNRequest.setDeleteOldRDN(false);
+        } else if ("true".equals(delStr) || "yes".equals(delStr) || "1".equals(delStr)) {
+            modifyDNRequest.setDeleteOldRDN(true);
+        } else {
+            final LocalizableMessage message =
+                    ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString(),
+                            pair.value);
+            throw DecodeException.error(message);
+        }
+
+        // Parse the newsuperior if present.
+        if (record.iterator.hasNext()) {
+            ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+            if (pair.key == null || !"newsuperior".equals(toLowerCase(pair.key))) {
+                throw DecodeException.error(
+                        ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(record.lineNumber, entryDN, ldifLine));
+            }
+
+            try {
+                final DN newSuperiorDN = DN.valueOf(pair.value, schema);
+                modifyDNRequest.setNewSuperior(newSuperiorDN.toString());
+            } catch (final LocalizedIllegalArgumentException e) {
+                final LocalizableMessage message =
+                        ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(record.lineNumber, entryDN.toString(),
+                                pair.value);
+                throw DecodeException.error(message);
+            }
+        }
+
+        return modifyDNRequest;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriter.java
new file mode 100644
index 0000000..33d0431
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriter.java
@@ -0,0 +1,392 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An LDIF change record writer writes change records using the LDAP Data
+ * Interchange Format (LDIF) to a user defined destination.
+ * <p>
+ * The following example reads changes from LDIF, and writes the changes to the
+ * directory server.
+ *
+ * <pre>
+ * InputStream ldif = ...;
+ * LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldif);
+ *
+ * Connection connection = ...;
+ * connection.bind(...);
+ *
+ * ConnectionChangeRecordWriter writer =
+ *         new ConnectionChangeRecordWriter(connection);
+ * while (reader.hasNext()) {
+ *     ChangeRecord changeRecord = reader.readChangeRecord();
+ *     writer.writeChangeRecord(changeRecord);
+ * }
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data
+ *      Interchange Format (LDIF) - Technical Specification </a>
+ */
+public final class LDIFChangeRecordWriter extends AbstractLDIFWriter implements ChangeRecordWriter {
+    /**
+     * Returns the LDIF string representation of the provided change record.
+     *
+     * @param change
+     *            The change record.
+     * @return The LDIF string representation of the provided change record.
+     */
+    public static String toString(final ChangeRecord change) {
+        final StringWriter writer = new StringWriter(128);
+        try (LDIFChangeRecordWriter ldifWriter = new LDIFChangeRecordWriter(writer)) {
+            ldifWriter.setAddUserFriendlyComments(true).writeChangeRecord(change);
+        } catch (final IOException e) {
+            // Should never happen.
+            throw new IllegalStateException(e);
+        }
+        return writer.toString();
+    }
+
+    /**
+     * Creates a new LDIF change record writer which will append lines of LDIF
+     * to the provided list.
+     *
+     * @param ldifLines
+     *            The list to which lines of LDIF should be appended.
+     */
+    public LDIFChangeRecordWriter(final List<String> ldifLines) {
+        super(ldifLines);
+    }
+
+    /**
+     * Creates a new LDIF change record writer whose destination is the provided
+     * output stream.
+     *
+     * @param out
+     *            The output stream to use.
+     */
+    public LDIFChangeRecordWriter(final OutputStream out) {
+        super(out);
+    }
+
+    /**
+     * Creates a new LDIF change record writer whose destination is the provided
+     * character stream writer.
+     *
+     * @param writer
+     *            The character stream writer to use.
+     */
+    public LDIFChangeRecordWriter(final Writer writer) {
+        super(writer);
+    }
+
+    @Override
+    public void close() throws IOException {
+        close0();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        flush0();
+    }
+
+    /**
+     * Specifies whether or not user-friendly comments should be added whenever
+     * distinguished names or UTF-8 attribute values are encountered which
+     * contained non-ASCII characters. The default is {@code false}.
+     *
+     * @param addUserFriendlyComments
+     *            {@code true} if user-friendly comments should be added, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFChangeRecordWriter setAddUserFriendlyComments(final boolean addUserFriendlyComments) {
+        this.addUserFriendlyComments = addUserFriendlyComments;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be excluded
+     * from any change records that are written to LDIF. The default is
+     * {@code false}.
+     *
+     * @param excludeOperationalAttributes
+     *            {@code true} if all operational attributes should be excluded,
+     *            or {@code false} otherwise.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setExcludeAllOperationalAttributes(
+            final boolean excludeOperationalAttributes) {
+        this.excludeOperationalAttributes = excludeOperationalAttributes;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be excluded from any
+     * change records that are written to LDIF. The default is {@code false}.
+     *
+     * @param excludeUserAttributes
+     *            {@code true} if all user attributes should be excluded, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setExcludeAllUserAttributes(final boolean excludeUserAttributes) {
+        this.excludeUserAttributes = excludeUserAttributes;
+        return this;
+    }
+
+    /**
+     * Excludes the named attribute from any change records that are written to
+     * LDIF. By default all attributes are included unless explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be excluded.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setExcludeAttribute(
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        excludeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Excludes all change records which target entries beneath the named entry
+     * (inclusive) from being written to LDIF. By default all change records are
+     * written unless explicitly excluded or included.
+     *
+     * @param excludeBranch
+     *            The distinguished name of the branch to be excluded.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setExcludeBranch(final DN excludeBranch) {
+        Reject.ifNull(excludeBranch);
+        excludeBranches.add(excludeBranch);
+        return this;
+    }
+
+    /**
+     * Ensures that the named attribute is not excluded from any change records
+     * that are written to LDIF. By default all attributes are included unless
+     * explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setIncludeAttribute(
+            final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        includeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Ensures that all change records which target entries beneath the named
+     * entry (inclusive) are written to LDIF. By default all change records are
+     * written unless explicitly excluded or included.
+     *
+     * @param includeBranch
+     *            The distinguished name of the branch to be included.
+     * @return A reference to this {@code LDIFChangeRecordWriter}.
+     */
+    public LDIFChangeRecordWriter setIncludeBranch(final DN includeBranch) {
+        Reject.ifNull(includeBranch);
+        includeBranches.add(includeBranch);
+        return this;
+    }
+
+    /**
+     * Specifies the column at which long lines should be wrapped. A value less
+     * than or equal to zero (the default) indicates that no wrapping should be
+     * performed.
+     *
+     * @param wrapColumn
+     *            The column at which long lines should be wrapped.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFChangeRecordWriter setWrapColumn(final int wrapColumn) {
+        this.wrapColumn = wrapColumn;
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeChangeRecord(final AddRequest change) throws IOException {
+        Reject.ifNull(change);
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(change.getName())) {
+            return this;
+        }
+
+        writeKeyAndValue("dn", change.getName().toString());
+        writeControls(change.getControls());
+        writeLine("changetype: add");
+        for (final Attribute attribute : change.getAllAttributes()) {
+            // Filter the attribute if required.
+            if (isAttributeExcluded(attribute.getAttributeDescription())) {
+                continue;
+            }
+
+            final String attributeDescription = attribute.getAttributeDescriptionAsString();
+            for (final ByteString value : attribute) {
+                writeKeyAndValue(attributeDescription, value);
+            }
+        }
+
+        // Make sure there is a blank line after the entry.
+        impl.println();
+
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeChangeRecord(final ChangeRecord change) throws IOException {
+        Reject.ifNull(change);
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(change.getName())) {
+            return this;
+        }
+
+        final IOException e = change.accept(ChangeRecordVisitorWriter.getInstance(), this);
+        if (e != null) {
+            throw e;
+        }
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeChangeRecord(final DeleteRequest change) throws IOException {
+        Reject.ifNull(change);
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(change.getName())) {
+            return this;
+        }
+
+        writeKeyAndValue("dn", change.getName().toString());
+        writeControls(change.getControls());
+        writeLine("changetype: delete");
+
+        // Make sure there is a blank line after the entry.
+        impl.println();
+
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeChangeRecord(final ModifyDNRequest change)
+            throws IOException {
+        Reject.ifNull(change);
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(change.getName())) {
+            return this;
+        }
+
+        writeKeyAndValue("dn", change.getName().toString());
+        writeControls(change.getControls());
+
+        /*
+         * Write the changetype. Some older tools may not support the "moddn"
+         * changetype, so only use it if a newSuperior element has been
+         * provided, but use modrdn elsewhere.
+         */
+        if (change.getNewSuperior() != null) {
+            writeLine("changetype: moddn");
+        } else {
+            writeLine("changetype: modrdn");
+        }
+
+        writeKeyAndValue("newrdn", change.getNewRDN().toString());
+        writeKeyAndValue("deleteoldrdn", change.isDeleteOldRDN() ? "1" : "0");
+        if (change.getNewSuperior() != null) {
+            writeKeyAndValue("newsuperior", change.getNewSuperior().toString());
+        }
+
+        // Make sure there is a blank line after the entry.
+        impl.println();
+
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeChangeRecord(final ModifyRequest change) throws IOException {
+        Reject.ifNull(change);
+
+        // If there aren't any modifications, then there's nothing to do.
+        if (change.getModifications().isEmpty()) {
+            return this;
+        }
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(change.getName())) {
+            return this;
+        }
+
+        writeKeyAndValue("dn", change.getName().toString());
+        writeControls(change.getControls());
+        writeLine("changetype: modify");
+
+        for (final Modification modification : change.getModifications()) {
+            final ModificationType type = modification.getModificationType();
+            final Attribute attribute = modification.getAttribute();
+            final String attributeDescription = attribute.getAttributeDescriptionAsString();
+
+            // Filter the attribute if required.
+            if (isAttributeExcluded(attribute.getAttributeDescription())) {
+                continue;
+            }
+
+            writeKeyAndValue(type.toString(), attributeDescription);
+            for (final ByteString value : attribute) {
+                writeKeyAndValue(attributeDescription, value);
+            }
+            writeLine("-");
+        }
+
+        // Make sure there is a blank line after the entry.
+        impl.println();
+
+        return this;
+    }
+
+    @Override
+    public LDIFChangeRecordWriter writeComment(final CharSequence comment) throws IOException {
+        writeComment0(comment);
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java
new file mode 100644
index 0000000..5834c27
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java
@@ -0,0 +1,420 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Matcher;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.util.Reject;
+
+/**
+ * An LDIF entry reader reads attribute value records (entries) using the LDAP
+ * Data Interchange Format (LDIF) from a user defined source.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data
+ *      Interchange Format (LDIF) - Technical Specification </a>
+ */
+public final class LDIFEntryReader extends AbstractLDIFReader implements EntryReader {
+    /** Poison used to indicate end of LDIF. */
+    private static final Entry EOF = new LinkedHashMapEntry();
+
+    /**
+     * Parses the provided array of LDIF lines as a single LDIF entry.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be parsed.
+     * @return The parsed LDIF entry.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code ldifLines} did not contain an LDIF entry, if it
+     *             contained multiple entries, if contained malformed LDIF, or
+     *             if the entry could not be decoded using the default schema.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public static Entry valueOfLDIFEntry(final String... ldifLines) {
+        try (final LDIFEntryReader reader = new LDIFEntryReader(ldifLines)) {
+            if (!reader.hasNext()) {
+                // No change record found.
+                final LocalizableMessage message =
+                        WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND.get();
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            final Entry entry = reader.readEntry();
+
+            if (reader.hasNext()) {
+                // Multiple change records found.
+                final LocalizableMessage message =
+                        WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND.get();
+                throw new LocalizedIllegalArgumentException(message);
+            }
+
+            return entry;
+        } catch (final DecodeException e) {
+            // Badly formed LDIF.
+            throw new LocalizedIllegalArgumentException(e.getMessageObject());
+        } catch (final IOException e) {
+            // This should never happen for a String based reader.
+            final LocalizableMessage message =
+                    WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage());
+            throw new LocalizedIllegalArgumentException(message);
+        }
+    }
+
+    private Entry nextEntry;
+
+    /**
+     * Creates a new LDIF entry reader whose source is the provided input
+     * stream.
+     *
+     * @param in
+     *            The input stream to use.
+     * @throws NullPointerException
+     *             If {@code in} was {@code null}.
+     */
+    public LDIFEntryReader(final InputStream in) {
+        super(in);
+    }
+
+    /**
+     * Creates a new LDIF entry reader which will read lines of LDIF from the
+     * provided list of LDIF lines.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be read.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public LDIFEntryReader(final List<String> ldifLines) {
+        super(ldifLines);
+    }
+
+    /**
+     * Creates a new LDIF entry reader whose source is the provided character
+     * stream reader.
+     *
+     * @param reader
+     *            The character stream reader to use.
+     * @throws NullPointerException
+     *             If {@code reader} was {@code null}.
+     */
+    public LDIFEntryReader(final Reader reader) {
+        super(reader);
+    }
+
+    /**
+     * Creates a new LDIF entry reader which will read lines of LDIF from the
+     * provided array of LDIF lines.
+     *
+     * @param ldifLines
+     *            The lines of LDIF to be read.
+     * @throws NullPointerException
+     *             If {@code ldifLines} was {@code null}.
+     */
+    public LDIFEntryReader(final String... ldifLines) {
+        super(Arrays.asList(ldifLines));
+    }
+
+    @Override
+    public void close() throws IOException {
+        close0();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws DecodeException
+     *             If the entry could not be decoded because it was malformed.
+     */
+    @Override
+    public boolean hasNext() throws DecodeException, IOException {
+        return getNextEntry() != EOF;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws DecodeException
+     *             If the entry could not be decoded because it was malformed.
+     */
+    @Override
+    public Entry readEntry() throws DecodeException, IOException {
+        if (!hasNext()) {
+            // LDIF reader has completed successfully.
+            throw new NoSuchElementException();
+        }
+
+        final Entry entry = nextEntry;
+        nextEntry = null;
+        return entry;
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be excluded
+     * from any entries that are read from LDIF. The default is {@code false}.
+     *
+     * @param excludeOperationalAttributes
+     *            {@code true} if all operational attributes should be excluded,
+     *            or {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setExcludeAllOperationalAttributes(
+            final boolean excludeOperationalAttributes) {
+        this.excludeOperationalAttributes = excludeOperationalAttributes;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be excluded from any
+     * entries that are read from LDIF. The default is {@code false}.
+     *
+     * @param excludeUserAttributes
+     *            {@code true} if all user attributes should be excluded, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setExcludeAllUserAttributes(final boolean excludeUserAttributes) {
+        this.excludeUserAttributes = excludeUserAttributes;
+        return this;
+    }
+
+    /**
+     * Excludes the named attribute from any entries that are read from LDIF. By
+     * default all attributes are included unless explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be excluded.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setExcludeAttribute(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        excludeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Excludes all entries beneath the named entry (inclusive) from being read
+     * from LDIF. By default all entries are written unless explicitly excluded
+     * or included by branches or filters.
+     *
+     * @param excludeBranch
+     *            The distinguished name of the branch to be excluded.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setExcludeBranch(final DN excludeBranch) {
+        Reject.ifNull(excludeBranch);
+        excludeBranches.add(excludeBranch);
+        return this;
+    }
+
+    /**
+     * Excludes all entries which match the provided filter matcher from being
+     * read from LDIF. By default all entries are read unless explicitly
+     * excluded or included by branches or filters.
+     *
+     * @param excludeFilter
+     *            The filter matcher.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setExcludeFilter(final Matcher excludeFilter) {
+        Reject.ifNull(excludeFilter);
+        excludeFilters.add(excludeFilter);
+        return this;
+    }
+
+    /**
+     * Ensures that the named attribute is not excluded from any entries that
+     * are read from LDIF. By default all attributes are included unless
+     * explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setIncludeAttribute(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        includeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Ensures that all entries beneath the named entry (inclusive) are read
+     * from LDIF. By default all entries are written unless explicitly excluded
+     * or included by branches or filters.
+     *
+     * @param includeBranch
+     *            The distinguished name of the branch to be included.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setIncludeBranch(final DN includeBranch) {
+        Reject.ifNull(includeBranch);
+        includeBranches.add(includeBranch);
+        return this;
+    }
+
+    /**
+     * Ensures that all entries which match the provided filter matcher are read
+     * from LDIF. By default all entries are read unless explicitly excluded or
+     * included by branches or filters.
+     *
+     * @param includeFilter
+     *            The filter matcher.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setIncludeFilter(final Matcher includeFilter) {
+        Reject.ifNull(includeFilter);
+        includeFilters.add(includeFilter);
+        return this;
+    }
+
+    /**
+     * Sets the rejected record listener which should be notified whenever an
+     * LDIF record is skipped, malformed, or fails schema validation.
+     * <p>
+     * By default the {@link RejectedLDIFListener#FAIL_FAST} listener is used.
+     *
+     * @param listener
+     *            The rejected record listener.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setRejectedLDIFListener(final RejectedLDIFListener listener) {
+        this.rejectedRecordListener = listener;
+        return this;
+    }
+
+    /**
+     * Sets the schema which should be used for decoding entries that are read
+     * from LDIF. The default schema is used if no other is specified.
+     *
+     * @param schema
+     *            The schema which should be used for decoding entries that are
+     *            read from LDIF.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setSchema(final Schema schema) {
+        Reject.ifNull(schema);
+        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
+        return this;
+    }
+
+    /**
+     * Specifies the schema validation which should be used when reading LDIF
+     * entry records. If attribute value validation is enabled then all checks
+     * will be performed.
+     * <p>
+     * Schema validation is disabled by default.
+     * <p>
+     * <b>NOTE:</b> this method copies the provided policy so changes made to it
+     * after this method has been called will have no effect.
+     *
+     * @param policy
+     *            The schema validation which should be used when reading LDIF
+     *            entry records.
+     * @return A reference to this {@code LDIFEntryReader}.
+     */
+    public LDIFEntryReader setSchemaValidationPolicy(final SchemaValidationPolicy policy) {
+        this.schemaValidationPolicy = SchemaValidationPolicy.copyOf(policy);
+        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
+        return this;
+    }
+
+    private Entry getNextEntry() throws DecodeException, IOException {
+        while (nextEntry == null) {
+            // Read the set of lines that make up the next entry.
+            final LDIFRecord record = readLDIFRecord();
+            if (record == null) {
+                nextEntry = EOF;
+                break;
+            }
+
+            try {
+                /* Read the DN of the entry and see if it is one that should be included in the import. */
+                final DN entryDN = readLDIFRecordDN(record);
+                if (entryDN == null) {
+                    // Skip version record.
+                    continue;
+                }
+
+                // Skip if branch containing the entry DN is excluded.
+                if (isBranchExcluded(entryDN)) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_ENTRY_EXCLUDED_BY_DN
+                                    .get(record.lineNumber, entryDN.toString());
+                    handleSkippedRecord(record, message);
+                    continue;
+                }
+
+                // Use an Entry for the AttributeSequence.
+                final Entry entry = new LinkedHashMapEntry(entryDN);
+                boolean schemaValidationFailure = false;
+                final List<LocalizableMessage> schemaErrors = new LinkedList<>();
+                while (record.iterator.hasNext()) {
+                    final String ldifLine = record.iterator.next();
+                    if (!readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors)) {
+                        schemaValidationFailure = true;
+                    }
+                }
+
+                // Skip if the entry is excluded by any filters.
+                if (isEntryExcluded(entry)) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_ENTRY_EXCLUDED_BY_FILTER.get(record.lineNumber, entryDN
+                                    .toString());
+                    handleSkippedRecord(record, message);
+                    continue;
+                }
+
+                if (!schema.validateEntry(entry, schemaValidationPolicy, schemaErrors)) {
+                    schemaValidationFailure = true;
+                }
+
+                if (schemaValidationFailure) {
+                    handleSchemaValidationFailure(record, schemaErrors);
+                    continue;
+                }
+
+                if (!schemaErrors.isEmpty()) {
+                    handleSchemaValidationWarning(record, schemaErrors);
+                }
+
+                nextEntry = entry;
+            } catch (final DecodeException e) {
+                handleMalformedRecord(record, e.getMessageObject());
+                continue;
+            }
+        }
+
+        return nextEntry;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryWriter.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryWriter.java
new file mode 100644
index 0000000..87cac56
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFEntryWriter.java
@@ -0,0 +1,293 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Matcher;
+
+import org.forgerock.util.Reject;
+
+/**
+ * An LDIF entry writer writes attribute value records (entries) using the LDAP
+ * Data Interchange Format (LDIF) to a user defined destination.
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data
+ *      Interchange Format (LDIF) - Technical Specification </a>
+ */
+public final class LDIFEntryWriter extends AbstractLDIFWriter implements EntryWriter {
+
+    /**
+     * Returns the LDIF string representation of the provided entry.
+     *
+     * @param entry
+     *            The entry.
+     * @return The LDIF string representation of the provided entry.
+     */
+    public static String toString(final Entry entry) {
+        final StringWriter writer = new StringWriter(128);
+        try (LDIFEntryWriter ldifWriter = new LDIFEntryWriter(writer)) {
+            ldifWriter.setAddUserFriendlyComments(true).writeEntry(entry);
+        } catch (final IOException e) {
+            // Should never happen.
+            throw new IllegalStateException(e);
+        }
+        return writer.toString();
+    }
+
+    /**
+     * Creates a new LDIF entry writer which will append lines of LDIF to the
+     * provided list.
+     *
+     * @param ldifLines
+     *            The list to which lines of LDIF should be appended.
+     */
+    public LDIFEntryWriter(final List<String> ldifLines) {
+        super(ldifLines);
+    }
+
+    /**
+     * Creates a new LDIF entry writer whose destination is the provided output
+     * stream.
+     *
+     * @param out
+     *            The output stream to use.
+     */
+    public LDIFEntryWriter(final OutputStream out) {
+        super(out);
+    }
+
+    /**
+     * Creates a new LDIF entry writer whose destination is the provided
+     * character stream writer.
+     *
+     * @param writer
+     *            The character stream writer to use.
+     */
+    public LDIFEntryWriter(final Writer writer) {
+        super(writer);
+    }
+
+    @Override
+    public void close() throws IOException {
+        close0();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        flush0();
+    }
+
+    /**
+     * Specifies whether or not user-friendly comments should be added whenever
+     * distinguished names or UTF-8 attribute values are encountered which
+     * contained non-ASCII characters. The default is {@code false}.
+     *
+     * @param addUserFriendlyComments
+     *            {@code true} if user-friendly comments should be added, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setAddUserFriendlyComments(final boolean addUserFriendlyComments) {
+        this.addUserFriendlyComments = addUserFriendlyComments;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be excluded
+     * from any entries that are written to LDIF. The default is {@code false}.
+     *
+     * @param excludeOperationalAttributes
+     *            {@code true} if all operational attributes should be excluded,
+     *            or {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setExcludeAllOperationalAttributes(
+            final boolean excludeOperationalAttributes) {
+        this.excludeOperationalAttributes = excludeOperationalAttributes;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be excluded from any
+     * entries that are written to LDIF. The default is {@code false}.
+     *
+     * @param excludeUserAttributes
+     *            {@code true} if all user attributes should be excluded, or
+     *            {@code false} otherwise.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setExcludeAllUserAttributes(final boolean excludeUserAttributes) {
+        this.excludeUserAttributes = excludeUserAttributes;
+        return this;
+    }
+
+    /**
+     * Excludes the named attribute from any entries that are written to LDIF.
+     * By default all attributes are included unless explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be excluded.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setExcludeAttribute(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        excludeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Excludes all entries beneath the named entry (inclusive) from being
+     * written to LDIF. By default all entries are written unless explicitly
+     * excluded or included by branches or filters.
+     *
+     * @param excludeBranch
+     *            The distinguished name of the branch to be excluded.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setExcludeBranch(final DN excludeBranch) {
+        Reject.ifNull(excludeBranch);
+        excludeBranches.add(excludeBranch);
+        return this;
+    }
+
+    /**
+     * Excludes all entries which match the provided filter matcher from being
+     * written to LDIF. By default all entries are written unless explicitly
+     * excluded or included by branches or filters.
+     *
+     * @param excludeFilter
+     *            The filter matcher.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setExcludeFilter(final Matcher excludeFilter) {
+        Reject.ifNull(excludeFilter);
+        excludeFilters.add(excludeFilter);
+        return this;
+    }
+
+    /**
+     * Ensures that the named attribute is not excluded from any entries that
+     * are written to LDIF. By default all attributes are included unless
+     * explicitly excluded.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setIncludeAttribute(final AttributeDescription attributeDescription) {
+        Reject.ifNull(attributeDescription);
+        includeAttributes.add(attributeDescription);
+        return this;
+    }
+
+    /**
+     * Ensures that all entries beneath the named entry (inclusive) are written
+     * to LDIF. By default all entries are written unless explicitly excluded or
+     * included by branches or filters.
+     *
+     * @param includeBranch
+     *            The distinguished name of the branch to be included.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setIncludeBranch(final DN includeBranch) {
+        Reject.ifNull(includeBranch);
+        includeBranches.add(includeBranch);
+        return this;
+    }
+
+    /**
+     * Ensures that all entries which match the provided filter matcher are
+     * written to LDIF. By default all entries are written unless explicitly
+     * excluded or included by branches or filters.
+     *
+     * @param includeFilter
+     *            The filter matcher.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setIncludeFilter(final Matcher includeFilter) {
+        Reject.ifNull(includeFilter);
+        includeFilters.add(includeFilter);
+        return this;
+    }
+
+    /**
+     * Specifies the column at which long lines should be wrapped. A value less
+     * than or equal to zero (the default) indicates that no wrapping should be
+     * performed.
+     *
+     * @param wrapColumn
+     *            The column at which long lines should be wrapped.
+     * @return A reference to this {@code LDIFEntryWriter}.
+     */
+    public LDIFEntryWriter setWrapColumn(final int wrapColumn) {
+        this.wrapColumn = wrapColumn;
+        return this;
+    }
+
+    @Override
+    public LDIFEntryWriter writeComment(final CharSequence comment) throws IOException {
+        writeComment0(comment);
+        return this;
+    }
+
+    @Override
+    public LDIFEntryWriter writeEntry(final Entry entry) throws IOException {
+        Reject.ifNull(entry);
+
+        // Skip if branch containing the entry is excluded.
+        if (isBranchExcluded(entry.getName())) {
+            return this;
+        }
+
+        // Skip if the entry is excluded by any filters.
+        if (isEntryExcluded(entry)) {
+            return this;
+        }
+
+        writeKeyAndValue("dn", entry.getName().toString());
+        for (final Attribute attribute : entry.getAllAttributes()) {
+            // Filter the attribute if required.
+            if (isAttributeExcluded(attribute.getAttributeDescription())) {
+                continue;
+            }
+
+            final String attributeDescription = attribute.getAttributeDescriptionAsString();
+            if (attribute.isEmpty()) {
+                writeKeyAndValue(attributeDescription, ByteString.empty());
+            } else {
+                for (final ByteString value : attribute) {
+                    writeKeyAndValue(attributeDescription, value);
+                }
+            }
+        }
+
+        // Make sure there is a blank line after the entry.
+        impl.println();
+
+        return this;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedChangeRecordListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedChangeRecordListener.java
new file mode 100644
index 0000000..f804c3b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedChangeRecordListener.java
@@ -0,0 +1,219 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.REJECTED_CHANGE_FAIL_ADD_DUPE;
+import static com.forgerock.opendj.ldap.CoreMessages.REJECTED_CHANGE_FAIL_MODIFYDN_DUPE;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+
+/**
+ * A listener interface which is notified whenever a change record cannot be
+ * applied to an entry. This may occur when an attempt is made to update a
+ * non-existent entry, or add an entry which already exists.
+ * <p>
+ * By default the {@link #FAIL_FAST} listener is used.
+ */
+public interface RejectedChangeRecordListener {
+    /**
+     * A handler which terminates processing by throwing a
+     * {@code DecodeException} as soon as a change is rejected.
+     */
+    RejectedChangeRecordListener FAIL_FAST = new RejectedChangeRecordListener() {
+
+        @Override
+        public Entry handleDuplicateEntry(final AddRequest change, final Entry existingEntry) throws DecodeException {
+            throw DecodeException.error(REJECTED_CHANGE_FAIL_ADD_DUPE.get(change.getName()));
+        }
+
+        @Override
+        public Entry handleDuplicateEntry(final ModifyDNRequest change, final Entry existingEntry,
+                final Entry renamedEntry) throws DecodeException {
+            throw DecodeException.error(REJECTED_CHANGE_FAIL_MODIFYDN_DUPE.get(renamedEntry.getName()));
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(final AddRequest change, final LocalizableMessage reason)
+                throws DecodeException {
+            throw DecodeException.error(reason);
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(final DeleteRequest change, final LocalizableMessage reason)
+                throws DecodeException {
+            throw DecodeException.error(reason);
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(final ModifyRequest change, final LocalizableMessage reason)
+                throws DecodeException {
+            throw DecodeException.error(reason);
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(final ModifyDNRequest change, final LocalizableMessage reason)
+                throws DecodeException {
+            throw DecodeException.error(reason);
+        }
+
+    };
+
+    /**
+     * The default handler which ignores changes applied to missing entries and
+     * tolerates duplicate entries by overwriting the existing entry with the
+     * new entry.
+     */
+    RejectedChangeRecordListener OVERWRITE = new RejectedChangeRecordListener() {
+
+        @Override
+        public Entry handleDuplicateEntry(final AddRequest change, final Entry existingEntry) throws DecodeException {
+            // Overwrite existing entries.
+            return change;
+        }
+
+        @Override
+        public Entry handleDuplicateEntry(final ModifyDNRequest change, final Entry existingEntry,
+                final Entry renamedEntry) throws DecodeException {
+            // Overwrite existing entries.
+            return renamedEntry;
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(AddRequest change, LocalizableMessage reason) throws DecodeException {
+            // Ignore.
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(DeleteRequest change, LocalizableMessage reason)
+                throws DecodeException {
+            // Ignore.
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(ModifyRequest change, LocalizableMessage reason)
+                throws DecodeException {
+            // Ignore.
+        }
+
+        @Override
+        public void handleRejectedChangeRecord(ModifyDNRequest change, LocalizableMessage reason)
+                throws DecodeException {
+            // Ignore.
+        }
+
+    };
+
+    /**
+     * Invoked when an attempt was made to add an entry which already exists.
+     *
+     * @param change
+     *            The conflicting add request.
+     * @param existingEntry
+     *            The pre-existing entry.
+     * @return The entry which should be kept.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    Entry handleDuplicateEntry(AddRequest change, Entry existingEntry) throws DecodeException;
+
+    /**
+     * Invoked when an attempt was made to rename an entry which already exists.
+     *
+     * @param change
+     *            The conflicting add request.
+     * @param existingEntry
+     *            The pre-existing entry.
+     * @param renamedEntry
+     *            The renamed entry.
+     * @return The entry which should be kept.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    Entry handleDuplicateEntry(ModifyDNRequest change, Entry existingEntry, Entry renamedEntry)
+            throws DecodeException;
+
+    /**
+     * Invoked when an attempt to add an entry was rejected. This may be because
+     * the target parent entry was not found, or controls provided with the
+     * request are not supported. This method will not be called when the entry
+     * to be added already exists, since this is handled by
+     * {@link #handleDuplicateEntry(AddRequest, Entry)}.
+     *
+     * @param change
+     *            The rejected add request.
+     * @param reason
+     *            The reason why the record was rejected.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleRejectedChangeRecord(AddRequest change, LocalizableMessage reason)
+            throws DecodeException;
+
+    /**
+     * Invoked when an attempt to delete an entry was rejected. This may be
+     * because the target entry was not found, or controls provided with the
+     * request are not supported.
+     *
+     * @param change
+     *            The rejected delete request.
+     * @param reason
+     *            The reason why the record was rejected.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleRejectedChangeRecord(DeleteRequest change, LocalizableMessage reason)
+            throws DecodeException;
+
+    /**
+     * Invoked when an attempt to modify an entry was rejected. This may be
+     * because the target entry was not found, or controls provided with the
+     * request are not supported.
+     *
+     * @param change
+     *            The rejected modify request.
+     * @param reason
+     *            The reason why the record was rejected.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleRejectedChangeRecord(ModifyRequest change, LocalizableMessage reason)
+            throws DecodeException;
+
+    /**
+     * Invoked when an attempt to rename an entry was rejected. This may be
+     * because the target entry was not found, or controls provided with the
+     * request are not supported. This method will not be called when a renamed
+     * entry already exists, since this is handled by
+     * {@link #handleDuplicateEntry(ModifyDNRequest, Entry, Entry)}.
+     *
+     * @param change
+     *            The rejected modify DN request.
+     * @param reason
+     *            The reason why the record was rejected.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleRejectedChangeRecord(ModifyDNRequest change, LocalizableMessage reason)
+            throws DecodeException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedLDIFListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedLDIFListener.java
new file mode 100644
index 0000000..262a2ed
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/RejectedLDIFListener.java
@@ -0,0 +1,164 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DecodeException;
+
+/**
+ * A listener interface which is notified whenever LDIF records are skipped,
+ * malformed, or fail schema validation.
+ * <p>
+ * By default the {@link #FAIL_FAST} listener is used.
+ */
+public interface RejectedLDIFListener {
+    /**
+     * The default handler which ignores skipped records but which terminates
+     * processing by throwing a {@code DecodeException} as soon as a record is
+     * found to be malformed or rejected due to a schema validation failure.
+     */
+    RejectedLDIFListener FAIL_FAST = new RejectedLDIFListener() {
+
+        @Override
+        public void handleMalformedRecord(final long lineNumber, final List<String> lines,
+                final LocalizableMessage reason) throws DecodeException {
+            // Fail fast.
+            throw DecodeException.error(reason);
+        }
+
+        @Override
+        public void handleSchemaValidationFailure(final long lineNumber, final List<String> lines,
+                final List<LocalizableMessage> reasons) throws DecodeException {
+            // Fail fast - just use first message.
+            throw DecodeException.error(reasons.get(0));
+        }
+
+        @Override
+        public void handleSchemaValidationWarning(final long lineNumber, final List<String> lines,
+                final List<LocalizableMessage> reasons) throws DecodeException {
+            // Ignore schema validation warnings.
+        }
+
+        @Override
+        public void handleSkippedRecord(final long lineNumber, final List<String> lines,
+                final LocalizableMessage reason) throws DecodeException {
+            // Ignore skipped records.
+        }
+    };
+
+    /**
+     * A handler which ignores all rejected record notifications.
+     */
+    RejectedLDIFListener IGNORE_ALL = new RejectedLDIFListener() {
+
+        @Override
+        public void handleMalformedRecord(final long lineNumber, final List<String> lines,
+                final LocalizableMessage reason) throws DecodeException {
+            // Ignore malformed records.
+        }
+
+        @Override
+        public void handleSchemaValidationFailure(final long lineNumber, final List<String> lines,
+                final List<LocalizableMessage> reasons) throws DecodeException {
+            // Ignore schema validation failures.
+        }
+
+        @Override
+        public void handleSchemaValidationWarning(final long lineNumber, final List<String> lines,
+                final List<LocalizableMessage> reasons) throws DecodeException {
+            // Ignore schema validation warnings.
+        }
+
+        @Override
+        public void handleSkippedRecord(final long lineNumber, final List<String> lines,
+                final LocalizableMessage reason) throws DecodeException {
+            // Ignore skipped records.
+        }
+    };
+
+    /**
+     * Invoked when a record was rejected because it was malformed in some way
+     * and could not be decoded.
+     *
+     * @param lineNumber
+     *            The line number within the source location in which the
+     *            malformed record is located, if known, otherwise {@code -1}.
+     * @param lines
+     *            The content of the malformed record.
+     * @param reason
+     *            The reason why the record is malformed.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleMalformedRecord(long lineNumber, List<String> lines, LocalizableMessage reason)
+            throws DecodeException;
+
+    /**
+     * Invoked when a record was rejected because it does not conform to the
+     * schema and schema validation is enabled.
+     *
+     * @param lineNumber
+     *            The line number within the source location in which the
+     *            rejected record is located, if known, otherwise {@code -1}.
+     * @param lines
+     *            The content of the record which failed schema validation.
+     * @param reasons
+     *            The reasons why the record failed schema validation.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleSchemaValidationFailure(long lineNumber, List<String> lines,
+            List<LocalizableMessage> reasons) throws DecodeException;
+
+    /**
+     * Invoked when a record was not rejected but contained one or more schema
+     * validation warnings.
+     *
+     * @param lineNumber
+     *            The line number within the source location in which the record
+     *            is located, if known, otherwise {@code -1}.
+     * @param lines
+     *            The content of the record which contained schema validation
+     *            warnings.
+     * @param reasons
+     *            The schema validation warnings.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleSchemaValidationWarning(long lineNumber, List<String> lines,
+            List<LocalizableMessage> reasons) throws DecodeException;
+
+    /**
+     * Invoked when a record was skipped because it did not match filter
+     * criteria defined by the reader.
+     *
+     * @param lineNumber
+     *            The line number within the source location in which the
+     *            skipped record is located, if known, otherwise {@code -1}.
+     * @param lines
+     *            The content of the record which was skipped.
+     * @param reason
+     *            The reason why the record was skipped.
+     * @throws DecodeException
+     *             If processing should terminate.
+     */
+    void handleSkippedRecord(long lineNumber, List<String> lines, LocalizableMessage reason)
+            throws DecodeException;
+
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java
new file mode 100644
index 0000000..6dd3d11
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java
@@ -0,0 +1,2068 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.AVA;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.TemplateTag.AttributeValueTag;
+import org.forgerock.opendj.ldif.TemplateTag.DNTag;
+import org.forgerock.opendj.ldif.TemplateTag.FileTag;
+import org.forgerock.opendj.ldif.TemplateTag.FirstNameTag;
+import org.forgerock.opendj.ldif.TemplateTag.GUIDTag;
+import org.forgerock.opendj.ldif.TemplateTag.IfAbsentTag;
+import org.forgerock.opendj.ldif.TemplateTag.IfPresentTag;
+import org.forgerock.opendj.ldif.TemplateTag.LastNameTag;
+import org.forgerock.opendj.ldif.TemplateTag.ListTag;
+import org.forgerock.opendj.ldif.TemplateTag.ParentDNTag;
+import org.forgerock.opendj.ldif.TemplateTag.PresenceTag;
+import org.forgerock.opendj.ldif.TemplateTag.RDNTag;
+import org.forgerock.opendj.ldif.TemplateTag.RandomTag;
+import org.forgerock.opendj.ldif.TemplateTag.SequentialTag;
+import org.forgerock.opendj.ldif.TemplateTag.StaticTextTag;
+import org.forgerock.opendj.ldif.TemplateTag.TagResult;
+import org.forgerock.opendj.ldif.TemplateTag.UnderscoreDNTag;
+import org.forgerock.opendj.ldif.TemplateTag.UnderscoreParentDNTag;
+import org.forgerock.util.Pair;
+import org.forgerock.util.Reject;
+
+/**
+ * A template file allow to generate entries from a collection of constant
+ * definitions, branches, and templates.
+ *
+ * @see EntryGenerator
+ */
+final class TemplateFile {
+
+    /** Default resource path used if no resource path is provided. */
+    private static final String DEFAULT_RESOURCES_PATH = "org/forgerock/opendj/ldif";
+
+    /** Default template path used if no template file is provided. */
+    private static final String DEFAULT_TEMPLATE_PATH = "example.template";
+
+    /** The name of the file holding the list of first names. */
+    private static final String FIRST_NAME_FILE = "first.names";
+
+    /** The name of the file holding the list of last names. */
+    private static final String LAST_NAME_FILE = "last.names";
+
+    /** Default value for infinite number of entries. */
+    private static final int INFINITE_ENTRIES = -1;
+
+    /**
+     * A map of the contents of various text files used during the parsing
+     * process, mapped from absolute path to the array of lines in the file.
+     */
+    private final Map<String, String[]> fileLines = new HashMap<>();
+
+    /** The index of the next first name value that should be used. */
+    private int firstNameIndex;
+
+    /** The index of the next last name value that should be used. */
+    private int lastNameIndex;
+
+    /**
+     * A counter used to keep track of the number of times that the larger of
+     * the first/last name list has been completed.
+     */
+    private int nameLoopCounter;
+
+    /**
+     * A counter that will be used in case we have exhausted all possible first
+     * and last name combinations.
+     */
+    private int nameUniquenessCounter = 1;
+
+    /** The set of branch definitions for this template file. */
+    private final Map<DN, Branch> branches = new LinkedHashMap<>();
+
+    /** The set of constant definitions for this template file. */
+    private final Map<String, String> constants;
+
+    /** The set of registered tags for this template file. */
+    private final Map<String, TemplateTag> registeredTags = new LinkedHashMap<>();
+
+    /** The set of template definitions for this template file. */
+    private final Map<String, Template> templates = new LinkedHashMap<>();
+
+    /** The random number generator for this template file. */
+    private final Random random;
+
+    /** The next first name that should be used. */
+    private String firstName;
+
+    /** The next last name that should be used. */
+    private String lastName;
+
+    /** Indicates whether branch entries should be generated. */
+    private boolean generateBranches;
+
+    /**
+     * The resource path to use for filesystem elements that cannot be found
+     * anywhere else.
+     */
+    private String resourcePath;
+
+    /** The set of first names to use when generating the LDIF. */
+    private String[] firstNames = new String[0];
+
+    /** The set of last names to use when generating the LDIF. */
+    private String[] lastNames = new String[0];
+
+    /** Schema used to create attributes. */
+    private final Schema schema;
+
+    /**
+     * Creates a new, empty template file structure.
+     *
+     * @param schema
+     *            LDAP Schema to use.
+     * @param constants
+     *            Constants to use, override any constant defined in the
+     *            template file. May be {@code null}.
+     * @param resourcePath
+     *            The path to the directory that may contain additional resource
+     *            files needed during the generation process. May be
+     *            {@code null}.
+     * @throws IOException
+     *             if a problem occurs when initializing
+     */
+    TemplateFile(Schema schema, Map<String, String> constants, String resourcePath) throws IOException {
+        this(schema, constants, resourcePath, new Random(), true);
+    }
+
+    /**
+     * Creates a new, empty template file structure.
+     *
+     * @param schema
+     *            LDAP Schema to use.
+     * @param constants
+     *            Constants to use, override any constant defined in the
+     *            template file. May be {@code null}.
+     * @param resourcePath
+     *            The path to the directory that may contain additional resource
+     *            files needed during the generation process. May be
+     *            {@code null}.
+     * @param random
+     *            The random number generator for this template file.
+     * @param generateBranches
+     *            Indicates whether branch entries should be generated.
+     * @throws IOException
+     *             if a problem occurs when initializing
+     */
+    TemplateFile(Schema schema, Map<String, String> constants, String resourcePath,
+                    Random random, boolean generateBranches)
+            throws IOException {
+        Reject.ifNull(schema, random);
+        this.generateBranches = generateBranches;
+        this.schema = schema;
+        this.constants = constants != null ? constants : new HashMap<String, String>();
+        this.resourcePath = resourcePath;
+        this.random = random;
+        registerDefaultTags();
+        retrieveFirstAndLastNames();
+    }
+
+    TemplateTag getTag(String lowerName) {
+        return registeredTags.get(lowerName);
+    }
+
+    /**
+     * Registers the set of tags that will always be available for use in
+     * templates.
+     */
+    private void registerDefaultTags() {
+        Class<?>[] defaultTagClasses = new Class<?>[] { AttributeValueTag.class, DNTag.class, FileTag.class,
+            FirstNameTag.class, GUIDTag.class, IfAbsentTag.class, IfPresentTag.class, LastNameTag.class,
+            ListTag.class, ParentDNTag.class, PresenceTag.class, RandomTag.class, RDNTag.class,
+            SequentialTag.class, StaticTextTag.class, UnderscoreDNTag.class, UnderscoreParentDNTag.class };
+
+        for (final Class<?> c : defaultTagClasses) {
+            try {
+                final TemplateTag t = (TemplateTag) c.newInstance();
+                registeredTags.put(t.getName().toLowerCase(), t);
+            } catch (Exception e) {
+                // this is a programming error
+                throw new RuntimeException(ERR_ENTRY_GENERATOR_CANNOT_INSTANTIATE_TAG.get(c.getName()).toString(), e);
+            }
+        }
+    }
+
+    Random getRandom() {
+        return random;
+    }
+
+    private void retrieveFirstAndLastNames() throws IOException {
+        try (BufferedReader first = getReader(FIRST_NAME_FILE)) {
+            if (first == null) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_COULD_NOT_FIND_NAME_FILE.get(FIRST_NAME_FILE));
+            }
+            final List<String> names = readLines(first);
+            firstNames = names.toArray(new String[names.size()]);
+        }
+
+        try (BufferedReader last = getReader(LAST_NAME_FILE)) {
+            if (last == null) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_COULD_NOT_FIND_NAME_FILE.get(LAST_NAME_FILE));
+            }
+            final List<String> names = readLines(last);
+            lastNames = names.toArray(new String[names.size()]);
+        }
+    }
+
+    /**
+     * Updates the first and last name indexes to choose new values. The
+     * algorithm used is designed to ensure that the combination of first and
+     * last names will never be repeated. It depends on the number of first
+     * names and the number of last names being relatively prime. This method
+     * should be called before beginning generation of each template entry.
+     */
+    void nextFirstAndLastNames() {
+        firstName = firstNames[firstNameIndex++];
+        lastName = lastNames[lastNameIndex++];
+
+        // If we've already exhausted every possible combination
+        // then append an integer to the last name.
+        if (nameUniquenessCounter > 1) {
+            lastName += nameUniquenessCounter;
+        }
+
+        if (firstNameIndex >= firstNames.length) {
+            // We're at the end of the first name list, so start over.
+            // If the first name list is larger than the last name list,
+            // then we'll also need to set the last name index
+            // to the next loop counter position.
+            firstNameIndex = 0;
+            if (firstNames.length > lastNames.length) {
+                lastNameIndex = ++nameLoopCounter;
+                if (lastNameIndex >= lastNames.length) {
+                    lastNameIndex = 0;
+                    nameUniquenessCounter++;
+                }
+            }
+        }
+
+        if (lastNameIndex >= lastNames.length) {
+            // We're at the end of the last name list, so start over.
+            // If the last name list is larger than the first name list,
+            // then we'll also need to set the first name index
+            // to the next loop counter position.
+            lastNameIndex = 0;
+            if (lastNames.length > firstNames.length) {
+                firstNameIndex = ++nameLoopCounter;
+                if (firstNameIndex >= firstNames.length) {
+                    firstNameIndex = 0;
+                    nameUniquenessCounter++;
+                }
+            }
+        }
+    }
+
+    String getFirstName() {
+        return firstName;
+    }
+
+    String getLastName() {
+        return lastName;
+    }
+
+    /**
+     * Parses the contents of the default template file definition, that will be
+     * used to generate entries.
+     *
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @throws IOException
+     *             If a problem occurs while attempting to read data from the
+     *             default template file.
+     * @throws DecodeException
+     *             If any other problem occurs while parsing the template file.
+     */
+    void parse(List<LocalizableMessage> warnings) throws IOException, DecodeException {
+        parse(DEFAULT_TEMPLATE_PATH, warnings);
+    }
+
+    /**
+     * Parses the contents of the provided file as an entry generator template
+     * file definition.
+     *
+     * @param templateFilename
+     *            The name of the file containing the template data.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @throws IOException
+     *             If a problem occurs while attempting to read data from the
+     *             specified file.
+     * @throws DecodeException
+     *             If any other problem occurs while parsing the template file.
+     */
+    void parse(String templateFilename, List<LocalizableMessage> warnings) throws IOException, DecodeException {
+        try (BufferedReader templateReader = getReader(templateFilename)) {
+            if (templateReader == null) {
+                throw DecodeException.fatalError(
+                        ERR_ENTRY_GENERATOR_COULD_NOT_FIND_TEMPLATE_FILE.get(templateFilename));
+            }
+            if (resourcePath == null) {
+                // Use the template file directory as resource path
+                final File file = getFile(templateFilename);
+                if (file != null) {
+                    resourcePath = file.getCanonicalFile().getParentFile().getAbsolutePath();
+                }
+            }
+            final List<String> fileLines = readLines(templateReader);
+            final String[] lines = fileLines.toArray(new String[fileLines.size()]);
+            parse(lines, warnings);
+        }
+    }
+
+    /**
+     * Parses the contents of the provided input stream as an entry generator
+     * template file definition.
+     *
+     * @param inputStream
+     *            The input stream from which to read the template file data.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @throws IOException
+     *             If a problem occurs while attempting to read data from the
+     *             provided input stream.
+     * @throws DecodeException
+     *             If any other problem occurs while parsing the template.
+     */
+    void parse(InputStream inputStream, List<LocalizableMessage> warnings) throws IOException, DecodeException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+            final List<String> fileLines = readLines(reader);
+            final String[] lines = fileLines.toArray(new String[fileLines.size()]);
+            parse(lines, warnings);
+        }
+    }
+
+    private static final String INCLUDE_LABEL = "include ";
+    private static final String DEFINE_LABEL = "define ";
+    private static final String BRANCH_LABEL = "branch: ";
+    private static final String TEMPLATE_LABEL = "template: ";
+    private static final String SUBORDINATE_TEMPLATE_LABEL = "subordinatetemplate: ";
+    private static final String RDNATTR_LABEL = "rdnattr: ";
+    private static final String EXTENDS_LABEL = "extends: ";
+
+    /**
+     * Structure to hold template data during parsing of the template.
+     */
+    private static class TemplateData {
+        final Map<String, TemplateTag> tags = new LinkedHashMap<>();
+        final Map<DN, Branch> branches = new LinkedHashMap<>();
+        final Map<String, Template> templates = new LinkedHashMap<>();
+    }
+
+    /**
+     * Enumeration of elements that act as "container" of other elements.
+     */
+    private enum Element {
+        BRANCH, TEMPLATE;
+
+        String getLabel() {
+            return toString().toLowerCase();
+        }
+    }
+
+    /**
+     * Parses the provided lines as an entry generator template file definition.
+     *
+     * @param lines
+     *            The lines that make up the template file.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @throws DecodeException
+     *             If any other problem occurs while parsing the template lines.
+     */
+    void parse(final String[] lines, final List<LocalizableMessage> warnings) throws DecodeException {
+        TemplateData templateData = new TemplateData();
+
+        for (int lineNumber = 0; lineNumber < lines.length; lineNumber++) {
+            final String currentRawLine = lines[lineNumber];
+            if (currentRawLine.isEmpty() || currentRawLine.startsWith("#")) {
+                // This is a comment or a blank line, so we'll ignore it.
+                continue;
+            }
+
+            final String line = replaceConstants(currentRawLine, lineNumber, constants, warnings);
+            final String lowerLine = line.toLowerCase();
+            if (line.length() == 0 || line.startsWith("#")) {
+                // This is a comment or a blank line, so we'll ignore it.
+                continue;
+            } else if (lowerLine.startsWith(INCLUDE_LABEL)) {
+                parseInclude(line, templateData.tags);
+            } else if (lowerLine.startsWith(DEFINE_LABEL)) {
+                parseDefine(lineNumber, line, constants, warnings);
+            } else if (lowerLine.startsWith(BRANCH_LABEL)) {
+                lineNumber = parseBranch(lineNumber, line, lines, templateData, warnings);
+            } else if (lowerLine.startsWith(TEMPLATE_LABEL)) {
+                lineNumber = parseTemplate(lineNumber, line, lines, templateData, warnings);
+            } else {
+                throw DecodeException.fatalError(
+                        ERR_ENTRY_GENERATOR_UNEXPECTED_TEMPLATE_FILE_LINE.get(line, lineNumber + 1));
+            }
+        }
+
+        // Finalize the branch and template definitions
+        // and then update the template file variables.
+        for (Branch b : templateData.branches.values()) {
+            b.completeBranchInitialization(templateData.templates, generateBranches);
+        }
+
+        for (Template t : templateData.templates.values()) {
+            t.completeTemplateInitialization(templateData.templates);
+        }
+
+        registeredTags.putAll(templateData.tags);
+        branches.putAll(templateData.branches);
+        templates.putAll(templateData.templates);
+
+        // Initialize iterator on branches and current branch used
+        // to read entries
+        if (branchesIterator == null) {
+            branchesIterator = branches.values().iterator();
+            if (branchesIterator.hasNext()) {
+                currentBranch = branchesIterator.next();
+            }
+        }
+    }
+
+    private void parseInclude(final String line, final Map<String, TemplateTag> templateFileIncludeTags)
+            throws DecodeException {
+        // The next element should be the name of the class.
+        // Load and instantiate it and make sure there are no conflicts.
+        final String className = line.substring(INCLUDE_LABEL.length()).trim();
+
+        Class<?> tagClass = null;
+        try {
+            tagClass = Class.forName(className);
+        } catch (Exception e) {
+            final LocalizableMessage message = ERR_ENTRY_GENERATOR_CANNOT_LOAD_TAG_CLASS.get(className);
+            throw DecodeException.fatalError(message, e);
+        }
+
+        TemplateTag tag;
+        try {
+            tag = (TemplateTag) tagClass.newInstance();
+        } catch (Exception e) {
+            final LocalizableMessage message = ERR_ENTRY_GENERATOR_CANNOT_INSTANTIATE_TAG.get(className);
+            throw DecodeException.fatalError(message, e);
+        }
+
+        String lowerName = tag.getName().toLowerCase();
+        if (registeredTags.containsKey(lowerName) || templateFileIncludeTags.containsKey(lowerName)) {
+            final LocalizableMessage message = ERR_ENTRY_GENERATOR_CONFLICTING_TAG_NAME.get(className, tag.getName());
+            throw DecodeException.fatalError(message);
+        }
+
+        templateFileIncludeTags.put(lowerName, tag);
+    }
+
+    private void parseDefine(final int lineNumber, final String line, final Map<String, String> templateFileConstants,
+            final List<LocalizableMessage> warnings) throws DecodeException {
+        // The rest of the line should contain the constant name,
+        // an equal sign, and the constant value.
+        final int equalPos = line.indexOf('=', DEFINE_LABEL.length());
+        if (equalPos < 0) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_DEFINE_MISSING_EQUALS.get(lineNumber + 1));
+        }
+
+        final String name = line.substring(DEFINE_LABEL.length(), equalPos).trim();
+        if (name.length() == 0) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_DEFINE_NAME_EMPTY.get(lineNumber + 1));
+        }
+
+        final String value = line.substring(equalPos + 1);
+        if (value.length() == 0) {
+            warnings.add(ERR_ENTRY_GENERATOR_WARNING_DEFINE_VALUE_EMPTY.get(name, lineNumber + 1));
+        }
+
+        final String lowerName = name.toLowerCase();
+        if (!templateFileConstants.containsKey(lowerName)) {
+            templateFileConstants.put(lowerName, value);
+        }
+    }
+
+    /**
+     * Parses the complete branch and returns the current line number at the
+     * end.
+     */
+    private int parseBranch(final int startLineNumber, final String startLine, final String[] lines,
+            final TemplateData templateData, final List<LocalizableMessage> warnings) throws DecodeException {
+        final String[] branchLines =
+                parseLinesUntilEndOfBlock(startLineNumber, startLine, lines, warnings);
+        final Branch branch = parseBranchDefinition(branchLines, startLineNumber, templateData.tags, warnings);
+        final DN branchDN = branch.getBranchDN();
+        if (templateData.branches.containsKey(branchDN)) {
+            throw DecodeException.fatalError(
+                    ERR_ENTRY_GENERATOR_CONFLICTING_BRANCH_DN.get(String.valueOf(branchDN), startLineNumber + 1));
+        }
+        templateData.branches.put(branchDN, branch);
+        // position to next line after end of branch
+        return startLineNumber + branchLines.length;
+    }
+
+    /**
+     * Parses the complete template and returns the current line number at the
+     * end.
+     */
+    private int parseTemplate(final int startLineNumber, final String startLine, final String[] lines,
+            final TemplateData templateData, final List<LocalizableMessage> warnings) throws DecodeException {
+        final String[] templateLines =
+                parseLinesUntilEndOfBlock(startLineNumber, startLine, lines, warnings);
+        final Template template =
+                parseTemplateDefinition(startLineNumber, templateLines, templateData, warnings);
+        final String lowerName = template.getName().toLowerCase();
+        if (templateData.templates.containsKey(lowerName)) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_CONFLICTING_TEMPLATE_NAME.get(
+                    String.valueOf(template.getName()), startLineNumber + 1));
+        }
+        templateData.templates.put(lowerName, template);
+        // position to next line after end of template
+        return startLineNumber + templateLines.length;
+    }
+
+    /**
+     * Parses lines of a block until the block ends (with an empty line) or
+     * lines ends.
+     *
+     * @param startLineNumber
+     *            Line number at beginning of block.
+     * @param startLine
+     *            First line of block.
+     * @param lines
+     *            The list of all lines in the template.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The lines of the block
+     */
+    private String[] parseLinesUntilEndOfBlock(final int startLineNumber, final String startLine,
+            final String[] lines, final List<LocalizableMessage> warnings) {
+        final List<String> lineList = new ArrayList<>();
+        String line = startLine;
+        lineList.add(line);
+
+        int lineNumber = startLineNumber;
+        while (true) {
+            lineNumber++;
+            if (lineNumber >= lines.length) {
+                break;
+            }
+            line = lines[lineNumber];
+            if (line.length() == 0) {
+                break;
+            }
+            line = replaceConstants(line, lineNumber, constants, warnings);
+            lineList.add(line);
+        }
+        return lineList.toArray(new String[lineList.size()]);
+    }
+
+    /**
+     * Parse a line and replace all constants within [ ] with their values.
+     *
+     * @param line
+     *            The line to parse.
+     * @param lineNumber
+     *            The line number in the template file.
+     * @param constants
+     *            The set of constants defined in the template file.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The line in which all constant variables have been replaced with
+     *         their value
+     */
+    private String replaceConstants(final String line, final int lineNumber, final Map<String, String> constants,
+            final List<LocalizableMessage> warnings) {
+        String newLine = line;
+        int closePos = line.lastIndexOf(']');
+        // Loop until we've scanned all closing brackets
+        do {
+            // Skip escaped closing brackets
+            while (closePos > 0 && newLine.charAt(closePos - 1) == '\\') {
+                closePos = newLine.lastIndexOf(']', closePos - 1);
+            }
+            if (closePos > 0) {
+                final StringBuilder lineBuffer = new StringBuilder(newLine);
+                int openPos = newLine.lastIndexOf('[', closePos);
+                // Find the opening bracket.
+                // If it's escaped, then it's not a constant
+                if ((openPos > 0 && newLine.charAt(openPos - 1) != '\\') || (openPos == 0)) {
+                    final String constantName = newLine.substring(openPos + 1, closePos).toLowerCase();
+                    final String constantValue = constants.get(constantName);
+                    if (constantValue != null) {
+                        lineBuffer.replace(openPos, closePos + 1, constantValue);
+                    } else {
+                        warnings.add(WARN_ENTRY_GENERATOR_WARNING_UNDEFINED_CONSTANT.get(constantName, lineNumber + 1));
+                    }
+                }
+                if (openPos >= 0) {
+                    closePos = openPos;
+                }
+                newLine = lineBuffer.toString();
+                closePos = newLine.lastIndexOf(']', closePos);
+            }
+        } while (closePos > 0);
+        return newLine;
+    }
+
+    /**
+     * Parses the information contained in the provided set of lines as a branch
+     * definition.
+     *
+     * @param branchLines
+     *            The set of lines containing the branch definition.
+     * @param startLineNumber
+     *            The line number in the template file on which the first of the
+     *            branch lines appears.
+     * @param tags
+     *            The set of defined tags from the template file. Note that this
+     *            does not include the tags that are always registered by
+     *            default.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The decoded branch definition.
+     * @throws DecodeException
+     *             If a problem occurs during initializing any of the branch
+     *             elements or during processing.
+     */
+    private Branch parseBranchDefinition(final String[] branchLines, final int startLineNumber,
+            final Map<String, TemplateTag> tags, final List<LocalizableMessage> warnings) throws DecodeException {
+        // The first line must be "branch: " followed by the branch DN.
+        final String dnString = branchLines[0].substring(BRANCH_LABEL.length()).trim();
+        DN branchDN;
+        try {
+            branchDN = DN.valueOf(dnString, schema);
+        } catch (Exception e) {
+            throw DecodeException.fatalError(
+                    ERR_ENTRY_GENERATOR_CANNOT_DECODE_BRANCH_DN.get(dnString, startLineNumber + 1));
+        }
+
+        final Branch branch = new Branch(this, branchDN, schema);
+
+        for (int i = 1; i < branchLines.length; i++) {
+            final String line = branchLines[i];
+            final String lowerLine = line.toLowerCase();
+            final int lineNumber = startLineNumber + i;
+
+            if (lowerLine.startsWith("#")) {
+                // It's a comment, so we should ignore it.
+                continue;
+            } else if (lowerLine.startsWith(SUBORDINATE_TEMPLATE_LABEL)) {
+                final Pair<String, Integer> pair =
+                        parseSubordinateTemplate(lineNumber, line, Element.BRANCH, dnString, warnings);
+                final String templateName = pair.getFirst();
+                final int numEntries = pair.getSecond();
+                branch.addSubordinateTemplate(templateName, numEntries);
+            } else {
+                final TemplateLine templateLine =
+                        parseTemplateLine(line, lineNumber, branch, null, Element.BRANCH, tags, warnings);
+                branch.addExtraLine(templateLine);
+            }
+        }
+        return branch;
+    }
+
+    /**
+     * Parses the information contained in the provided set of lines as a
+     * template definition.
+     *
+     * @param startLineNumber
+     *            The line number in the template file on which the first of the
+     *            template lines appears.
+     * @param templateLines
+     *            The set of lines containing the template definition.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The decoded template definition.
+     * @throws DecodeException
+     *             If a problem occurs during initializing any of the template
+     *             elements or during processing.
+     */
+    private Template parseTemplateDefinition(final int startLineNumber, final String[] templateLines,
+            final TemplateData templateData, final List<LocalizableMessage> warnings) throws DecodeException {
+        final Map<String, TemplateTag> tags = templateData.tags;
+        final Map<String, Template> definedTemplates = templateData.templates;
+
+        // The first line must be "template: " followed by the template name.
+        final String templateName = templateLines[0].substring(TEMPLATE_LABEL.length()).trim();
+
+        // The next line may be with an "extends", a rdn attribute, or
+        // a subordinate template. Keep reading until we find something
+        // that's not one of those.
+        int lineCount = 1;
+        Template parentTemplate = null;
+        final List<AttributeType> rdnAttributes = new ArrayList<>();
+        final List<String> subordinatesTemplateNames = new ArrayList<>();
+        final List<Integer> numberOfentriesPerTemplate = new ArrayList<>();
+
+        for (; lineCount < templateLines.length; lineCount++) {
+            final int lineNumber = startLineNumber + lineCount;
+            final String line = templateLines[lineCount];
+            final String lowerLine = line.toLowerCase();
+
+            if (lowerLine.startsWith("#")) {
+                // It's a comment. Ignore it.
+                continue;
+            } else if (lowerLine.startsWith(EXTENDS_LABEL)) {
+                final String parentTemplateName = line.substring(EXTENDS_LABEL.length()).trim();
+                parentTemplate = definedTemplates.get(parentTemplateName.toLowerCase());
+                if (parentTemplate == null) {
+                    throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TEMPLATE_INVALID_PARENT_TEMPLATE.get(
+                            parentTemplateName, lineNumber + 1, templateName));
+                }
+            } else if (lowerLine.startsWith(RDNATTR_LABEL)) {
+                // This is the set of RDN attributes. If there are multiple,
+                // they may be separated by plus signs.
+                final String rdnAttrNames = lowerLine.substring(RDNATTR_LABEL.length()).trim();
+                final StringTokenizer tokenizer = new StringTokenizer(rdnAttrNames, "+");
+                while (tokenizer.hasMoreTokens()) {
+                    rdnAttributes.add(schema.getAttributeType(tokenizer.nextToken()));
+                }
+            } else if (lowerLine.startsWith(SUBORDINATE_TEMPLATE_LABEL)) {
+                final Pair<String, Integer> pair =
+                        parseSubordinateTemplate(lineNumber, line, Element.BRANCH, templateName, warnings);
+                subordinatesTemplateNames.add(pair.getFirst());
+                numberOfentriesPerTemplate.add(pair.getSecond());
+            } else {
+                // Not recognized, it must be a template line.
+                break;
+            }
+        }
+
+        final List<TemplateLine> parentLines =
+                (parentTemplate == null) ? new ArrayList<TemplateLine>() : parentTemplate.getTemplateLines();
+
+        final Template template = new Template(this, templateName, rdnAttributes, subordinatesTemplateNames,
+                numberOfentriesPerTemplate, parentLines);
+
+        // Add lines to template
+        for (; lineCount < templateLines.length; lineCount++) {
+            final String line = templateLines[lineCount];
+            final String lowerLine = line.toLowerCase();
+
+            if (lowerLine.startsWith("#")) {
+                // It's a comment, ignore it.
+                continue;
+            } else {
+                final int lineNumber = startLineNumber + lineCount;
+                final TemplateLine templateLine =
+                        parseTemplateLine(line, lineNumber, null, template, Element.TEMPLATE, tags, warnings);
+                template.addTemplateLine(templateLine);
+            }
+        }
+
+        return template;
+    }
+
+    /**
+     * Parses a subordinate template for a template or a branch.
+     * <p>
+     * A subordinate template has a name and a number of entries.
+     *
+     * @param lineNumber
+     *            Line number of definition.
+     * @param line
+     *            Line containing the definition.
+     * @param element
+     *            indicates the kind of element to use in error messages.
+     * @param elementName
+     *            Name of the branch or template.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return the pair (template name, number of entries in template)
+     */
+    private Pair<String, Integer> parseSubordinateTemplate(final int lineNumber, final String line,
+            final Element element, final String elementName, final List<LocalizableMessage> warnings)
+            throws DecodeException {
+        // It's a subordinate template, so we'll want to parse the template name
+        // and the number of entries if it is provided.
+        final int colonPos = line.indexOf(':', SUBORDINATE_TEMPLATE_LABEL.length());
+        final String templateName;
+        int numEntries = INFINITE_ENTRIES;
+
+        if (colonPos <= SUBORDINATE_TEMPLATE_LABEL.length()) {
+            //No number of entries provided, generator will provides an infinite number of entries
+            templateName = line.substring(SUBORDINATE_TEMPLATE_LABEL.length(), line.length()).trim();
+        } else {
+            templateName = line.substring(SUBORDINATE_TEMPLATE_LABEL.length(), colonPos).trim();
+
+            try {
+                numEntries = Integer.parseInt(line.substring(colonPos + 1).trim());
+                if (numEntries == 0) {
+                    warnings.add(WARN_ENTRY_GENERATOR_SUBORDINATE_ZERO_ENTRIES.get(
+                            lineNumber + 1, element.getLabel(), elementName, templateName));
+                }
+            } catch (NumberFormatException nfe) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_SUBORDINATE_CANT_PARSE_NUMENTRIES.get(
+                        templateName, lineNumber + 1, element.getLabel(), elementName));
+            }
+        }
+
+        return Pair.of(templateName, numEntries);
+    }
+
+    private static final int PARSING_STATIC_TEXT = 0;
+    private static final int PARSING_REPLACEMENT_TAG = 1;
+    private static final int PARSING_ATTRIBUTE_TAG = 2;
+    private static final int PARSING_ESCAPED_CHAR = 3;
+
+    /**
+     * Parses the provided line as a template line. Note that exactly one of the
+     * branch or template arguments must be non-null and the other must be null.
+     *
+     * @param line
+     *            The text of the template line.
+     * @param lineNumber
+     *            The line number on which the template line appears.
+     * @param branch
+     *            The branch with which the template line is associated.
+     * @param template
+     *            The template with which the template line is associated.
+     * @param tags
+     *            The set of defined tags from the template file. Note that this
+     *            does not include the tags that are always registered by
+     *            default.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The template line that has been parsed.
+     * @throws DecodeException
+     *             If a problem occurs during initializing any of the template
+     *             elements or during processing.
+     */
+    private TemplateLine parseTemplateLine(final String line, final int lineNumber, final Branch branch,
+            final Template template, final Element element, final Map<String, TemplateTag> tags,
+            final List<LocalizableMessage> warnings) throws DecodeException {
+        final String elementName = element == Element.BRANCH ? branch.getBranchDN().toString() : template.getName();
+
+        // The first component must be the attribute type, followed by a colon.
+        final String lowerLine = line.toLowerCase();
+        final int colonPos = lowerLine.indexOf(':');
+        if (colonPos < 0) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_NO_COLON_IN_TEMPLATE_LINE.get(
+                    lineNumber + 1, element.getLabel(), elementName));
+        } else if (colonPos == 0) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_NO_ATTR_IN_TEMPLATE_LINE.get(
+                    lineNumber + 1, element.getLabel(), elementName));
+        }
+
+        final AttributeType attributeType = schema.getAttributeType(lowerLine.substring(0, colonPos));
+
+        // First, check whether the value is an URL value: <attrName>:< <url>
+        final int length = line.length();
+        int pos = colonPos + 1;
+        boolean valueIsURL = false;
+        boolean valueIsBase64 = false;
+        if (pos < length) {
+            if (lowerLine.charAt(pos) == '<') {
+                valueIsURL = true;
+                pos++;
+            } else if (lowerLine.charAt(pos) == ':') {
+                valueIsBase64 = true;
+                pos++;
+            }
+        }
+        // Then, find the position of the first non-blank character in the line.
+        while (pos < length && lowerLine.charAt(pos) == ' ') {
+            pos++;
+        }
+
+        if (pos >= length) {
+            // We've hit the end of the line with no value.
+            // We'll allow it, but add a warning.
+            warnings.add(WARN_ENTRY_GENERATOR_NO_VALUE_IN_TEMPLATE_LINE.get(
+                    lineNumber + 1, element.getLabel(), elementName));
+        }
+
+        int phase = PARSING_STATIC_TEXT;
+        int previousPhase = PARSING_STATIC_TEXT;
+
+        final List<TemplateTag> tagList = new ArrayList<>();
+        StringBuilder buffer = new StringBuilder();
+
+        for (; pos < length; pos++) {
+            char c = line.charAt(pos);
+            switch (phase) {
+            case PARSING_STATIC_TEXT:
+                switch (c) {
+                case '\\':
+                    phase = PARSING_ESCAPED_CHAR;
+                    previousPhase = PARSING_STATIC_TEXT;
+                    break;
+                case '<':
+                    if (buffer.length() > 0) {
+                        StaticTextTag t = new StaticTextTag();
+                        String[] args = new String[] { buffer.toString() };
+                        t.initializeForBranch(schema, this, branch, args, lineNumber, warnings);
+                        tagList.add(t);
+                        buffer = new StringBuilder();
+                    }
+
+                    phase = PARSING_REPLACEMENT_TAG;
+                    break;
+                case '{':
+                    if (buffer.length() > 0) {
+                        StaticTextTag t = new StaticTextTag();
+                        String[] args = new String[] { buffer.toString() };
+                        t.initializeForBranch(schema, this, branch, args, lineNumber, warnings);
+                        tagList.add(t);
+                        buffer = new StringBuilder();
+                    }
+
+                    phase = PARSING_ATTRIBUTE_TAG;
+                    break;
+                default:
+                    buffer.append(c);
+                }
+                break;
+
+            case PARSING_REPLACEMENT_TAG:
+                switch (c) {
+                case '\\':
+                    phase = PARSING_ESCAPED_CHAR;
+                    previousPhase = PARSING_REPLACEMENT_TAG;
+                    break;
+                case '>':
+                    TemplateTag t =
+                        parseReplacementTag(buffer.toString(), branch, template, lineNumber, tags, warnings);
+                    tagList.add(t);
+                    buffer = new StringBuilder();
+                    phase = PARSING_STATIC_TEXT;
+                    break;
+                default:
+                    buffer.append(c);
+                    break;
+                }
+                break;
+
+            case PARSING_ATTRIBUTE_TAG:
+                switch (c) {
+                case '\\':
+                    phase = PARSING_ESCAPED_CHAR;
+                    previousPhase = PARSING_ATTRIBUTE_TAG;
+                    break;
+                case '}':
+                    TemplateTag t = parseAttributeTag(buffer.toString(), branch, template, lineNumber, warnings);
+                    tagList.add(t);
+                    buffer = new StringBuilder();
+
+                    phase = PARSING_STATIC_TEXT;
+                    break;
+                default:
+                    buffer.append(c);
+                    break;
+                }
+                break;
+
+            default: // PARSING_ESCAPED_CHAR:
+                buffer.append(c);
+                phase = previousPhase;
+                break;
+            }
+        }
+
+        if (phase == PARSING_STATIC_TEXT) {
+            if (buffer.length() > 0) {
+                StaticTextTag t = new StaticTextTag();
+                String[] args = new String[] { buffer.toString() };
+                t.initializeForBranch(schema, this, branch, args, lineNumber, warnings);
+                tagList.add(t);
+            }
+        } else {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_INCOMPLETE_TAG.get(lineNumber + 1));
+        }
+
+        return new TemplateLine(attributeType, lineNumber, tagList, valueIsURL, valueIsBase64);
+    }
+
+    /**
+     * Parses the provided string as a replacement tag. Exactly one of the
+     * branch or template must be null, and the other must be non-null.
+     *
+     * @param tagString
+     *            The string containing the encoded tag.
+     * @param branch
+     *            The branch in which this tag appears.
+     * @param template
+     *            The template in which this tag appears.
+     * @param lineNumber
+     *            The line number on which this tag appears in the template
+     *            file.
+     * @param tags
+     *            The set of defined tags from the template file. Note that this
+     *            does not include the tags that are always registered by
+     *            default.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The replacement tag parsed from the provided string.
+     * @throws DecodeException
+     *             If some problem occurs during processing.
+     */
+    private TemplateTag parseReplacementTag(final String tagString, final Branch branch, final Template template,
+            final int lineNumber, final Map<String, TemplateTag> tags, final List<LocalizableMessage> warnings)
+            throws DecodeException {
+        // The components of the replacement tag will be separated by colons,
+        // with the first being the tag name and the remainder being arguments.
+        final StringTokenizer tokenizer = new StringTokenizer(tagString, ":");
+        final String tagName = tokenizer.nextToken().trim();
+        final String lowerTagName = tagName.toLowerCase();
+
+        TemplateTag tag = getTag(lowerTagName);
+        if (tag == null) {
+            tag = tags.get(lowerTagName);
+            if (tag == null) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_NO_SUCH_TAG.get(tagName, lineNumber + 1));
+            }
+        }
+
+        final List<String> args = new ArrayList<>();
+        while (tokenizer.hasMoreTokens()) {
+            args.add(tokenizer.nextToken().trim());
+        }
+        final String[] arguments = args.toArray(new String[args.size()]);
+
+        TemplateTag newTag;
+        try {
+            newTag = tag.getClass().newInstance();
+        } catch (Exception e) {
+            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_CANNOT_INSTANTIATE_NEW_TAG.get(
+                    tagName, lineNumber + 1, String.valueOf(e)), e);
+        }
+
+        if (branch == null) {
+            newTag.initializeForTemplate(schema, this, template, arguments, lineNumber, warnings);
+        } else if (newTag.allowedInBranch()) {
+            newTag.initializeForBranch(schema, this, branch, arguments, lineNumber, warnings);
+        } else {
+            throw DecodeException.fatalError(
+                    ERR_ENTRY_GENERATOR_TAG_NOT_ALLOWED_IN_BRANCH.get(newTag.getName(), lineNumber + 1));
+        }
+        return newTag;
+    }
+
+    /**
+     * Parses the provided string as an attribute tag. Exactly one of the branch
+     * or template must be null, and the other must be non-null.
+     *
+     * @param tagString
+     *            The string containing the encoded tag.
+     * @param branch
+     *            The branch in which this tag appears.
+     * @param template
+     *            The template in which this tag appears.
+     * @param lineNumber
+     *            The line number on which this tag appears in the template
+     *            file.
+     * @param warnings
+     *            A list into which any warnings identified may be placed.
+     * @return The attribute tag parsed from the provided string.
+     * @throws DecodeException
+     *             If some other problem occurs during processing.
+     */
+    private TemplateTag parseAttributeTag(final String tagString, final Branch branch,
+            final Template template, final int lineNumber, final List<LocalizableMessage> warnings)
+            throws DecodeException {
+        // The attribute tag must have at least one argument, which is the name
+        // of the attribute to reference. It may have a second argument, which
+        // is the number of characters to use from the attribute value. The
+        // arguments will be delimited by colons.
+        final StringTokenizer tokenizer = new StringTokenizer(tagString, ":");
+        final List<String> args = new ArrayList<>();
+        while (tokenizer.hasMoreTokens()) {
+            args.add(tokenizer.nextToken());
+        }
+        final String[] arguments = args.toArray(new String[args.size()]);
+
+        final AttributeValueTag tag = new AttributeValueTag();
+        if (branch != null) {
+            tag.initializeForBranch(schema, this, branch, arguments, lineNumber, warnings);
+        } else {
+            tag.initializeForTemplate(schema, this, template, arguments, lineNumber, warnings);
+        }
+        return tag;
+    }
+
+    /**
+     * Retrieves a file based on the provided path.
+     * <p>
+     * To allow retrieval of a file located in a jar, you must use
+     * {@code getReader()} method instead of this one.
+     * <p>
+     * File is searched successively in two locations :
+     * <ul>
+     * <li>Using the provided path as is.</li>
+     * <li>Using resource path + provided path.</li>
+     * </ul>
+     *
+     * @param filePath
+     *            The path provided for the file, which can be absolute or
+     *            relative.
+     * @return the file, or <code>null</code> if it could not be found.
+     */
+    private File getFile(final String filePath) {
+        File file = new File(filePath);
+        // try raw path first
+        if (file.exists()) {
+            return file;
+        }
+        // try using resource path
+        if (resourcePath != null) {
+            file = new File(resourcePath + File.separator + filePath);
+            if (file.exists()) {
+                return file;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves a reader based on the provided path.
+     * <p>
+     * The path represent a file path either on the file system or in a jar.
+     * File is searched successively in three locations :
+     * <ul>
+     * <li>Using the provided path on the file system.</li>
+     * <li>Using resource path + provided path on the file system.</li>
+     * <li>Using default resources path + provided path on the file system or in
+     * a jar.</li>
+     * </ul>
+     *
+     * @param filePath
+     *            The path provided for the file, which can be absolute or
+     *            relative.
+     * @return A reader on the file, or <code>null</code> if it could not be
+     *         found. It is the responsibility of caller to close the returned
+     *         reader.
+     */
+    BufferedReader getReader(final String filePath) {
+        BufferedReader reader = null;
+        File file = new File(filePath);
+        try {
+            if (file.exists()) {
+                // try raw path first
+                reader = new BufferedReader(new FileReader(file));
+            } else if (resourcePath != null) {
+                // try using resource path
+                file = new File(resourcePath + File.separator + filePath);
+                if (file.exists()) {
+                    reader = new BufferedReader(new FileReader(file));
+                }
+            }
+            if (reader == null) {
+                // try to find in default resources provided
+                final InputStream stream = TemplateFile.class.getClassLoader()
+                        .getResourceAsStream(DEFAULT_RESOURCES_PATH + "/" + filePath);
+                if (stream != null) {
+                    reader = new BufferedReader(new InputStreamReader(stream));
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // Should never happen as we test file existence first.
+            // In any case, nothing to do as we want to return null
+        }
+        return reader;
+    }
+
+    /**
+     * Retrieves the lines of the provided reader, possibly reading them from
+     * memory cache.
+     * <p>
+     * Lines are retrieved from reader at the first call, then cached in memory
+     * for next calls, using the provided identifier.
+     * <p>
+     * Use {@code readFile()} method to avoid caching.
+     *
+     * @param reader
+     *            Reader to parse for lines.
+     * @return a list of lines
+     * @throws IOException
+     *             If a problem occurs while reading the file.
+     */
+    String[] getLines(String identifier, final BufferedReader reader) throws IOException {
+        String[] lines = fileLines.get(identifier);
+        if (lines == null) {
+            lines = readLines(reader).toArray(new String[] {});
+            fileLines.put(identifier, lines);
+        }
+        return lines;
+    }
+
+    /**
+     * Retrieves the lines from the provided reader.
+     *
+     * @param reader
+     *            The reader containing the lines.
+     * @return a list of lines
+     * @throws IOException
+     *             If a problem occurs while reading the lines.
+     */
+    private List<String> readLines(final BufferedReader reader) throws IOException {
+        final List<String> lines = new ArrayList<>();
+        String line;
+        for (int lineNumber = 1; (line = reader.readLine()) != null; lineNumber++) {
+            if (line.startsWith(" ")) {
+                final int lastLineIndex = lines.size() - 1;
+                final String previousLine = lines.get(lastLineIndex);
+                if (lines.isEmpty() || previousLine.isEmpty()) {
+                    throw DecodeException.fatalError(ERR_TEMPLATE_FILE_INVALID_LEADING_SPACE.get(lineNumber, line));
+                }
+                lines.set(lastLineIndex, previousLine + line.substring(1));
+            } else {
+                lines.add(line);
+            }
+        }
+        return lines;
+    }
+
+    /** Iterator on branches that are used to read entries. */
+    private Iterator<Branch> branchesIterator;
+
+    /** Branch from which entries are currently read. */
+    private Branch currentBranch;
+
+    /** Entry to return when calling {@code nextEntry} method. */
+    private TemplateEntry nextEntry;
+
+    /**
+     * Returns {@code true} if there is another generated entry
+     * to return.
+     *
+     * @return {@code true} if another entry can be returned.
+     */
+    boolean hasNext() {
+        if (nextEntry != null) {
+            return true;
+        }
+        while (currentBranch != null) {
+            if (currentBranch.hasNext()) {
+                nextEntry = currentBranch.nextEntry();
+                return true;
+            }
+            currentBranch = branchesIterator.hasNext() ? branchesIterator.next() : null;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the next generated entry.
+     *
+     * @return The next entry.
+     * @throws NoSuchElementException
+     *             If this reader does not contain any more entries.
+     */
+    Entry nextEntry() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+        final Entry entry = nextEntry.toEntry();
+        nextEntry = null;
+        return entry;
+    }
+
+    /**
+     * Represents a branch that should be included in the generated results. A
+     * branch may or may not have subordinate entries.
+     */
+    static final class Branch {
+        /** The DN for this branch entry. */
+        private final DN branchDN;
+
+        /**
+         * The number of entries that should be created below this branch for
+         * each subordinate template.
+         */
+        private final List<Integer> numEntriesPerTemplate;
+
+        /** The names of the subordinate templates for this branch. */
+        private final List<String> subordinateTemplateNames;
+
+        /** The set of subordinate templates for this branch. */
+        private List<Template> subordinateTemplates;
+
+        /** The set of template lines that correspond to the RDN components. */
+        private final List<TemplateLine> rdnLines;
+
+        /** The set of extra lines that should be included in this branch entry. */
+        private final List<TemplateLine> extraLines;
+
+        /** Entry to return when calling {@code nextEntry} method. */
+        private TemplateEntry nextEntry;
+
+        /** Index of subordinate template currently read. */
+        private int currentSubTemplateIndex;
+
+        /**
+         * Creates a new branch with the provided information.
+         *
+         * @param templateFile
+         *            The template file in which this branch appears.
+         * @param branchDN
+         *            The DN for this branch entry.
+         * @param schema
+         *            schema used to create attribute
+         * @throws DecodeException
+         *             if a problem occurs during initialization
+         */
+        Branch(final TemplateFile templateFile, final DN branchDN, final Schema schema) throws DecodeException {
+            this(templateFile, branchDN, schema, new ArrayList<String>(), new ArrayList<Integer>(),
+                    new ArrayList<TemplateLine>());
+        }
+
+        /**
+         * Creates a new branch with the provided information.
+         *
+         * @param templateFile
+         *            The template file in which this branch appears.
+         * @param branchDN
+         *            The DN for this branch entry.
+         * @param schema
+         *            schema used to create attributes
+         * @param subordinateTemplateNames
+         *            The names of the subordinate templates used to generate
+         *            entries below this branch.
+         * @param numEntriesPerTemplate
+         *            The number of entries that should be created below this
+         *            branch for each subordinate template.
+         * @param extraLines
+         *            The set of extra lines that should be included in this
+         *            branch entry.
+         * @throws DecodeException
+         *             if a problem occurs during initialization
+         */
+        Branch(final TemplateFile templateFile, final DN branchDN, final Schema schema,
+                final List<String> subordinateTemplateNames, final List<Integer> numEntriesPerTemplate,
+                final List<TemplateLine> extraLines) throws DecodeException {
+            this.branchDN = branchDN;
+            this.subordinateTemplateNames = subordinateTemplateNames;
+            this.numEntriesPerTemplate = numEntriesPerTemplate;
+            this.extraLines = extraLines;
+
+            // The RDN template lines are based on the DN.
+            final List<LocalizableMessage> warnings = new ArrayList<>();
+            rdnLines = new ArrayList<>();
+            for (final AVA ava : branchDN.rdn()) {
+                final Attribute attribute = ava.toAttribute();
+                for (final ByteString value : attribute.toArray()) {
+                    final List<TemplateTag> tags =
+                            buildTagListForValue(value.toString(), templateFile, schema, warnings);
+                    rdnLines.add(new TemplateLine(attribute.getAttributeDescription().getAttributeType(), 0, tags));
+                }
+            }
+        }
+
+        private List<TemplateTag> buildTagListForValue(final String value, final TemplateFile templateFile,
+                final Schema schema, final List<LocalizableMessage> warnings) throws DecodeException {
+            final StaticTextTag tag = new StaticTextTag();
+            tag.initializeForBranch(schema, templateFile, this, new String[] { value }, 0, warnings);
+            final List<TemplateTag> tags = new ArrayList<>();
+            tags.add(tag);
+            return tags;
+        }
+
+        /**
+         * Performs any necessary processing to ensure that the branch
+         * initialization is completed. In particular, it should make sure that
+         * all referenced subordinate templates actually exist in the template
+         * file.
+         */
+        private void completeBranchInitialization(final Map<String, Template> templates,
+                            boolean generateBranches) throws DecodeException {
+            subordinateTemplates = new ArrayList<>();
+            for (int i = 0; i < subordinateTemplateNames.size(); i++) {
+                subordinateTemplates.add(templates.get(subordinateTemplateNames.get(i).toLowerCase()));
+                if (subordinateTemplates.get(i) == null) {
+                    throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_UNDEFINED_BRANCH_SUBORDINATE.get(
+                            branchDN.toString(), subordinateTemplateNames.get(i)));
+                }
+            }
+
+            nextEntry = buildBranchEntry(generateBranches);
+        }
+
+        DN getBranchDN() {
+            return branchDN;
+        }
+
+        /**
+         * Adds a new subordinate template to this branch. Note that this should
+         * not be used after <CODE>completeBranchInitialization</CODE> has been
+         * called.
+         *
+         * @param name
+         *            The name of the template to use to generate the entries.
+         * @param numEntries
+         *            The number of entries to create based on the template.
+         */
+        void addSubordinateTemplate(final String name, final int numEntries) {
+            subordinateTemplateNames.add(name);
+            numEntriesPerTemplate.add(numEntries);
+        }
+
+        /**
+         * Adds the provided template line to the set of extra lines for this
+         * branch.
+         *
+         * @param line
+         *            The line to add to the set of extra lines for this branch.
+         */
+        void addExtraLine(final TemplateLine line) {
+            extraLines.add(line);
+        }
+
+        /**
+         * Indicates whether this branch contains a reference to the specified
+         * attribute type, either in the RDN components of the DN or in the
+         * extra lines.
+         *
+         * @param attributeType
+         *            The attribute type for which to make the determination.
+         * @return <code>true</code> if the branch does contain the specified
+         *         attribute type, or <code>false</code> if it does not.
+         */
+        boolean hasAttribute(final AttributeType attributeType) {
+            if (branchDN.rdn().getAttributeValue(attributeType) != null) {
+                return true;
+            }
+            for (final TemplateLine line : extraLines) {
+                if (line.getAttributeType().equals(attributeType)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns the entry corresponding to this branch.
+         */
+        private TemplateEntry buildBranchEntry(boolean generateBranches) {
+            final TemplateEntry entry = new TemplateEntry(this);
+            final List<TemplateLine> lines = new ArrayList<>(rdnLines);
+            lines.addAll(extraLines);
+            for (final TemplateLine line : lines) {
+                line.generateLine(entry);
+            }
+            for (int i = 0; i < subordinateTemplates.size(); i++) {
+                subordinateTemplates.get(i).reset(entry.getDN(), numEntriesPerTemplate.get(i));
+            }
+
+            if (!generateBranches) {
+                return null;
+            }
+
+            return entry;
+        }
+
+        /**
+         * Returns {@code true} if there is another generated entry to return.
+         *
+         * @return {@code true} if another entry can be returned.
+         */
+        boolean hasNext() {
+            if (nextEntry != null) {
+                return true;
+            }
+            // get the next entry from current subtemplate
+            if (nextEntry == null) {
+                for (; currentSubTemplateIndex < subordinateTemplates.size(); currentSubTemplateIndex++) {
+                    if (subordinateTemplates.get(currentSubTemplateIndex).hasNext()) {
+                        nextEntry = subordinateTemplates.get(currentSubTemplateIndex).nextEntry();
+                        if (nextEntry != null) {
+                            return true;
+                        }
+                    }
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns the next generated entry.
+         *
+         * @return The next entry.
+         * @throws NoSuchElementException
+         *             If this reader does not contain any more entries.
+         */
+        TemplateEntry nextEntry() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            final TemplateEntry entry = nextEntry;
+            nextEntry = null;
+            return entry;
+        }
+    }
+
+    /**
+     * Represents a template, which is a pattern that may be used to generate
+     * entries. A template may be used either below a branch or below another
+     * template.
+     */
+    static class Template {
+        /**
+         * The attribute types that are used in the RDN for entries generated
+         * using this template.
+         */
+        private final List<AttributeType> rdnAttributes;
+
+        /** The number of entries to create for each subordinate template. */
+        private final List<Integer> numEntriesPerTemplate;
+
+        /** The name for this template. */
+        private final String name;
+
+        /** The names of the subordinate templates below this template. */
+        private final List<String> subTemplateNames;
+
+        /** The subordinate templates below this template. */
+        private List<Template> subTemplates;
+
+        /** The template file that contains this template. */
+        private final TemplateFile templateFile;
+
+        /** The set of template lines for this template. */
+        private final List<TemplateLine> templateLines;
+
+        /**
+         * Creates a new template with the provided information.
+         *
+         * @param templateFile
+         *            The template file that contains this template.
+         * @param name
+         *            The name for this template.
+         * @param rdnAttributes
+         *            The set of attribute types that are used in the RDN for
+         *            entries generated using this template.
+         * @param subordinateTemplateNames
+         *            The names of the subordinate templates below this
+         *            template.
+         * @param numEntriesPerTemplate
+         *            The number of entries to create below each subordinate
+         *            template.
+         * @param templateLines
+         *            The set of template lines for this template.
+         */
+        Template(final TemplateFile templateFile, final String name, final List<AttributeType> rdnAttributes,
+                final List<String> subordinateTemplateNames, final List<Integer> numEntriesPerTemplate,
+                final List<TemplateLine> templateLines) {
+            this.templateFile = templateFile;
+            this.name = name;
+            this.rdnAttributes = rdnAttributes;
+            this.subTemplateNames = subordinateTemplateNames;
+            this.numEntriesPerTemplate = numEntriesPerTemplate;
+            this.templateLines = templateLines;
+        }
+
+        /**
+         * Performs any necessary processing to ensure that the template
+         * initialization is completed. In particular, it should make sure that
+         * all referenced subordinate templates actually exist in the template
+         * file, and that all of the RDN attributes are contained in the
+         * template lines.
+         *
+         * @param templates
+         *            The set of templates defined in the template file.
+         * @throws DecodeException
+         *             If any of the subordinate templates are not defined in
+         *             the template file.
+         */
+        void completeTemplateInitialization(final Map<String, Template> templates) throws DecodeException {
+            subTemplates = new ArrayList<>();
+            for (final String subordinateName : subTemplateNames) {
+                final Template template = templates.get(subordinateName.toLowerCase());
+                if (template == null) {
+                    throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_UNDEFINED_TEMPLATE_SUBORDINATE.get(
+                            this.name, subordinateName));
+                }
+                subTemplates.add(template);
+            }
+            ensureAllRDNAttributesAreDefined();
+        }
+
+        private void ensureAllRDNAttributesAreDefined() throws DecodeException {
+            Set<AttributeType> rdnAttrs = new HashSet<>(rdnAttributes);
+            List<AttributeType> templateAttrs = new ArrayList<>();
+            for (TemplateLine line : templateLines) {
+                templateAttrs.add(line.getAttributeType());
+            }
+            rdnAttrs.removeAll(templateAttrs);
+            if (!rdnAttrs.isEmpty()) {
+                AttributeType t = rdnAttrs.iterator().next();
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TEMPLATE_MISSING_RDN_ATTR.get(
+                        name, t.getNameOrOID()));
+            }
+        }
+
+        String getName() {
+            return name;
+        }
+
+        List<AttributeType> getRDNAttributes() {
+            return rdnAttributes;
+        }
+
+        List<TemplateLine> getTemplateLines() {
+            return templateLines;
+        }
+
+        void addTemplateLine(final TemplateLine line) {
+            templateLines.add(line);
+        }
+
+        /**
+         * Indicates whether this template contains any template lines that
+         * reference the provided attribute type.
+         *
+         * @param attributeType
+         *            The attribute type for which to make the determination.
+         * @return <CODE>true</CODE> if this template contains one or more
+         *         template lines that reference the provided attribute type, or
+         *         <CODE>false</CODE> if not.
+         */
+        boolean hasAttribute(final AttributeType attributeType) {
+            for (final TemplateLine line : templateLines) {
+                if (line.getAttributeType().equals(attributeType)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /** Parent DN of entries to generate for this template. */
+        private DN parentDN;
+
+        /**
+         * Number of entries to generate for this template.
+         * Negative number means infinite generation.
+         */
+        private int numberOfEntries;
+
+        /** Current count of generated entries for this template. */
+        private int entriesCount;
+
+        /** Indicates if current entry has been initialized. */
+        private boolean currentEntryIsInitialized;
+
+        /** Index of current subordinate template to use for current entry. */
+        private int subTemplateIndex;
+
+        /** Entry to return when calling {@code nextEntry} method. */
+        private TemplateEntry nextEntry;
+
+        /**
+         * Reset this template with provided parentDN and number of entries to
+         * generate.
+         * <p>
+         * After a reset, the template can be used again to generate some
+         * entries with a different parent DN and number of entries.
+         *
+         * @param parentDN
+         *            The parent DN of entires to generate for this template.
+         * @param numberOfEntries
+         *            The number of entries to generate for this template.
+         */
+        void reset(final DN parentDN, final int numberOfEntries) {
+            this.parentDN = parentDN;
+            this.numberOfEntries = numberOfEntries;
+            entriesCount = 0;
+            currentEntryIsInitialized = false;
+            subTemplateIndex = 0;
+            nextEntry = null;
+        }
+
+        /**
+         * Returns an entry for this template.
+         *
+         * @return the entry
+         */
+        private TemplateEntry buildTemplateEntry() {
+            templateFile.nextFirstAndLastNames();
+            final TemplateEntry templateEntry = new TemplateEntry(this, parentDN);
+            for (final TemplateLine line : templateLines) {
+                line.generateLine(templateEntry);
+            }
+            for (int i = 0; i < subTemplates.size(); i++) {
+                subTemplates.get(i).reset(templateEntry.getDN(), numEntriesPerTemplate.get(i));
+            }
+            return templateEntry;
+        }
+
+        /**
+         * Returns {@code true} if there is another generated entry to return.
+         *
+         * @return {@code true} if another entry can be returned.
+         */
+        boolean hasNext() {
+            if (nextEntry != null) {
+                return true;
+            }
+            while (entriesCount < numberOfEntries || generateForever()) {
+                // get the template entry
+                if (!currentEntryIsInitialized) {
+                    nextEntry = buildTemplateEntry();
+                    currentEntryIsInitialized = true;
+                    return true;
+                }
+                // get the next entry from current subtemplate
+                if (nextEntry == null) {
+                    for (; subTemplateIndex < subTemplates.size(); subTemplateIndex++) {
+                        if (subTemplates.get(subTemplateIndex).hasNext()) {
+                            nextEntry = subTemplates.get(subTemplateIndex).nextEntry();
+                            if (nextEntry != null) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                // reset for next template entry
+                entriesCount++;
+                currentEntryIsInitialized = false;
+                subTemplateIndex = 0;
+            }
+            return false;
+        }
+
+        private boolean generateForever() {
+            return numberOfEntries < 0;
+        }
+
+        /**
+         * Returns the next generated entry.
+         *
+         * @return The next entry.
+         * @throws NoSuchElementException
+         *             If this reader does not contain any more entries.
+         */
+        TemplateEntry nextEntry() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+            final TemplateEntry entry = nextEntry;
+            nextEntry = null;
+            return entry;
+        }
+    }
+
+    /**
+     * Represents an entry that is generated using a branch or a template.
+     */
+    static class TemplateEntry {
+
+        /** Template entry that represents a null object. */
+        static final TemplateEntry NULL_TEMPLATE_ENTRY = new TemplateEntry(null, null);
+
+        /** The DN for this template entry, if it is known. */
+        private DN dn;
+
+        /**
+         * The DN of the parent entry for this template entry, if it is
+         * available.
+         */
+        private final DN parentDN;
+
+        /**
+         * The set of attributes associated with this template entry, mapped
+         * from the lowercase name of the attribute to the list of generated
+         * values.
+         * A list of template values is never empty in the map, it always has
+         * at least one element.
+         */
+        private final LinkedHashMap<AttributeType, List<TemplateValue>> attributes = new LinkedHashMap<>();
+
+        /**
+         * The template used to generate this entry if it is associated with a
+         * template.
+         */
+        private final Template template;
+
+        /**
+         * Creates a new template entry that will be associated with the
+         * provided branch.
+         *
+         * @param branch
+         *            The branch to use when creating this template entry.
+         */
+        TemplateEntry(final Branch branch) {
+            dn = branch.getBranchDN();
+            template = null;
+            parentDN = null;
+        }
+
+        /**
+         * Creates a new template entry that will be associated with the
+         * provided template.
+         *
+         * @param template
+         *            The template used to generate this entry.
+         * @param parentDN
+         *            The DN of the parent entry for this template entry.
+         */
+        TemplateEntry(final Template template, final DN parentDN) {
+            this.template = template;
+            this.parentDN = parentDN;
+        }
+
+        DN getParentDN() {
+            return parentDN;
+        }
+
+        /**
+         * Retrieves the DN for this template entry, if it is known.
+         *
+         * @return The DN for this template entry if it is known, or
+         *         <CODE>null</CODE> if it cannot yet be determined.
+         */
+        DN getDN() {
+            if (dn == null) {
+                final Collection<AVA> avas = new ArrayList<>();
+                for (final AttributeType attrType : template.getRDNAttributes()) {
+                    final TemplateValue templateValue = getValue(attrType);
+                    if (templateValue == null) {
+                        return null;
+                    }
+                    avas.add(new AVA(attrType, templateValue.getValueAsString()));
+                }
+                dn = parentDN.child(new RDN(avas));
+            }
+            return dn;
+        }
+
+        /**
+         * Retrieves the value for the specified attribute, if defined. If the
+         * specified attribute has multiple values, then the first will be
+         * returned.
+         *
+         * @param attributeType
+         *            The attribute type for which to retrieve the value.
+         * @return The value for the specified attribute, or <CODE>null</CODE>
+         *         if there are no values for that attribute type.
+         */
+        TemplateValue getValue(final AttributeType attributeType) {
+            final List<TemplateValue> values = attributes.get(attributeType);
+            if (values != null) {
+                return values.get(0);
+            }
+            return null;
+        }
+
+        /**
+         * Retrieves the set of values for the specified attribute, if defined.
+         *
+         * @param attributeType
+         *            The attribute type for which to retrieve the set of
+         *            values.
+         * @return The set of values for the specified attribute, or
+         *         <CODE>null</CODE> if there are no values for that attribute
+         *         type.
+         */
+        List<TemplateValue> getValues(AttributeType attributeType) {
+            return attributes.get(attributeType);
+        }
+
+        void addValue(TemplateValue value) {
+            List<TemplateValue> values = attributes.get(value.getAttributeType());
+            if (values == null) {
+                values = new ArrayList<>();
+                attributes.put(value.getAttributeType(), values);
+            }
+            values.add(value);
+        }
+
+        /**
+         * Returns an entry built from this template entry.
+         *
+         * @return an entry
+         */
+        Entry toEntry() {
+            final Entry entry = new LinkedHashMapEntry(getDN());
+
+            for (final AttributeType attributeType : attributes.keySet()) {
+                final List<TemplateValue> valueList = attributes.get(attributeType);
+                final Attribute newAttribute =
+                        new LinkedAttribute(AttributeDescription.create(attributeType));
+                for (final TemplateValue value : valueList) {
+                    newAttribute.add(value.getValueAsString());
+                }
+                entry.addAttribute(newAttribute);
+            }
+            return entry;
+        }
+    }
+
+    /**
+     * Represents a line that may appear in a template or branch. It may contain
+     * any number of tags to be evaluated.
+     */
+    static class TemplateLine {
+
+        /** The attribute type to which this template line corresponds. */
+        private final AttributeType attributeType;
+
+        /**
+         * The line number on which this template line appears in the template
+         * file.
+         */
+        @SuppressWarnings("unused")
+        private final int lineNumber;
+
+        /** The set of tags for this template line. */
+        private final List<TemplateTag> tags;
+
+        /** Whether this line corresponds to an URL value or not. */
+        @SuppressWarnings("unused")
+        private final boolean isURL;
+
+        /** Whether this line corresponds to a base64 encoded value or not. */
+        @SuppressWarnings("unused")
+        private final boolean isBase64;
+
+        /**
+         * Creates a new template line.
+         *
+         * @param attributeType
+         *            The attribute type for this template line.
+         * @param lineNumber
+         *            The line number on which this template line appears in the
+         *            template file.
+         * @param tags
+         *            The set of tags for this template line.
+         */
+        TemplateLine(final AttributeType attributeType, final int lineNumber, final List<TemplateTag> tags) {
+            this(attributeType, lineNumber, tags, false, false);
+        }
+
+        /**
+         * Creates a new template line with URL and base64 flags.
+         *
+         * @param attributeType
+         *            The attribute type for this template line.
+         * @param lineNumber
+         *            The line number on which this template line appears in the
+         *            template file.
+         * @param tags
+         *            The set of tags for this template line.
+         * @param isURL
+         *            Whether this template line's value is an URL or not.
+         * @param isBase64
+         *            Whether this template line's value is Base64 encoded or
+         *            not.
+         */
+        TemplateLine(final AttributeType attributeType, final int lineNumber, final List<TemplateTag> tags,
+                final boolean isURL, final boolean isBase64) {
+            this.attributeType = attributeType;
+            this.lineNumber = lineNumber;
+            this.tags = tags;
+            this.isURL = isURL;
+            this.isBase64 = isBase64;
+        }
+
+        AttributeType getAttributeType() {
+            return attributeType;
+        }
+
+        /**
+         * Generates the content for this template line and places it in the
+         * provided template entry.
+         *
+         * @param templateEntry
+         *            The template entry being generated.
+         * @return The result of generating the template line.
+         */
+        TagResult generateLine(final TemplateEntry templateEntry) {
+            final TemplateValue value = new TemplateValue(this);
+            for (final TemplateTag tag : tags) {
+                final TagResult result = tag.generateValue(templateEntry, value);
+                if (result != TagResult.SUCCESS) {
+                    return result;
+                }
+            }
+            templateEntry.addValue(value);
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Represents a value generated from a template line.
+     */
+    static class TemplateValue {
+
+        /** The generated template value. */
+        private final StringBuilder templateValue;
+
+        /** The template line used to generate this value. */
+        private final TemplateLine templateLine;
+
+        TemplateValue(final TemplateLine templateLine) {
+            this.templateLine = templateLine;
+            templateValue = new StringBuilder();
+        }
+
+        AttributeType getAttributeType() {
+            return templateLine.getAttributeType();
+        }
+
+        /** Returns the generated value as String. */
+        String getValueAsString() {
+            return templateValue.toString();
+        }
+
+        /** Appends the provided string to this template value. */
+        void append(final String s) {
+            templateValue.append(s);
+        }
+
+        /**
+         * Appends the string representation of the provided object to this
+         * template value.
+         */
+        void append(final Object o) {
+            templateValue.append(o);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateTag.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateTag.java
new file mode 100644
index 0000000..dfcabc9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateTag.java
@@ -0,0 +1,1462 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2009 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.TemplateFile.Branch;
+import org.forgerock.opendj.ldif.TemplateFile.Template;
+import org.forgerock.opendj.ldif.TemplateFile.TemplateEntry;
+import org.forgerock.opendj.ldif.TemplateFile.TemplateValue;
+
+/**
+ * Represents a tag that may be used in a template line when generating entries.
+ * It can be used to generate content.
+ *
+ * @see EntryGenerator
+ */
+abstract class TemplateTag {
+
+    /**
+     * Retrieves the name for this tag.
+     */
+    abstract String getName();
+
+    /**
+     * Indicates whether this tag is allowed for use in the extra lines for
+     * branches.
+     */
+    abstract boolean allowedInBranch();
+
+    /**
+     * Performs any initialization for this tag that may be needed while parsing
+     * a branch definition.
+     *
+     * @param schema
+     *            The schema used to create attributes.
+     * @param templateFile
+     *            The template file in which this tag is used.
+     * @param branch
+     *            The branch in which this tag is used.
+     * @param arguments
+     *            The set of arguments provided for this tag.
+     * @param lineNumber
+     *            The line number on which this tag appears in the template
+     *            file.
+     * @param warnings
+     *            A list into which any appropriate warning messages may be
+     *            placed.
+     * @throws DecodeException
+     *             If tag cannot be initialized.
+     */
+    void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+            int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+        // No implementation required by default.
+    }
+
+    /**
+     * Performs any initialization for this tag that may be needed while parsing
+     * a template definition.
+     *
+     * @param schema
+     *            The schema used to create attributes.
+     * @param templateFile
+     *            The template file in which this tag is used.
+     * @param template
+     *            The template in which this tag is used.
+     * @param tagArguments
+     *            The set of arguments provided for this tag.
+     * @param lineNumber
+     *            The line number on which this tag appears in the template
+     *            file.
+     * @param warnings
+     *            A list into which any appropriate warning messages may be
+     *            placed.
+     */
+    abstract void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+            String[] tagArguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException;
+
+    /**
+     * Performs any initialization for this tag that may be needed when starting
+     * to generate entries below a new parent.
+     *
+     * @param parentEntry
+     *            The entry below which the new entries will be generated.
+     */
+    void initializeForParent(TemplateEntry parentEntry) {
+        // No implementation required by default.
+    }
+
+    /**
+     * Check for an attribute type in a branch or in a template.
+     *
+     * @param attrType
+     *            The attribute type to check for.
+     * @param branch
+     *            The branch that contains the type, or {@code null}
+     * @param template
+     *            The template that contains the type, or {@code null}
+     * @return true if either the branch or the template has the provided
+     *         attribute
+     */
+    final boolean hasAttributeTypeInBranchOrTemplate(AttributeType attrType, Branch branch,
+            Template template) {
+        return (branch != null && branch.hasAttribute(attrType))
+                || (template != null && template.hasAttribute(attrType));
+    }
+
+    /**
+     * Generates the content for this tag by appending it to the provided tag.
+     *
+     * @param templateEntry
+     *            The entry for which this tag is being generated.
+     * @param templateValue
+     *            The template value to which the generated content should be
+     *            appended.
+     * @return The result of generating content for this tag.
+     */
+    abstract TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue);
+
+    /**
+     * Represents the result of tag generation.
+     */
+    static enum TagResult {
+        SUCCESS, FAILURE
+    }
+
+    /**
+     * Tag used to reference the value of a specified attribute
+     * already defined in the entry.
+     */
+    static class AttributeValueTag extends TemplateTag {
+
+        /** The attribute type that specifies which value should be used. */
+        private AttributeType attributeType;
+
+        /** The maximum number of characters to include from the value. */
+        private int numCharacters;
+
+        AttributeValueTag() {
+            attributeType = null;
+            numCharacters = 0;
+        }
+
+        @Override
+        String getName() {
+            return "AttributeValue";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(schema, branch, null, arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(schema, null, template, arguments, lineNumber);
+        }
+
+        private void initialize(Schema schema, Branch branch, Template template, String[] arguments, int lineNumber)
+                throws DecodeException {
+            if (arguments.length < 1 || arguments.length > 2) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                        lineNumber, 1, 2, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+
+            attributeType = schema.getAttributeType(arguments[0].toLowerCase());
+            if (!hasAttributeTypeInBranchOrTemplate(attributeType, branch, template)) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_UNDEFINED_ATTRIBUTE.get(arguments[0], lineNumber);
+                throw DecodeException.fatalError(message);
+            }
+
+            if (arguments.length == 2) {
+                try {
+                    numCharacters = Integer.parseInt(arguments[1]);
+                    if (numCharacters < 0) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(
+                                numCharacters, 0, getName(), lineNumber);
+                        throw DecodeException.fatalError(message);
+                    }
+                } catch (NumberFormatException nfe) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[1],
+                            getName(), lineNumber);
+                    throw DecodeException.fatalError(message);
+                }
+            } else {
+                numCharacters = 0;
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            TemplateValue value = templateEntry.getValue(attributeType);
+            if (value == null) {
+                // This is fine -- we just won't append anything.
+                return TagResult.SUCCESS;
+            }
+
+            if (numCharacters > 0) {
+                String valueString = value.getValueAsString();
+                if (valueString.length() > numCharacters) {
+                    templateValue.append(valueString.substring(0, numCharacters));
+                } else {
+                    templateValue.append(valueString);
+                }
+            } else {
+                templateValue.append(value.getValueAsString());
+            }
+
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to include the DN of the current entry in the attribute value.
+     */
+    static class DNTag extends TemplateTag {
+
+        /** The number of DN components to include. */
+        private int numComponents;
+
+        @Override
+        String getName() {
+            return "DN";
+        }
+
+        @Override
+        final boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        final void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        @Override
+        final void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        private void initialize(String[] arguments, int lineNumber)
+                throws DecodeException {
+            if (arguments.length == 0) {
+                numComponents = 0;
+            } else if (arguments.length == 1) {
+                try {
+                    numComponents = Integer.parseInt(arguments[0]);
+                } catch (NumberFormatException nfe) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[0],
+                            getName(), lineNumber);
+                    throw DecodeException.fatalError(message);
+                }
+            } else {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                        lineNumber, 0, 1, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            return generateValue(templateEntry, templateValue, ",");
+        }
+
+        final TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue,
+                String separator) {
+            DN dn = templateEntry.getDN();
+            if (dn == null || dn.isRootDN()) {
+                return TagResult.SUCCESS;
+            }
+
+            String dnAsString = "";
+            if (numComponents == 0) {
+                // Return the DN of the entry
+                dnAsString = dn.toString();
+            } else if (numComponents > 0) {
+                // Return the first numComponents RDNs of the DN
+                dnAsString = dn.localName(numComponents).toString();
+            } else {
+                // numComponents is negative
+                // Return the last numComponents RDNs of the DN
+                dnAsString = dn.parent(dn.size() - Math.abs(numComponents)).toString();
+            }
+            // If expected separator is not standard separator
+            // Then substitute expected to standard
+            if (!separator.equals(",")) {
+                dnAsString = dnAsString.replaceAll(",", separator);
+            }
+            templateValue.append(dnAsString);
+
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to provide values from a text file. The file should have one
+     * value per line. Access to the values may be either at random or in
+     * sequential order.
+     */
+    static class FileTag extends TemplateTag {
+        /**
+         * Indicates whether the values should be selected sequentially or at
+         * random.
+         */
+        private boolean isSequential;
+
+        /** The index used for sequential access. */
+        private int nextIndex;
+
+        /** The random number generator for this tag. */
+        private Random random;
+
+        /** The lines read from the file. */
+        private String[] fileLines;
+
+        @Override
+        String getName() {
+            return "File";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber);
+        }
+
+        private void initialize(TemplateFile templateFile, String[] arguments, int lineNumber)
+                throws DecodeException {
+            random = templateFile.getRandom();
+
+            // There must be at least one argument, and possibly two.
+            if (arguments.length < 1 || arguments.length > 2) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                        lineNumber, 1, 2, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+
+            // The first argument should be the path to the file.
+            final String filePath = arguments[0];
+            try (BufferedReader dataReader = templateFile.getReader(filePath)) {
+                if (dataReader == null) {
+                    throw DecodeException.fatalError(
+                        ERR_ENTRY_GENERATOR_TAG_CANNOT_FIND_FILE.get(filePath, getName(), lineNumber));
+                }
+
+                // See if the file has already been read into memory. If not, then read it.
+                fileLines = templateFile.getLines(filePath, dataReader);
+            } catch (IOException e) {
+                throw DecodeException.fatalError(
+                    ERR_ENTRY_GENERATOR_TAG_CANNOT_READ_FILE.get(filePath, getName(), lineNumber, e), e);
+            }
+
+            // If there is a second argument, then it should be either "sequential" or "random".
+            // If there isn't one, then we should assume "random".
+            if (arguments.length == 2) {
+                if ("sequential".equalsIgnoreCase(arguments[1])) {
+                    isSequential = true;
+                    nextIndex = 0;
+                } else if ("random".equalsIgnoreCase(arguments[1])) {
+                    isSequential = false;
+                } else {
+                    throw DecodeException.fatalError(
+                        ERR_ENTRY_GENERATOR_TAG_INVALID_FILE_ACCESS_MODE.get(arguments[1], getName(), lineNumber));
+                }
+            } else {
+                isSequential = false;
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            if (isSequential) {
+                templateValue.append(fileLines[nextIndex++]);
+                if (nextIndex >= fileLines.length) {
+                    nextIndex = 0;
+                }
+            } else {
+                templateValue.append(fileLines[random.nextInt(fileLines.length)]);
+            }
+
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to include a first name in the attribute value.
+     */
+    static class FirstNameTag extends NameTag {
+        @Override
+        String getName() {
+            return "First";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            templateValue.append(templateFile.getFirstName());
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to include a GUID in the attribute value.
+     */
+    static class GUIDTag extends TemplateTag {
+
+        @Override
+        String getName() {
+            return "GUID";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        private void initialize(String[] arguments, int lineNumber) throws DecodeException {
+            if (arguments.length != 0) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        0, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            templateValue.append(UUID.randomUUID().toString());
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Base tag to use to base presence of one attribute on the absence/presence
+     * of another attribute and/or attribute value.
+     */
+    static abstract class IfTag extends TemplateTag {
+        /** The attribute type for which to make the determination. */
+        AttributeType attributeType;
+
+        /** The value for which to make the determination. */
+        String assertionValue;
+
+        @Override
+        final boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        final void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(schema, branch, null, arguments, lineNumber);
+        }
+
+        @Override
+        final void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(schema, null, template, arguments, lineNumber);
+        }
+
+        private void initialize(Schema schema, Branch branch, Template template, String[] arguments, int lineNumber)
+                throws DecodeException {
+            if (arguments.length < 1 || arguments.length > 2) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                        lineNumber, 1, 2, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+
+            attributeType = schema.getAttributeType(arguments[0].toLowerCase());
+            if (!hasAttributeTypeInBranchOrTemplate(attributeType, branch, template)) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_UNDEFINED_ATTRIBUTE.get(arguments[0], lineNumber);
+                throw DecodeException.fatalError(message);
+            }
+
+            if (arguments.length == 2) {
+                assertionValue = arguments[1];
+            } else {
+                assertionValue = null;
+            }
+        }
+    }
+
+    /**
+     * Tag used to base presence of one attribute on the absence of another
+     * attribute and/or attribute value.
+     */
+    static class IfAbsentTag extends IfTag {
+        @Override
+        String getName() {
+            return "IfAbsent";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            List<TemplateValue> values = templateEntry.getValues(attributeType);
+            if (values == null) {
+                return TagResult.SUCCESS;
+            }
+            if (assertionValue == null) {
+                return TagResult.FAILURE;
+            }
+            for (TemplateValue v : values) {
+                if (assertionValue.equals(v.getValueAsString())) {
+                    return TagResult.FAILURE;
+                }
+            }
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to base presence of one attribute on the presence of another
+     * attribute and/or attribute value.
+     */
+    static class IfPresentTag extends IfTag {
+        @Override
+        String getName() {
+            return "IfPresent";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            List<TemplateValue> values = templateEntry.getValues(attributeType);
+            if (values == null || values.isEmpty()) {
+                return TagResult.FAILURE;
+            }
+
+            if (assertionValue == null) {
+                return TagResult.SUCCESS;
+            }
+            for (TemplateValue v : values) {
+                if (assertionValue.equals(v.getValueAsString())) {
+                    return TagResult.SUCCESS;
+                }
+            }
+            return TagResult.FAILURE;
+        }
+    }
+
+    /**
+     * Tag used to include a last name in the attribute value.
+     */
+    static class LastNameTag extends NameTag {
+        @Override
+        String getName() {
+            return "Last";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            templateValue.append(templateFile.getLastName());
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to select a value from a pre-defined list, optionally defining
+     * weights for each value that can impact the likelihood of a given item
+     * being selected.
+     * <p>
+     * The items to include in the list should be specified as arguments to the
+     * tag. If the argument ends with a semicolon followed by an integer, then
+     * that will be the weight for that particular item. If no weight is given,
+     * then the weight for that item will be assumed to be one.
+     */
+    static class ListTag extends TemplateTag {
+        /** The ultimate cumulative weight. */
+        private int cumulativeWeight;
+
+        /** The set of cumulative weights for the list items. */
+        private int[] valueWeights;
+
+        /** The set of values in the list. */
+        private String[] valueStrings;
+
+        /** The random number generator for this tag. */
+        private Random random;
+
+        @Override
+        String getName() {
+            return "List";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber, warnings);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber, warnings);
+        }
+
+        private void initialize(TemplateFile templateFile, String[] arguments, int lineNumber,
+                List<LocalizableMessage> warnings) throws DecodeException {
+            if (arguments.length == 0) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TAG_LIST_NO_ARGUMENTS.get(lineNumber));
+            }
+            valueStrings = new String[arguments.length];
+            valueWeights = new int[arguments.length];
+            cumulativeWeight = 0;
+            random = templateFile.getRandom();
+
+            for (int i = 0; i < arguments.length; i++) {
+                String value = arguments[i];
+                int weight = 1;
+                int semicolonPos = value.lastIndexOf(';');
+                if (semicolonPos >= 0) {
+                    try {
+                        weight = Integer.parseInt(value.substring(semicolonPos + 1));
+                        value = value.substring(0, semicolonPos);
+                    } catch (NumberFormatException e) {
+                        warnings.add(WARN_ENTRY_GENERATOR_TAG_LIST_INVALID_WEIGHT.get(lineNumber, value));
+                    }
+                }
+                cumulativeWeight += weight;
+                valueStrings[i] = value;
+                valueWeights[i] = cumulativeWeight;
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            int selectedWeight = random.nextInt(cumulativeWeight) + 1;
+            for (int i = 0; i < valueWeights.length; i++) {
+                if (selectedWeight <= valueWeights[i]) {
+                    templateValue.append(valueStrings[i]);
+                    break;
+                }
+            }
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Base tag to use to include a first name or last name in the attribute
+     * value.
+     */
+    static abstract class NameTag extends TemplateTag {
+        /** The template file with which this tag is associated. */
+        TemplateFile templateFile;
+
+        @Override
+        final boolean allowedInBranch() {
+            return false;
+        }
+
+        @Override
+        final void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            this.templateFile = templateFile;
+
+            if (arguments.length != 0) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        0, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+        }
+    }
+
+    /**
+     * Base tag to use to include the DN of the parent entry in the attribute value.
+     */
+    static abstract class ParentTag extends TemplateTag {
+
+        @Override
+        final boolean allowedInBranch() {
+            return false;
+        }
+
+        @Override
+        final void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            if (arguments.length != 0) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        0, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+        }
+    }
+
+    /**
+     * Tag used to include the DN of the parent entry in the attribute value.
+     */
+    static class ParentDNTag extends ParentTag {
+
+        @Override
+        String getName() {
+            return "ParentDN";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            DN parentDN = templateEntry.getParentDN();
+            if (parentDN == null || parentDN.isRootDN()) {
+                return TagResult.SUCCESS;
+            }
+            templateValue.append(parentDN);
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to indicate that a value should only be included in a percentage
+     * of the entries.
+     */
+    static class PresenceTag extends TemplateTag {
+        /**
+         * The percentage of the entries in which this attribute value should
+         * appear.
+         */
+        private int percentage;
+
+        /** The random number generator for this tag. */
+        private Random random;
+
+        @Override
+        String getName() {
+            return "Presence";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber);
+        }
+
+        private void initialize(TemplateFile templateFile, String[] arguments, int lineNumber)
+                throws DecodeException {
+            random = templateFile.getRandom();
+
+            if (arguments.length != 1) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        1, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+
+            try {
+                percentage = Integer.parseInt(arguments[0]);
+                if (percentage < 0) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(percentage,
+                            0, getName(), lineNumber);
+                    throw DecodeException.fatalError(message);
+                }
+                if (percentage > 100) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_ABOVE_UPPER_BOUND.get(percentage,
+                            100, getName(), lineNumber);
+                    throw DecodeException.fatalError(message);
+                }
+            } catch (NumberFormatException nfe) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[0],
+                        getName(), lineNumber);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            int intValue = random.nextInt(100);
+            return intValue < percentage ? TagResult.SUCCESS : TagResult.FAILURE;
+        }
+    }
+
+    /**
+     * Tag used to generate random values. It has a number of subtypes based on
+     * the type of information that should be generated, including:
+     * <UL>
+     * <LI>alpha:length</LI>
+     * <LI>alpha:minlength:maxlength</LI>
+     * <LI>numeric:length</LI>
+     * <LI>numeric:minvalue:maxvalue</LI>
+     * <LI>numeric:minvalue:maxvalue:format</LI>
+     * <LI>alphanumeric:length</LI>
+     * <LI>alphanumeric:minlength:maxlength</LI>
+     * <LI>chars:characters:length</LI>
+     * <LI>chars:characters:minlength:maxlength</LI>
+     * <LI>hex:length</LI>
+     * <LI>hex:minlength:maxlength</LI>
+     * <LI>base64:length</LI>
+     * <LI>base64:minlength:maxlength</LI>
+     * <LI>month</LI>
+     * <LI>month:maxlength</LI>
+     * <LI>telephone</LI>
+     * </UL>
+     */
+    static class RandomTag extends TemplateTag {
+
+        static enum RandomType {
+            /**
+             * Generates values from a fixed number of characters from a given
+             * character set.
+             */
+            CHARS_FIXED,
+            /**
+             * Generates values from a variable number of characters from a given
+             * character set.
+             */
+            CHARS_VARIABLE,
+            /**
+             * Generates numbers.
+             */
+            NUMERIC,
+            /**
+             * Generates months (as text).
+             */
+            MONTH,
+            /**
+             * Generates telephone numbers.
+             */
+            TELEPHONE
+        }
+
+        /**
+         * The character set that will be used for alphabetic characters.
+         */
+        public static final char[] ALPHA_CHARS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
+
+        /**
+         * The character set that will be used for numeric characters.
+         */
+        public static final char[] NUMERIC_CHARS = "01234567890".toCharArray();
+
+        /**
+         * The character set that will be used for alphanumeric characters.
+         */
+        public static final char[] ALPHANUMERIC_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
+
+        /**
+         * The character set that will be used for hexadecimal characters.
+         */
+        public static final char[] HEX_CHARS = "01234567890abcdef".toCharArray();
+
+        /**
+         * The character set that will be used for base64 characters.
+         */
+        public static final char[] BASE64_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+                + "01234567890+/").toCharArray();
+
+        /**
+         * The set of month names that will be used.
+         */
+        public static final String[] MONTHS = { "January", "February", "March", "April", "May", "June", "July",
+            "August", "September", "October", "November", "December" };
+
+        /** The character set that should be used to generate the values. */
+        private char[] characterSet;
+
+        /** The decimal format used to format numeric values. */
+        private DecimalFormat decimalFormat;
+
+        /** The number of characters between the minimum and maximum length (inclusive). */
+        private int lengthRange = 1;
+
+        /** The maximum number of characters to include in the value. */
+        private int maxLength;
+
+        /** The minimum number of characters to include in the value. */
+        private int minLength;
+
+        /** The type of random value that should be generated. */
+        private RandomType randomType;
+
+        /** The maximum numeric value that should be generated. */
+        private long maxValue;
+
+        /** The minimum numeric value that should be generated. */
+        private long minValue;
+
+        /**
+         * The number of values between the minimum and maximum value
+         * (inclusive).
+         */
+        private long valueRange = 1L;
+
+        /** The random number generator for this tag. */
+        private Random random;
+
+        @Override
+        String getName() {
+            return "Random";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber, warnings);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(templateFile, arguments, lineNumber, warnings);
+        }
+
+        private void initialize(TemplateFile templateFile, String[] arguments, int lineNumber,
+                List<LocalizableMessage> warnings) throws DecodeException {
+            random = templateFile.getRandom();
+
+            // There must be at least one argument, to specify the type of
+            // random value to generate.
+            if (arguments == null || arguments.length == 0) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_NO_RANDOM_TYPE_ARGUMENT.get(lineNumber);
+                throw DecodeException.fatalError(message);
+            }
+
+            int numArgs = arguments.length;
+            String randomTypeString = arguments[0].toLowerCase();
+
+            if ("alpha".equals(randomTypeString)) {
+                characterSet = ALPHA_CHARS;
+                decodeLength(arguments, 1, lineNumber, warnings);
+            } else if ("numeric".equals(randomTypeString)) {
+                if (numArgs == 2) {
+                    randomType = RandomType.CHARS_FIXED;
+                    characterSet = NUMERIC_CHARS;
+
+                    try {
+                        minLength = Integer.parseInt(arguments[1]);
+
+                        if (minLength < 0) {
+                            LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(
+                                    minLength, 0, getName(), lineNumber);
+                            throw DecodeException.fatalError(message);
+                        } else if (minLength == 0) {
+                            LocalizableMessage message = WARN_ENTRY_GENERATOR_TAG_WARNING_EMPTY_VALUE.get(lineNumber);
+                            warnings.add(message);
+                        }
+                    } catch (NumberFormatException nfe) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[1],
+                                getName(), lineNumber);
+                        throw DecodeException.fatalError(message, nfe);
+                    }
+                } else if (numArgs == 3 || numArgs == 4) {
+                    randomType = RandomType.NUMERIC;
+
+                    if (numArgs == 4) {
+                        try {
+                            decimalFormat = new DecimalFormat(arguments[3]);
+                        } catch (Exception e) {
+                            LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_FORMAT_STRING.get(
+                                    arguments[3], getName(), lineNumber);
+                            throw DecodeException.fatalError(message, e);
+                        }
+                    } else {
+                        decimalFormat = null;
+                    }
+
+                    try {
+                        minValue = Long.parseLong(arguments[1]);
+                    } catch (NumberFormatException nfe) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[1],
+                                getName(), lineNumber);
+                        throw DecodeException.fatalError(message, nfe);
+                    }
+
+                    try {
+                        maxValue = Long.parseLong(arguments[2]);
+                        if (maxValue < minValue) {
+                            LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(
+                                    maxValue, minValue, getName(), lineNumber);
+                            throw DecodeException.fatalError(message);
+                        }
+
+                        valueRange = maxValue - minValue + 1;
+                    } catch (NumberFormatException nfe) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[2],
+                                getName(), lineNumber);
+                        throw DecodeException.fatalError(message, nfe);
+                    }
+                } else {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                            lineNumber, 2, 4, numArgs);
+                    throw DecodeException.fatalError(message);
+                }
+            } else if ("alphanumeric".equals(randomTypeString)) {
+                characterSet = ALPHANUMERIC_CHARS;
+                decodeLength(arguments, 1, lineNumber, warnings);
+            } else if ("chars".equals(randomTypeString)) {
+                if (numArgs < 3 || numArgs > 4) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                            lineNumber, 3, 4, numArgs);
+                    throw DecodeException.fatalError(message);
+                }
+
+                characterSet = arguments[1].toCharArray();
+                decodeLength(arguments, 2, lineNumber, warnings);
+            } else if ("hex".equals(randomTypeString)) {
+                characterSet = HEX_CHARS;
+                decodeLength(arguments, 1, lineNumber, warnings);
+            } else if ("base64".equals(randomTypeString)) {
+                characterSet = BASE64_CHARS;
+                decodeLength(arguments, 1, lineNumber, warnings);
+            } else if ("month".equals(randomTypeString)) {
+                randomType = RandomType.MONTH;
+
+                if (numArgs == 1) {
+                    maxLength = 0;
+                } else if (numArgs == 2) {
+                    try {
+                        maxLength = Integer.parseInt(arguments[1]);
+                        if (maxLength <= 0) {
+                            LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(
+                                    maxLength, 1, getName(), lineNumber);
+                            throw DecodeException.fatalError(message);
+                        }
+                    } catch (NumberFormatException nfe) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(arguments[1],
+                                getName(), lineNumber);
+                        throw DecodeException.fatalError(message, nfe);
+                    }
+                } else {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                            lineNumber, 1, 2, numArgs);
+                    throw DecodeException.fatalError(message);
+                }
+            } else if ("telephone".equals(randomTypeString)) {
+                randomType = RandomType.TELEPHONE;
+            } else {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_UNKNOWN_RANDOM_TYPE.get(lineNumber,
+                        randomTypeString);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        /**
+         * Decodes the information in the provided argument list as either a
+         * single integer specifying the number of characters, or two integers
+         * specifying the minimum and maximum number of characters.
+         *
+         * @param arguments
+         *            The set of arguments to be processed.
+         * @param startPos
+         *            The position at which the first legth value should appear
+         *            in the argument list.
+         * @param lineNumber
+         *            The line number on which the tag appears in the template
+         *            file.
+         * @param warnings
+         *            A list into which any appropriate warning messages may be
+         *            placed.
+         */
+        private void decodeLength(String[] arguments, int startPos, int lineNumber, List<LocalizableMessage> warnings)
+                throws DecodeException {
+            int numArgs = arguments.length - startPos + 1;
+
+            if (numArgs == 2) {
+                // There is a fixed number of characters in the value.
+                randomType = RandomType.CHARS_FIXED;
+
+                try {
+                    minLength = Integer.parseInt(arguments[startPos]);
+
+                    if (minLength < 0) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(minLength,
+                                0, getName(), lineNumber);
+                        throw DecodeException.fatalError(message);
+                    } else if (minLength == 0) {
+                        LocalizableMessage message = WARN_ENTRY_GENERATOR_TAG_WARNING_EMPTY_VALUE.get(lineNumber);
+                        warnings.add(message);
+                    }
+                } catch (NumberFormatException nfe) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(
+                            arguments[startPos], getName(), lineNumber);
+                    throw DecodeException.fatalError(message, nfe);
+                }
+            } else if (numArgs == 3) {
+                // There are minimum and maximum lengths.
+                randomType = RandomType.CHARS_VARIABLE;
+
+                try {
+                    minLength = Integer.parseInt(arguments[startPos]);
+
+                    if (minLength < 0) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(minLength,
+                                0, getName(), lineNumber);
+                        throw DecodeException.fatalError(message);
+                    }
+                } catch (NumberFormatException nfe) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(
+                            arguments[startPos], getName(), lineNumber);
+                    throw DecodeException.fatalError(message, nfe);
+                }
+
+                try {
+                    maxLength = Integer.parseInt(arguments[startPos + 1]);
+                    lengthRange = maxLength - minLength + 1;
+
+                    if (maxLength < minLength) {
+                        LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND.get(maxLength,
+                                minLength, getName(), lineNumber);
+                        throw DecodeException.fatalError(message);
+                    } else if (maxLength == 0) {
+                        LocalizableMessage message = WARN_ENTRY_GENERATOR_TAG_WARNING_EMPTY_VALUE.get(lineNumber);
+                        warnings.add(message);
+                    }
+                } catch (NumberFormatException nfe) {
+                    LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(
+                            arguments[startPos + 1], getName(), lineNumber);
+                    throw DecodeException.fatalError(message, nfe);
+                }
+            } else {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(getName(),
+                        lineNumber, startPos + 1, startPos + 2, numArgs);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            switch (randomType) {
+            case CHARS_FIXED:
+                for (int i = 0; i < minLength; i++) {
+                    templateValue.append(characterSet[random.nextInt(characterSet.length)]);
+                }
+                break;
+
+            case CHARS_VARIABLE:
+                int numChars = random.nextInt(lengthRange) + minLength;
+                for (int i = 0; i < numChars; i++) {
+                    templateValue.append(characterSet[random.nextInt(characterSet.length)]);
+                }
+                break;
+
+            case NUMERIC:
+                long randomValue = (random.nextLong() & 0x7FFFFFFFFFFFFFFFL) % valueRange + minValue;
+                if (decimalFormat != null) {
+                    templateValue.append(decimalFormat.format(randomValue));
+                } else {
+                    templateValue.append(randomValue);
+                }
+                break;
+
+            case MONTH:
+                String month = MONTHS[random.nextInt(MONTHS.length)];
+                if ((maxLength == 0) || (month.length() <= maxLength)) {
+                    templateValue.append(month);
+                } else {
+                    templateValue.append(month.substring(0, maxLength));
+                }
+                break;
+
+            case TELEPHONE:
+                templateValue.append("+1 ");
+                for (int i = 0; i < 3; i++) {
+                    templateValue.append(NUMERIC_CHARS[random.nextInt(NUMERIC_CHARS.length)]);
+                }
+                templateValue.append(' ');
+                for (int i = 0; i < 3; i++) {
+                    templateValue.append(NUMERIC_CHARS[random.nextInt(NUMERIC_CHARS.length)]);
+                }
+                templateValue.append(' ');
+                for (int i = 0; i < 4; i++) {
+                    templateValue.append(NUMERIC_CHARS[random.nextInt(NUMERIC_CHARS.length)]);
+                }
+                break;
+            }
+
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to include the RDN of the current entry in the attribute value.
+     */
+    static class RDNTag extends TemplateTag {
+
+        @Override
+        String getName() {
+            return "RDN";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        private void initialize(String[] arguments, int lineNumber) throws DecodeException {
+            if (arguments.length != 0) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        0, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            DN dn = templateEntry.getDN();
+            if (dn != null && !dn.isRootDN()) {
+                templateValue.append(dn.rdn());
+            }
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag that is used to include a sequentially-incrementing integer in the
+     * generated values.
+     */
+    static class SequentialTag extends TemplateTag {
+        /** Indicates whether to reset for each parent. */
+        private boolean resetOnNewParents = true;
+
+        /** The initial value in the sequence. */
+        private int initialValue;
+
+        /** The next value in the sequence. */
+        private int nextValue;
+
+        @Override
+        String getName() {
+            return "Sequential";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        private void initialize(String[] arguments, int lineNumber) throws DecodeException {
+            if (arguments.length < 0 || arguments.length > 2) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT.get(
+                        getName(), lineNumber, 0, 2, arguments.length));
+            }
+            if (arguments.length > 0) {
+                initializeValue(arguments[0], lineNumber);
+                if (arguments.length > 1) {
+                    initializeReset(arguments[1], lineNumber);
+                }
+            }
+        }
+
+        private void initializeReset(String resetValue, int lineNumber) throws DecodeException {
+            if ("true".equalsIgnoreCase(resetValue)) {
+                resetOnNewParents = true;
+            } else if ("false".equalsIgnoreCase(resetValue)) {
+                resetOnNewParents = false;
+            } else {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_BOOLEAN.get(
+                        resetValue, getName(), lineNumber));
+            }
+        }
+
+        private void initializeValue(String value, int lineNumber) throws DecodeException {
+            try {
+                initialValue = Integer.parseInt(value);
+            } catch (NumberFormatException nfe) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER.get(
+                        value, getName(), lineNumber));
+            }
+            nextValue = initialValue;
+        }
+
+        @Override
+        void initializeForParent(TemplateEntry parentEntry) {
+            if (resetOnNewParents) {
+                nextValue = initialValue;
+            }
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            templateValue.append(nextValue++);
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag that is used to hold static text (i.e., text that appears outside of
+     * any tag).
+     */
+    static class StaticTextTag extends TemplateTag {
+
+        /** The static text to include. */
+        private String text = "";
+
+        @Override
+        String getName() {
+            return "StaticText";
+        }
+
+        @Override
+        boolean allowedInBranch() {
+            return true;
+        }
+
+        @Override
+        void initializeForBranch(Schema schema, TemplateFile templateFile, Branch branch, String[] arguments,
+                int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        @Override
+        void initializeForTemplate(Schema schema, TemplateFile templateFile, Template template,
+                String[] arguments, int lineNumber, List<LocalizableMessage> warnings) throws DecodeException {
+            initialize(arguments, lineNumber);
+        }
+
+        private void initialize(String[] arguments, int lineNumber) throws DecodeException {
+            if (arguments.length != 1) {
+                LocalizableMessage message = ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT.get(getName(), lineNumber,
+                        1, arguments.length);
+                throw DecodeException.fatalError(message);
+            }
+            text = arguments[0];
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            templateValue.append(text);
+            return TagResult.SUCCESS;
+        }
+    }
+
+    /**
+     * Tag used to include the DN of the current entry in the attribute value,
+     * with underscores in place of the commas.
+     */
+    static class UnderscoreDNTag extends DNTag {
+
+        @Override
+        String getName() {
+            return "_DN";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            return generateValue(templateEntry, templateValue, "_");
+        }
+    }
+
+    /**
+     * Tag used to include the DN of the parent entry in the attribute value,
+     * with underscores in place of commas.
+     */
+    static class UnderscoreParentDNTag extends ParentTag {
+
+        @Override
+        String getName() {
+            return "_ParentDN";
+        }
+
+        @Override
+        TagResult generateValue(TemplateEntry templateEntry, TemplateValue templateValue) {
+            DN parentDN = templateEntry.getParentDN();
+            if (parentDN == null || parentDN.isRootDN()) {
+                return TagResult.SUCCESS;
+            }
+            templateValue.append(parentDN.rdn());
+            for (int i = 1; i < parentDN.size(); i++) {
+                templateValue.append("_");
+                templateValue.append(parentDN.parent(i).rdn());
+            }
+
+            return TagResult.SUCCESS;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/package-info.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/package-info.java
new file mode 100644
index 0000000..8c5b38b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes and interfaces for reading and writing LDIF.
+ */
+package org.forgerock.opendj.ldif;
+
diff --git a/opendj-sdk/opendj-core/src/main/javadoc/overview.html b/opendj-sdk/opendj-core/src/main/javadoc/overview.html
new file mode 100644
index 0000000..3ff7810
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/javadoc/overview.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+ 
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+ 
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+ 
+  Copyright 2011-2015 ForgeRock AS.
+ -->
+<html>
+<body>
+    The OpenDJ SDK for Java provides a high performance easy to use
+    library of classes and interfaces for accessing and implementing
+    LDAP Directory Services as defined in <a
+      href="http://tools.ietf.org/html/rfc4510">RFC 4510</a>.
+    <br>
+    <h1>Getting Started</h1>
+    The following example shows how the OpenDJ SDK may be used to
+    connect to a directory server, authenticate, and then perform a
+    search. The search results are output as LDIF to the standard
+    output:
+    <br>
+    <table width="100%">
+      <tbody>
+        <tr>
+         <td>
+          <pre>    // Create an LDIF writer which will write the search results to stdout.
+    final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+    Connection connection = null;
+    try
+    {
+      // Connect and bind to the server.
+      final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", 1389);
+
+      connection = factory.getConnection();
+      connection.bind(userName, password);
+
+      // Read the entries and output them as LDIF.
+      final ConnectionEntryReader reader = connection.search(baseDN, scope, filter, attributes);
+      while (reader.hasNext())
+      {
+        if (reader.isEntry())
+        {
+          // Got an entry.
+          final SearchResultEntry entry = reader.readEntry();
+          writer.writeComment("Search result entry: " + entry.getName().toString());
+          writer.writeEntry(entry);
+        }
+        else
+        {
+          // Got a continuation reference.
+          final SearchResultReference ref = reader.readReference();
+          writer.writeComment("Search result reference: " + ref.getURIs().toString());
+        }
+      }
+      writer.flush();
+    }
+    catch (final Exception e)
+    {
+      // Handle exceptions...
+      System.err.println(e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+    }</pre>
+       </td>
+        </tr>
+      </tbody>
+    </table>
+    <br>
+    <h1>Creating Connections</h1>
+    The following classes can be used to create and manage connections to
+    LDAP directory servers:
+    <ul>
+      <li>{@link org.forgerock.opendj.ldap.LDAPConnectionFactory}</li>
+      <li>{@link org.forgerock.opendj.ldap.Connection}</li>
+      <li>{@link org.forgerock.opendj.ldap.Connections}</li>
+    </ul>
+    <br>
+    <h1>Creating Requests</h1>
+    The following classes can be used to create LDAP requests:
+    <ul>
+      <li>{@link org.forgerock.opendj.ldap.requests.Requests}</li>
+      <li>{@link org.forgerock.opendj.ldap.requests.Request}</li>
+    </ul>
+    <br>
+    <h1>Using Controls</h1>
+    Common LDAP control implementations can be found in
+    {@link org.forgerock.opendj.ldap.controls}.
+    <br>
+    <h1>Core Types</h1>
+    The following classes and interfaces represent core types:
+    <ul>
+      <li>{@link org.forgerock.opendj.ldap.AttributeDescription}</li>
+      <li>{@link org.forgerock.opendj.ldap.Attribute}</li>
+      <li>{@link org.forgerock.opendj.ldap.DN}</li>
+      <li>{@link org.forgerock.opendj.ldap.Entry}</li>
+      <li>{@link org.forgerock.opendj.ldap.Filter}</li>
+    </ul>
+    <br>
+@see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
+      Directory Access Protocol (LDAP): The Protocol </a>
+@see org.forgerock.opendj.ldap
+</body>
+</html>
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
new file mode 100644
index 0000000..f2cf12d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
@@ -0,0 +1,1649 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2010 Sun Microsystems, Inc.
+# Portions copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2014 Manuel Gaupp
+
+ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE=Unable to retrieve \
+ approximate matching rule %s used as the default for the %s attribute syntax. \
+ Approximate matching will not be allowed by default for attributes with this \
+ syntax
+ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE=Unable to retrieve \
+ equality matching rule %s used as the default for the %s attribute syntax. \
+ Equality matching will not be allowed by default for attributes with this \
+ syntax
+ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE=Unable to retrieve \
+ ordering matching rule %s used as the default for the %s attribute syntax. \
+ Ordering matches will not be allowed by default for attributes with this \
+ syntax
+ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE=Unable to retrieve \
+ substring matching rule %s used as the default for the %s attribute syntax. \
+ Substring matching will not be allowed by default for attributes with this \
+ syntax
+WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN=The provided value "%s" is not \
+ allowed for attributes with a Boolean syntax.  The only allowed values are \
+ 'TRUE' and 'FALSE'
+WARN_ATTR_SYNTAX_BIT_STRING_TOO_SHORT=The provided value "%s" is too \
+ short to be a valid bit string.  A bit string must be a series of binary \
+ digits surrounded by single quotes and followed by a capital letter B
+WARN_ATTR_SYNTAX_BIT_STRING_NOT_QUOTED=The provided value "%s" is not \
+ a valid bit string because it is not surrounded by single quotes and followed \
+ by a capital letter B
+WARN_ATTR_SYNTAX_BIT_STRING_INVALID_BIT=The provided value "%s" is \
+ not a valid bit string because '%s' is not a valid binary digit
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH=The provided value "%s" \
+ is not a valid country string because the length is not exactly two characters
+ERR_ATTR_SYNTAX_COUNTRY_STRING_NO_VALID_ISO_CODE=The provided value "%s" \
+ is not a valid ISO 3166 country code
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS=The provided value "%s" is \
+ not a valid delivery method value because it does not contain any elements
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT=The provided value \
+ "%s" is not a valid delivery method value because "%s" is not a valid method
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT=The provided value "%s" \
+ is too short to be a valid generalized time value
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR=The provided value \
+ "%s" is not a valid generalized time value because the '%s' character is not \
+ allowed in the century or year specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid month \
+ specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid day \
+ specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid hour \
+ specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid minute \
+ specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid second \
+ specification
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET=The provided value \
+ "%s" is not a valid generalized time value because "%s" is not a valid GMT \
+ offset
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR=The provided value \
+ "%s" is not a valid generalized time value because it contains an invalid \
+ character '%s' at position %d
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR=The provided value "%s" could not \
+ be parsed as a valid distinguished name because character '%c' at position %d \
+ is not allowed in an attribute name
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME=The provided value "%s" could not be \
+ parsed as a valid distinguished name because it contained an RDN containing \
+ an empty attribute name
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME=The provided value "%s" could \
+ not be parsed as a valid distinguished name because the last non-space \
+ character was part of the attribute name '%s'
+ERR_ATTR_SYNTAX_DN_NO_EQUAL=The provided value "%s" could not be \
+ parsed as a valid distinguished name because the next non-space character \
+ after attribute name "%s" should have been an equal sign but instead was '%c'
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT=The provided value "%s" could \
+ not be parsed as a valid distinguished name because an attribute value \
+ started with an octothorpe (#) but was not followed by a positive multiple of \
+ two hexadecimal digits
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT=The provided value "%s" could not \
+ be parsed as a valid distinguished name because an attribute value started \
+ with an octothorpe (#) but contained a character %c that was not a valid \
+ hexadecimal digit
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE=The provided value "%s" \
+ could not be parsed as a valid distinguished name because an unexpected \
+ failure occurred while attempting to parse an attribute value from one of the \
+ RDN components:  "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE=The provided value "%s" could not \
+ be parsed as a valid distinguished name because one of the RDN components \
+ included a quoted value that did not have a corresponding closing quotation \
+ mark
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID=The provided value "%s" \
+ could not be parsed as a valid distinguished name because one of the RDN \
+ components included a value with an escaped hexadecimal digit that was not \
+ followed by a second hexadecimal digit
+WARN_ATTR_SYNTAX_INTEGER_INITIAL_ZERO=The provided value "%s" could \
+ not be parsed as a valid integer because the first digit may not be zero \
+ unless it is the only digit
+WARN_ATTR_SYNTAX_INTEGER_INVALID_CHARACTER=The provided value "%s" \
+ could not be parsed as a valid integer because character '%c' at position %d \
+ is not allowed in an integer value
+WARN_ATTR_SYNTAX_INTEGER_EMPTY_VALUE=The provided value "%s" could \
+ not be parsed as a valid integer because it did not contain any digits
+WARN_ATTR_SYNTAX_INTEGER_DASH_NEEDS_VALUE=The provided value "%s" \
+ could not be parsed as a valid integer because it contained only a dash not \
+ followed by an integer value
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS=The provided value \
+ "%s" could not be parsed as an attribute type description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+WARN_ATTR_SYNTAX_IA5_ILLEGAL_CHARACTER=The provided value "%s" \
+ cannot be parsed as a valid IA5 string because it contains an illegal \
+ character "%s" that is not allowed in the IA5 (ASCII) character set
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY=The provided value is not a valid \
+ telephone number because it is empty or null
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS=The provided value "%s" is not a \
+ valid telephone number because strict telephone number checking is enabled \
+ and the value does not start with a plus sign in compliance with the ITU-T \
+ E.123 specification
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR=The provided value "%s" is not \
+ a valid telephone number because strict telephone number checking is enabled \
+ and the character %s at position %d is not allowed by the ITU-T E.123 \
+ specification
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS=The provided value "%s" is not a \
+ valid telephone number because it does not contain any numeric digits
+WARN_ATTR_SYNTAX_NUMERIC_STRING_ILLEGAL_CHAR=The provided value \
+ "%s" is not a valid numeric string because it contained character %s at \
+ position %d that was neither a digit nor a space
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE=The provided value is not \
+ a valid numeric string because it did not contain any characters.  A numeric \
+ string value must contain at least one numeric digit or space
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS=The provided \
+ value "%s" could not be parsed as an attribute syntax description because an \
+ open parenthesis was expected at position %d but instead a '%s' character was \
+ found
+WARN_ATTR_SYNTAX_PRINTABLE_STRING_EMPTY_VALUE=The provided value \
+ could not be parsed as a printable string because it was null or empty.  A \
+ printable string must contain at least one character
+WARN_ATTR_SYNTAX_PRINTABLE_STRING_ILLEGAL_CHARACTER=The provided \
+ value "%s" could not be parsed as a printable string because it contained an \
+ invalid character %s at position %d
+WARN_ATTR_SYNTAX_SUBSTRING_ONLY_WILDCARD=The provided value "*" \
+ could not be parsed as a substring assertion because it consists only of a \
+ wildcard character and zero-length substrings are not allowed
+WARN_ATTR_SYNTAX_SUBSTRING_CONSECUTIVE_WILDCARDS=The provided \
+ value "%s" could not be parsed as a substring assertion because it contains \
+ consecutive wildcard characters at position %d and zero-length substrings are \
+ not allowed
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT=The provided value %s is too \
+ short to be a valid UTC time value
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR=The provided value %s is not a \
+ valid UTC time value because the %s character is not allowed in the century \
+ or year specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH=The provided value %s is not \
+ a valid UTC time value because %s is not a valid month specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY=The provided value %s is not a \
+ valid UTC time value because %s is not a valid day specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR=The provided value %s is not a \
+ valid UTC time value because %s is not a valid hour specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE=The provided value %s is not \
+ a valid UTC time value because %s is not a valid minute specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR=The provided value %s is not a \
+ valid UTC time value because it contains an invalid character %s at position \
+ %d
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND=The provided value %s is not \
+ a valid UTC time value because %s is not a valid second specification
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET=The provided value %s is not \
+ a valid UTC time value because %s is not a valid GMT offset
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE=The provided value %s could \
+ not be parsed as a valid UTC time:  %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS=The provided value \
+ "%s" could not be parsed as a DIT content rule description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS=The provided \
+ value "%s" could not be parsed as a name form description because an open \
+ parenthesis was expected at position %d but instead a '%c' character was \
+ found
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS=The provided value "%s" \
+ could not be parsed as a matching rule description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX=The provided value "%s" could not be \
+ parsed as a matching rule description because it does not specify the \
+ attribute syntax with which it is associated
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS=The provided value \
+ "%s" could not be parsed as a matching rule use description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR=The provided value "%s" could not be \
+ parsed as a matching rule description because it does not specify the set of \
+ attribute types that may be used with the associated OID
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS=The provided value \
+ "%s" could not be parsed as a DIT structure rule description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM=The provided value "%s" could \
+ not be parsed as a DIT structure rule description because it referenced an \
+ unknown name form %s
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID=The provided value "%s" could \
+ not be parsed as a DIT structure rule description because it referenced an \
+ unknown rule ID %d for a superior DIT structure rule
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM=The provided value "%s" could not \
+ be parsed as a DIT structure rule description because it did not specify the \
+ name form for the rule
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT=The provided value "%s" is too short \
+ to be a valid telex number value
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE=The provided value "%s" does not \
+ hold a valid telex number because a character %s at position %d was not a \
+ valid printable string character
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR=The provided value "%s" does not \
+ hold a valid telex number because character %s at position %d was neither a \
+ valid printable string character nor a dollar sign to separate the telex \
+ number components
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED=The provided value "%s" does not \
+ hold a valid telex number because the end of the value was found before three \
+ dollar-delimited printable strings could be read
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY=The provided value could not be \
+ parsed as a valid facsimile telephone number because it was empty
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE=The provided value "%s" \
+ could not be parsed as a valid facsimile telephone number because character \
+ %s at position %d was not a valid printable string character
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR=The provided value "%s" \
+ could not be parsed as a valid facsimile telephone number because it ends \
+ with a dollar sign, but that dollar sign should have been followed by a fax \
+ parameter
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER=The provided value "%s" \
+ could not be parsed as a valid facsimile telephone number because the string \
+ "%s" between positions %d and %d was not a valid fax parameter
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN=The provided value "%s" could \
+ not be parsed as a valid name and optional UID value because an error \
+ occurred while trying to parse the DN portion:  %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT=The provided value \
+ "%s" could not be parsed as a valid name and optional UID value because the \
+ UID portion contained an illegal binary digit %s at position %d
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY=The provided value could not be \
+ parsed as a valid teletex terminal identifier because it was empty
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE=The provided value "%s" \
+ could not be parsed as a valid teletex terminal identifier because character \
+ %s at position %d was not a valid printable string character
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR=The provided value "%s" \
+ could not be parsed as a valid teletex terminal identifier because it ends \
+ with a dollar sign, but that dollar sign should have been followed by a TTX \
+ parameter
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON=The provided value "%s" \
+ could not be parsed as a valid teletex terminal identifier because the \
+ parameter string does not contain a colon to separate the name from the value
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER=The provided value "%s" \
+ could not be parsed as a valid teletex terminal identifier because the string \
+ "%s" is not a valid TTX parameter name
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE=The provided value could \
+ not be parsed as an other mailbox value because it was empty
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE=The provided value "%s" \
+ could not be parsed as an other mailbox value because there was no mailbox \
+ type before the dollar sign
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR=The provided value \
+ "%s" could not be parsed as an other mailbox value because the mailbox type \
+ contained an illegal character %s at position %d
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX=The provided value "%s" \
+ could not be parsed as an other mailbox value because there was no mailbox \
+ after the dollar sign
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR=The provided value \
+ "%s" could not be parsed as an other mailbox value because the mailbox \
+ contained an illegal character %s at position %d
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR=The provided value "%s" could not \
+ be parsed as a guide value because the criteria portion %s contained an \
+ illegal character %c at position %d
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN=The provided value "%s" \
+ could not be parsed as a guide value because the criteria portion %s did not \
+ contain a close parenthesis that corresponded to the initial open parenthesis
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK=The provided value "%s" \
+ could not be parsed as a guide value because the criteria portion %s started \
+ with a question mark but was not followed by the string "true" or "false"
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR=The provided value "%s" could not be \
+ parsed as a guide value because the criteria portion %s did not contain a \
+ dollar sign to separate the attribute type from the match type
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR=The provided value "%s" could not be \
+ parsed as a guide value because the criteria portion %s did not specify an \
+ attribute type before the dollar sign
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE=The provided value "%s" could \
+ not be parsed as a guide value because the criteria portion %s did not \
+ specify a match type after the dollar sign
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE=The provided value "%s" \
+ could not be parsed as a guide value because the criteria portion %s had an \
+ invalid match type starting at position %d
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP=The provided value "%s" \
+ could not be parsed as an enhanced guide value because it did not have an \
+ octothorpe (#) character to separate the criteria from the scope
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE=The provided value "%s" could \
+ not be parsed as an enhanced guide value because no scope was provided after \
+ the final octothorpe (#) character
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE=The provided value "%s" \
+ could not be parsed as an enhanced guide value because the specified scope %s \
+ was invalid
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA=The provided value "%s" \
+ could not be parsed as an enhanced guide value because it did not specify any \
+ criteria between the octothorpe (#) characters
+WARN_ATTR_SYNTAX_UUID_INVALID_LENGTH=The provided value "%s" has \
+ an invalid length for a UUID.  All UUID values must have a length of exactly \
+ 36 bytes, but the provided value had a length of %d bytes
+WARN_ATTR_SYNTAX_UUID_EXPECTED_DASH=The provided value "%s" should \
+ have had a dash at position %d, but the character '%s' was found instead
+WARN_ATTR_SYNTAX_UUID_EXPECTED_HEX=The provided value "%s" should \
+ have had a hexadecimal digit at position %d, but the character '%s' was found \
+ instead
+ERR_ATTR_SYNTAX_DIRECTORYSTRING_INVALID_ZEROLENGTH_VALUE=The \
+ operation attempted to assign a zero-length value to an attribute with the \
+ directory string syntax
+ERR_ATTR_SYNTAX_AUTHPW_INVALID_SCHEME_CHAR=The provided \
+ authPassword value had an invalid scheme character at position %d
+ERR_ATTR_SYNTAX_AUTHPW_NO_SCHEME=The provided authPassword value \
+ had a zero-length scheme element
+ERR_ATTR_SYNTAX_AUTHPW_NO_SCHEME_SEPARATOR=The provided \
+ authPassword value was missing the separator character or had an illegal \
+ character between the scheme and authInfo elements
+ERR_ATTR_SYNTAX_AUTHPW_INVALID_AUTH_INFO_CHAR=The provided \
+ authPassword value had an invalid authInfo character at position %d
+ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_INFO=The provided authPassword \
+ value had a zero-length authInfo element
+ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_INFO_SEPARATOR=The provided \
+ authPassword value was missing the separator character or had an illegal \
+ character between the authInfo and authValue elements
+ERR_EMR_INTFIRSTCOMP_FIRST_COMPONENT_NOT_INT=The provided value \
+ "%s" could not be parsed by the integer first component matching rule because \
+ the first component does not appear to be an integer value
+ERR_ATTR_SYNTAX_USERPW_NO_VALUE=No value was given to decode by \
+ the user password attribute syntax
+ERR_ATTR_SYNTAX_USERPW_NO_OPENING_BRACE=Unable to decode the \
+ provided value according to the user password syntax because the value does \
+ not start with the opening curly brace ("{") character
+ERR_ATTR_SYNTAX_USERPW_NO_CLOSING_BRACE=Unable to decode the \
+ provided value according to the user password syntax because the value does \
+ not contain a closing curly brace ("}") character
+ERR_ATTR_SYNTAX_USERPW_NO_SCHEME=Unable to decode the provided \
+ value according to the user password syntax because the value does not \
+ contain a storage scheme name
+WARN_ATTR_SYNTAX_ILLEGAL_INTEGER=The provided value %s is not \
+ allowed for attributes with a Integer syntax
+ERR_ATTR_SYNTAX_AUTHPW_INVALID_AUTH_VALUE_CHAR=The provided \
+ authPassword value had an invalid authValue character at position %d
+ERR_ATTR_SYNTAX_AUTHPW_NO_AUTH_VALUE=The provided authPassword \
+ value had a zero-length authValue element
+ERR_ATTR_SYNTAX_AUTHPW_INVALID_TRAILING_CHAR=The provided \
+ authPassword value had an invalid trailing character at position %d
+WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_SUPERIOR_USAGE=The definition \
+ for attribute type %s is invalid because its attribute usage %s is not the \
+ same as the usage for its superior type %s
+WARN_ATTR_SYNTAX_ATTRTYPE_NONCOLLECTIVE_FROM_COLLECTIVE=The \
+ definition for attribute type %s is invalid because it is not defined as a \
+ collective type but the superior type %s is collective
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL=The DIT \
+ content rule "%s" is not valid because it prohibits the use of attribute type \
+ %s which is required by the associated structural object class %s
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY=The DIT content \
+ rule "%s" is not valid because it prohibits the use of attribute type %s \
+ which is required by the associated auxiliary object class %s
+WARN_ATTR_SYNTAX_ATTRTYPE_COLLECTIVE_IS_OPERATIONAL=The definition \
+ for attribute type %s is invalid because it is declared COLLECTIVE but does \
+ not have a usage of userApplications
+WARN_ATTR_SYNTAX_ATTRTYPE_NO_USER_MOD_NOT_OPERATIONAL=The \
+ definition for attribute type %s is invalid because it is declared \
+ NO-USER-MODIFICATION but does not have an operational usage
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR=The \
+ provided value %s is not a valid generalized time value because it contains \
+ illegal character %s in the fraction component
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION=The provided \
+ value %s is not a valid generalized time value because it does not contain at \
+ least one digit after the period to use as the fractional component
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO=The provided \
+ value %s is not a valid generalized time value because it does not end with \
+ 'Z' or a time zone offset
+WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME=The provided value \
+ %s is not a valid generalized time value because it represents an invalid \
+ time (e.g., a date that does not exist):  %s
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR=The provided \
+ value "%s" could not be parsed as a valid distinguished name because an \
+ attribute value started with a character at position %d that needs to be escaped
+WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_VALUE=The provided value \
+  "%s" cannot be parsed as a valid regex syntax because it does not match  \
+  the pattern "%s"
+WARN_ATTR_SYNTAX_LDAPSYNTAX_REGEX_INVALID_PATTERN=The definition for the \
+ syntax with OID "%s" could not be parsed as a regex syntax because the provided \
+ regex pattern "%s" is invalid
+ERR_ATTR_SYNTAX_NAME_FORM_NO_REQUIRED_ATTR=The provided value \
+ "%s" could not be parsed as a name form description because it does not \
+ specify a required naming attribute
+ERR_ATTR_SYNTAX_UNKNOWN_SUB_SYNTAX=The definition for the \
+ syntax with OID %s declared that it should have a substitution syntax with \
+ OID %s. No such syntax is defined
+ERR_ATTR_SYNTAX_CYCLIC_SUB_SYNTAX=The definition for the \
+ syntax with OID %s declared a cyclic substitution.
+WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_INVALID_VALUE=The provided value \
+  "%s" cannot be parsed because it is not allowed by enumeration syntax \
+  with OID "%s"
+WARN_ATTR_SYNTAX_LDAPSYNTAX_ENUM_DUPLICATE_VALUE=The provided value \
+  "%s" cannot be parsed as an enumeration syntax  because it contains a \
+  duplicate value "%s" at position %d
+ERR_ATTR_SYNTAX_EMPTY_VALUE=The provided value could not be \
+ parsed because it was empty or contained only whitespace
+ERR_ATTR_SYNTAX_EXPECTED_OPEN_PARENTHESIS=The provided value \
+ "%s" could not be parsed because an open parenthesis was expected at \
+ position %d but instead a '%s' character was found
+WARN_ATTR_TYPE_UNKNOWN=No attribute type with name or \
+  OID "%s" exists in the schema
+WARN_OBJECTCLASS_UNKNOWN=No object class with name or \
+  OID "%s" exists in the schema
+WARN_MR_UNKNOWN=No matching rule with name or \
+  OID "%s" exists in the schema
+WARN_MRU_UNKNOWN=No matching rule use with name or \
+  OID "%s" exists in the schema
+WARN_DCR_UNKNOWN=No DIT content rule with name or \
+  OID "%s" exists in the schema
+WARN_DSR_UNKNOWN=No DIT structure rule with ID "%s" exists \
+  in the schema
+WARN_NAMEFORM_UNKNOWN=No name form with name or \
+  OID "%s" exists in the schema
+WARN_SYNTAX_UNKNOWN=No syntax with OID "%s" exists in the \
+  schema
+WARN_NAME_AMBIGUOUS=Multiple OID with name %s exists in the schema
+WARN_ATTR_TYPE_AMBIGUOUS=Multiple attribute types with name %s \
+  exists in the schema
+WARN_OBJECTCLASS_AMBIGUOUS=Multiple object classes with name \
+  %s exists in the schema
+WARN_MR_AMBIGUOUS=Multiple matching rules with name %s \
+  exists in the schema
+WARN_MRU_AMBIGUOUS=Multiple matching rule uses with name %s \
+  exists in the schema
+WARN_DCR_AMBIGUOUS=Multiple DIT content rules with name %s \
+  exists in the schema
+WARN_NAMEFORM_AMBIGUOUS=Multiple name forms with name %s \
+  exists in the schema
+WARN_ATTR_SYNTAX_SUBSTRING_NO_WILDCARDS=The provided \
+ value "%s" could not be parsed as a substring assertion because it does not \
+ contain any wildcard characters
+WARN_ATTR_SYNTAX_SUBSTRING_EMPTY=The provided value \
+ could not be parsed as a substring assertion because it is zero-length
+ERR_SYNTAX_VALIDATION_FAIL=Validation of syntax definition %s \
+ failed and will be removed from the schema: %s
+ERR_OC_VALIDATION_FAIL=Validation of object class definition %s \
+ failed and will be removed from the schema: %s
+ERR_ATTR_TYPE_VALIDATION_FAIL=Validation of attribute type \
+ definition %s failed and will be removed from the schema: %s
+ERR_MR_VALIDATION_FAIL=Validation of matching rule definition %s \
+ failed and will be removed from the schema: %s
+ERR_MRU_VALIDATION_FAIL=Validation of matching rule use definition \
+ %s failed and will be removed from the schema: %s
+ERR_DCR_VALIDATION_FAIL=Validation of DIT content rule definition \
+ %s failed and will be removed from the schema: %s
+ERR_DSR_VALIDATION_FAIL=Validation of DIT structure rule definition \
+ %s failed and will be removed from the schema: %s
+ERR_NAMEFORM_VALIDATION_FAIL=Validation of name form definition %s \
+ failed and will be removed from the schema: %s
+ERR_NO_SUBSCHEMA_SUBENTRY_ATTR=The entry %s does not include \
+ a subschemaSubentry attribute
+ERR_RDN_TYPE_NOT_FOUND=The RDN "%s" could not be parsed due to the \
+ following reason: %s
+ERR_RDN_TRAILING_GARBAGE=The RDN "%s" could not be parsed because it \
+  contained trailing content after the RDN: "%s"
+ERR_RDN_DUPLICATE_AVA_TYPES=An RDN contained multiple components \
+  having the same attribute type "%s"
+ERR_RDN_NO_AVAS=An RDN must contain at least one attribute type and value
+ERR_DN_TYPE_NOT_FOUND=The DN "%s" could not be parsed due to the \
+ following reason: %s
+ERR_ATTRIBUTE_DESCRIPTION_EMPTY=The attribute description \
+ "%s" could not be parsed because it was empty
+ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER=The attribute description \
+ "%s" could not be parsed because it contains an invalid character "%c" at \
+ position %d
+ERR_ATTRIBUTE_DESCRIPTION_NO_TYPE=The attribute description \
+ "%s" could not be parsed because it does not contain an attribute type name
+ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION=The attribute description \
+ "%s" could not be parsed because it contains a zero length option
+ERR_ATTRIBUTE_DESCRIPTION_INTERNAL_WHITESPACE=The attribute \
+ description "%s" could not be parsed because it contains internal white space
+ERR_INVALID_SUBSCHEMA_SUBENTRY_ATTR=The entry %s includes \
+ a subschemaSubentry attribute but it contains an invalid distinguished \
+ name "%s": %s
+WARN_GSER_PATTERN_NO_MATCH=The GSER value does not contain a String matching \
+ the pattern %s at the current position: %s
+WARN_GSER_NO_VALID_SEPARATOR=The GSER value does not contain a separator at \
+ the current position: %s
+WARN_GSER_NO_VALID_STRING=The GSER value does not contain a valid String value \
+ at the current position: %s
+WARN_GSER_NO_VALID_INTEGER=The GSER value does not contain a valid integer \
+ value at the current position: %s
+WARN_GSER_NO_VALID_IDENTIFIER=The GSER value does not contain a valid \
+ identifier at the current position: %s
+WARN_GSER_SPACE_CHAR_EXPECTED=The GSER value does not contain a whitespace \
+ character at the current position: %s
+WARN_GSER_NO_VALID_IDENTIFIEDCHOICE=The GSER value does not contain a valid \
+ IdentifiedChoiceValue at the current position: %s
+ERR_MR_CERTIFICATE_MATCH_PARSE_ERROR=The value could not be parsed as an X.509 \
+ certificate: "%s"
+ERR_MR_CERTIFICATE_MATCH_INVALID_DN=The provided value "%s" could not be \
+ parsed as a valid distinguished name because an error occurred while trying \
+ to parse the DN portion:  %s
+ERR_MR_CERTIFICATE_MATCH_IDENTIFIER_NOT_FOUND=The identifier "%s" could not be \
+ found at the correct position
+ERR_MR_CERTIFICATE_MATCH_EXPECTED_END=The GSER value contains additional \
+ characters at the end of the assertion
+ERR_MR_CERTIFICATE_MATCH_GSER_INVALID=An error occurred while parsing the GSER \
+ String: "%s"
+ERR_SYNTAX_CERTIFICATE_NOTVALID=The provided value is not a valid X.509 \
+ Certificate: %s
+ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V23=The provided value is not a valid X.509 \
+ Certificate because "%s" is only valid in X.509 v2/v3
+ERR_SYNTAX_CERTIFICATE_ONLY_VALID_V3=The provided value is not a valid X.509 \
+ Certificate because "%s" is only valid in X.509 v3
+ERR_SYNTAX_CERTIFICATE_INVALID_VERSION=The provided value is not a valid X.509 \
+ Certificate because it contains an invalid version number (%d)
+ERR_SYNTAX_CERTIFICATE_INVALID_DER=The provided value is not a valid X.509 \
+ Certificate because it contains invalid DER encodings
+ERR_SYNTAX_CERTIFICATE_NO_ELEMENT_EXPECTED=The provided value is not a valid X.509 \
+ Certificate because it contains additional ASN.1 elements
+#
+# Core messages
+#
+ERR_SCHEMA_CONFLICTING_ATTRIBUTE_OID=Unable to register attribute \
+ type %s with the server schema because its OID %s conflicts with the OID of \
+ an existing attribute type %s
+ERR_SCHEMA_CONFLICTING_SYNTAX_OID=Unable to register attribute \
+ syntax %s with the server schema because its OID %s conflicts with the OID of \
+ an existing syntax %s
+ERR_SCHEMA_CONFLICTING_MR_OID=Unable to register matching rule %s \
+ with the server schema because its OID %s conflicts with the OID of an \
+ existing matching rule %s
+ERR_SCHEMA_CONFLICTING_MATCHING_RULE_USE=Unable to register matching \
+ rule use %s with the server schema because its matching rule %s conflicts \
+ with the matching rule for an existing matching rule use %s
+ERR_SCHEMA_CONFLICTING_DIT_STRUCTURE_RULE_ID=Unable to register DIT \
+ structure rule %s with the server schema because its rule ID %d conflicts \
+ with the rule ID for an existing DIT structure rule %s
+ERR_SCHEMA_CONFLICTING_NAME_FORM_OID=Unable to register name form %s \
+ with the server schema because its OID %s conflicts with the OID for an \
+ existing name form %s
+INFO_RESULT_UNDEFINED=Undefined
+INFO_RESULT_SUCCESS=Success
+INFO_RESULT_OPERATIONS_ERROR=Operations Error
+INFO_RESULT_PROTOCOL_ERROR=Protocol Error
+INFO_RESULT_TIME_LIMIT_EXCEEDED=Time Limit Exceeded
+INFO_RESULT_SIZE_LIMIT_EXCEEDED=Size Limit Exceeded
+INFO_RESULT_COMPARE_FALSE=Compare False
+INFO_RESULT_COMPARE_TRUE=Compare True
+INFO_RESULT_AUTH_METHOD_NOT_SUPPORTED=Authentication Method Not Supported
+INFO_RESULT_STRONG_AUTH_REQUIRED=Strong Authentication Required
+INFO_RESULT_REFERRAL=Referral
+INFO_RESULT_ADMIN_LIMIT_EXCEEDED=Administrative Limit Exceeded
+INFO_RESULT_UNAVAILABLE_CRITICAL_EXTENSION=Unavailable Critical Extension
+INFO_RESULT_CONFIDENTIALITY_REQUIRED=Confidentiality Required
+INFO_RESULT_SASL_BIND_IN_PROGRESS=SASL Bind in Progress
+INFO_RESULT_NO_SUCH_ATTRIBUTE=No Such Attribute
+INFO_RESULT_UNDEFINED_ATTRIBUTE_TYPE=Undefined Attribute Type
+INFO_RESULT_INAPPROPRIATE_MATCHING=Inappropriate Matching
+INFO_RESULT_CONSTRAINT_VIOLATION=Constraint Violation
+INFO_RESULT_ATTRIBUTE_OR_VALUE_EXISTS=Attribute or Value Exists
+INFO_RESULT_INVALID_ATTRIBUTE_SYNTAX=Invalid Attribute Syntax
+INFO_RESULT_NO_SUCH_OBJECT=No Such Entry
+INFO_RESULT_ALIAS_PROBLEM=Alias Problem
+INFO_RESULT_INVALID_DN_SYNTAX=Invalid DN Syntax
+INFO_RESULT_ALIAS_DEREFERENCING_PROBLEM=Alias Dereferencing Problem
+INFO_RESULT_INAPPROPRIATE_AUTHENTICATION=Inappropriate Authentication
+INFO_RESULT_INVALID_CREDENTIALS=Invalid Credentials
+INFO_RESULT_INSUFFICIENT_ACCESS_RIGHTS=Insufficient Access Rights
+INFO_RESULT_BUSY=Busy
+INFO_RESULT_UNAVAILABLE=Unavailable
+INFO_RESULT_UNWILLING_TO_PERFORM=Unwilling to Perform
+INFO_RESULT_LOOP_DETECT=Loop Detected
+INFO_RESULT_NAMING_VIOLATION=Naming Violation
+INFO_RESULT_OBJECTCLASS_VIOLATION=Object Class Violation
+INFO_RESULT_NOT_ALLOWED_ON_NONLEAF=Not Allowed on Non-Leaf
+INFO_RESULT_NOT_ALLOWED_ON_RDN=Not Allowed on RDN
+INFO_RESULT_ENTRY_ALREADY_EXISTS=Entry Already Exists
+INFO_RESULT_OBJECTCLASS_MODS_PROHIBITED=Object Class Modifications \
+ Prohibited
+INFO_RESULT_AFFECTS_MULTIPLE_DSAS=Affects Multiple DSAs
+INFO_RESULT_CANCELED=Canceled
+INFO_RESULT_NO_SUCH_OPERATION=No Such Operation
+INFO_RESULT_TOO_LATE=Too Late
+INFO_RESULT_CANNOT_CANCEL=Cannot Cancel
+INFO_RESULT_OTHER=Other
+INFO_RESULT_CLIENT_SIDE_SERVER_DOWN=Server Connection Closed
+INFO_RESULT_CLIENT_SIDE_LOCAL_ERROR=Local Error
+INFO_RESULT_CLIENT_SIDE_ENCODING_ERROR=Encoding Error
+INFO_RESULT_CLIENT_SIDE_DECODING_ERROR=Decoding Error
+INFO_RESULT_CLIENT_SIDE_TIMEOUT=Client-Side Timeout
+INFO_RESULT_CLIENT_SIDE_AUTH_UNKNOWN=Unknown Authentication Mechanism
+INFO_RESULT_CLIENT_SIDE_FILTER_ERROR=Filter Error
+INFO_RESULT_CLIENT_SIDE_USER_CANCELLED=Cancelled by User
+INFO_RESULT_CLIENT_SIDE_PARAM_ERROR=Parameter Error
+INFO_RESULT_CLIENT_SIDE_NO_MEMORY=Out of Memory
+INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR=Connect Error
+INFO_RESULT_CLIENT_SIDE_NOT_SUPPORTED=Operation Not Supported
+INFO_RESULT_CLIENT_SIDE_CONTROL_NOT_FOUND=Control Not Found
+INFO_RESULT_CLIENT_SIDE_NO_RESULTS_RETURNED=No Results Returned
+INFO_RESULT_CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED=Unexpected Results Returned
+INFO_RESULT_CLIENT_SIDE_CLIENT_LOOP=Referral Loop Detected
+INFO_RESULT_CLIENT_SIDE_REFERRAL_LIMIT_EXCEEDED=Referral Hop Limit Exceeded
+INFO_RESULT_AUTHORIZATION_DENIED=Authorization Denied
+INFO_RESULT_ASSERTION_FAILED=Assertion Failed
+INFO_RESULT_SORT_CONTROL_MISSING=Sort Control Missing
+INFO_RESULT_OFFSET_RANGE_ERROR=Offset Range Error
+INFO_RESULT_VIRTUAL_LIST_VIEW_ERROR=Virtual List View Error
+INFO_RESULT_NO_OPERATION=No Operation
+#
+# Protocol messages
+#
+ERR_ASN1_TRUCATED_TYPE_BYTE=Cannot decode the ASN.1 element because an \
+ unexpected end of file was reached while reading the type byte
+ERR_ASN1_TRUNCATED_LENGTH_BYTE=Cannot decode the ASN.1 element because \
+ an unexpected end of file was reached while reading the first length byte
+ERR_ASN1_INVALID_NUM_LENGTH_BYTES=Cannot decode the ASN.1 element \
+ because it contained a multi-byte length with an invalid number of bytes (%d)
+ERR_ASN1_TRUNCATED_LENGTH_BYTES=Cannot decode the ASN.1 element because \
+ an unexpected end of file was reached while reading a multi-byte length of \
+ %d bytes
+ERR_ASN1_BOOLEAN_TRUNCATED_VALUE=Cannot decode the ASN.1 boolean \
+ element of because an unexpected end of file was reached while reading value \
+ bytes (%d)
+ERR_ASN1_BOOLEAN_INVALID_LENGTH=Cannot decode the ASN.1 \
+ boolean element because the decoded value length was not exactly one byte \
+ (decoded length was %d)
+ERR_ASN1_NULL_INVALID_LENGTH=Cannot decode the ASN.1 null element \
+ because the decoded value length was not exactly zero bytes \
+ (decoded length was %d)
+ERR_ASN1_OCTET_STRING_TRUNCATED_VALUE=Cannot decode the ASN.1 octet \
+ string element of because an unexpected end of file was reached while reading \
+ value bytes (%d)
+ERR_ASN1_INTEGER_TRUNCATED_VALUE=Cannot decode the ASN.1 integer \
+ element of because an unexpected end of file was reached while reading \
+ value bytes (%d)
+ERR_ASN1_INTEGER_INVALID_LENGTH=Cannot decode the \
+ provided ASN.1 integer element because the length of the \
+ element value was not between one and four bytes (actual length was %d)
+ERR_ASN1_SEQUENCE_READ_NOT_STARTED=Cannot decode the end of the ASN.1 \
+ sequence or set because the start of the sequence was not read
+ERR_ASN1_SKIP_TRUNCATED_VALUE=Cannot skip the ASN.1 element of because \
+ an unexpected end of file was reached while reading value bytes (%d)
+ERR_ASN1_SEQUENCE_SET_TRUNCATED_VALUE=Cannot decode the ASN.1 sequence \
+ or set element of because an unexpected end of file was reached while reading \
+ value bytes (%d)
+ERR_LDAP_MODIFICATION_DECODE_INVALID_MOD_TYPE=Cannot decode the \
+ provided ASN.1 element as an LDAP modification because it contained an \
+ invalid modification type (%d)
+ERR_LDAP_SEARCH_REQUEST_DECODE_INVALID_SCOPE=Cannot decode the \
+ provided ASN.1 element as an LDAP search request protocol op because the \
+ provided scope value (%d) is invalid
+ERR_LDAP_SEARCH_REQUEST_DECODE_INVALID_DEREF=Cannot decode the \
+ provided ASN.1 element as an LDAP search request protocol op because the \
+ provided alias dereferencing policy value (%d) is invalid
+ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED=The client sent a \
+ request to the Directory Server with an ASN.1 element value length of %d \
+ bytes.  This exceeds the maximum allowed request size of %d bytes, so \
+ processing cannot continue on this connection
+ERR_LDAP_FILTER_STRING_NULL=Cannot decode the provided string as an \
+ LDAP search filter because the string was null
+ERR_LDAP_FILTER_UNCAUGHT_EXCEPTION=Cannot decode the provided string \
+ %s as an LDAP search filter because an unexpected exception was thrown during \
+ processing:  %s
+ERR_LDAP_FILTER_MISMATCHED_PARENTHESES=The provided search filter \
+ "%s" had mismatched parentheses around the portion between positions %d and \
+ %d
+ERR_LDAP_FILTER_NO_EQUAL_SIGN=The provided search filter "%s" was \
+ missing an equal sign in the suspected simple filter component between \
+ positions %d and %d
+ERR_LDAP_FILTER_INVALID_ESCAPED_BYTE=The provided search filter "%s" \
+ had an invalid escaped byte value at position %d.  A backslash in a value \
+ must be followed by two hexadecimal characters that define the byte that has \
+ been encoded
+ERR_LDAP_FILTER_COMPOUND_MISSING_PARENTHESES=The provided search \
+ filter "%s" could not be decoded because the compound filter between \
+ positions %d and %d did not start with an open parenthesis and end with a \
+ close parenthesis (they might be parentheses for different filter components)
+ERR_LDAP_FILTER_NO_CORRESPONDING_OPEN_PARENTHESIS=The provided \
+ search filter "%s" could not be decoded because the closing parenthesis at \
+ position %d did not have a corresponding open parenthesis
+ERR_LDAP_FILTER_NO_CORRESPONDING_CLOSE_PARENTHESIS=The provided \
+ search filter "%s" could not be decoded because the opening parenthesis at \
+ position %d did not have a corresponding close parenthesis
+ERR_LDAP_FILTER_SUBSTRING_NO_ASTERISKS=The provided search filter \
+ "%s" could not be decoded because the assumed substring filter value between \
+ positions %d and %d did not have any asterisk wildcard characters
+ERR_LDAP_FILTER_EXTENSIBLE_MATCH_NO_COLON=The provided search filter \
+ "%s" could not be decoded because the extensible match component starting at \
+ position %d did not have a colon to denote the end of the attribute type name
+ERR_LDAP_PAGED_RESULTS_DECODE_NULL=Cannot decode the provided ASN.1 \
+ element as an LDAP paged results control value because the element is null
+ERR_LDAP_PAGED_RESULTS_DECODE_SEQUENCE=Cannot decode the provided \
+ ASN.1 element as an LDAP paged results control value because the element \
+ could not be decoded as a sequence:  %s
+ERR_LDAP_PAGED_RESULTS_DECODE_SIZE=Cannot decode the provided ASN.1 \
+ element as an LDAP paged results control value because the size element could \
+ not be properly decoded:  %s
+ERR_LDAP_PAGED_RESULTS_DECODE_COOKIE=Cannot decode the provided \
+ ASN.1 element as an LDAP paged results control value because the cookie could \
+ not be properly decoded:  %s
+ERR_LDAPASSERT_NO_CONTROL_VALUE=Cannot decode the provided LDAP \
+ assertion control because the control does not have a value
+ERR_LDAPASSERT_INVALID_CONTROL_VALUE=Cannot decode the provided LDAP \
+ assertion control because the control value cannot be decoded as an ASN.1 \
+ element:  %s
+ERR_PREREADREQ_NO_CONTROL_VALUE=Cannot decode the provided LDAP \
+ pre-read request control because the control does not have a value
+ERR_PREREADREQ_CANNOT_DECODE_VALUE=Cannot decode the provided LDAP \
+ pre-read request control because an error occurred while trying to decode the \
+ control value:  %s
+ERR_POSTREADREQ_NO_CONTROL_VALUE=Cannot decode the provided LDAP \
+ post-read request control because the control does not have a value
+ERR_POSTREADREQ_CANNOT_DECODE_VALUE=Cannot decode the provided LDAP \
+ post-read request control because an error occurred while trying to decode \
+ the control value:  %s
+ERR_PREREADRESP_NO_CONTROL_VALUE=Cannot decode the provided LDAP \
+ pre-read response control because the control does not have a value
+ERR_PREREADRESP_CANNOT_DECODE_VALUE=Cannot decode the provided LDAP \
+ pre-read response control because an error occurred while trying to decode \
+ the control value:  %s
+ERR_POSTREADRESP_NO_CONTROL_VALUE=Cannot decode the provided LDAP \
+ post-read response control because the control does not have a value
+ERR_POSTREADRESP_CANNOT_DECODE_VALUE=Cannot decode the provided LDAP \
+ post-read response control because an error occurred while trying to decode \
+ the control value:  %s
+ERR_PROXYAUTH1_NO_CONTROL_VALUE=Cannot decode the provided proxied \
+ authorization V1 control because it does not have a value
+ERR_PROXYAUTH1_CANNOT_DECODE_VALUE=Cannot decode the provided \
+ proxied authorization V1 control because an error occurred while attempting \
+ to decode the control value:  %s
+ERR_PROXYAUTH2_NO_CONTROL_VALUE=Cannot decode the provided proxied \
+ authorization V2 control because it does not have a value
+ERR_PROXYAUTH2_CANNOT_DECODE_VALUE=Cannot decode the provided \
+ proxied authorization V2 control because an error occurred while attempting \
+ to decode the control value:  %s
+ERR_PSEARCH_NO_CONTROL_VALUE=Cannot decode the provided persistent \
+ search control because it does not have a value
+ERR_PSEARCH_CANNOT_DECODE_VALUE=Cannot decode the provided \
+ persistent search control because an error occurred while attempting to \
+ decode the control value:  %s
+ERR_ECN_NO_CONTROL_VALUE=Cannot decode the provided entry change \
+ notification control because it does not have a value
+ERR_ECN_ILLEGAL_PREVIOUS_DN=Cannot decode the provided entry change \
+ notification control because it contains a previous DN element but had a \
+ change type of %s.  The previous DN element can only be provided with the \
+ modify DN change type
+ERR_ECN_CANNOT_DECODE_VALUE=Cannot decode the provided entry change \
+ notification control because an error occurred while attempting to decode the \
+ control value:  %s
+ERR_AUTHZIDRESP_NO_CONTROL_VALUE=Cannot decode the provided \
+ authorization identity response control because it does not have a value
+ERR_MATCHEDVALUES_NO_CONTROL_VALUE=Cannot decode the provided \
+ matched values control because it does not have a value
+ERR_MATCHEDVALUES_CANNOT_DECODE_VALUE_AS_SEQUENCE=Cannot decode \
+ the provided matched values control because an error occurred while \
+ attempting to decode the value as an ASN.1 sequence:  %s
+ERR_MATCHEDVALUES_NO_FILTERS=Cannot decode the provided matched \
+ values control because the control value does not specify any filters for use \
+ in matching attribute values
+ERR_PWEXPIRED_CONTROL_INVALID_VALUE=Cannot decode the provided \
+ control as a password expired control because the provided control had a \
+ value that could not be parsed as an integer
+ERR_PWEXPIRING_NO_CONTROL_VALUE=Cannot decode the provided \
+ password expiring control because it does not have a value
+ERR_PWEXPIRING_CANNOT_DECODE_SECONDS_UNTIL_EXPIRATION=Cannot \
+ decode the provided control as a password expiring control because an error \
+ occurred while attempting to decode the number of seconds until expiration: \
+ %s
+ERR_PWPOLICYREQ_CONTROL_HAS_VALUE=Cannot decode the provided \
+ control as a password policy request control because the provided control had \
+ a value but the password policy request control should not have a value
+ERR_PWPOLICYRES_NO_CONTROL_VALUE=Cannot decode the provided \
+ password policy response control because it does not have a value
+ERR_PWPOLICYRES_INVALID_WARNING_TYPE=Cannot decode the provided \
+ password policy response control because the warning element has an invalid \
+ type of %s
+ERR_PWPOLICYRES_INVALID_ERROR_TYPE=Cannot decode the provided \
+ password policy response control because the error element has an invalid \
+ type of %d
+ERR_PWPOLICYRES_DECODE_ERROR=Cannot decode the provided password \
+ policy response control:  %s
+ERR_ACCTUSABLEREQ_CONTROL_HAS_VALUE=Cannot decode the provided \
+ control as an account availability request control because the provided \
+ control had a value but the account availability request control should not \
+ have a value
+ERR_ACCTUSABLERES_NO_CONTROL_VALUE=Cannot decode the provided \
+ account availability response control because it does not have a value
+ERR_ACCTUSABLERES_UNKNOWN_VALUE_ELEMENT_TYPE=The account \
+ availability response control had an unknown ACCOUNT_USABLE_RESPONSE element \
+ type of %s
+ERR_ACCTUSABLERES_DECODE_ERROR=Cannot decode the provided account \
+ availability response control:  %s
+ERR_PROXYAUTH1_CONTROL_NOT_CRITICAL=Unwilling to process the request \
+ because it contains a proxied authorization V1 control which is not marked \
+ critical.  The proxied authorization control must always have a criticality \
+ of "true"
+ERR_PROXYAUTH2_CONTROL_NOT_CRITICAL=Unwilling to process the request \
+ because it contains a proxied authorization V2 control which is not marked \
+ critical.  The proxied authorization control must always have a criticality \
+ of "true"
+ERR_LDAP_FILTER_NOT_EXACTLY_ONE=The provided search filter "%s" \
+ could not be decoded because the NOT filter between positions %d and %d did \
+ not contain exactly one filter component
+INFO_SORTREQ_CONTROL_NO_VALUE=Unable to decode the provided control as a \
+ server-side sort request control because it does not include a control value
+INFO_SORTREQ_CONTROL_CANNOT_DECODE_VALUE=Unable to process the provided \
+ server-side sort request control because an error occurred while attempting \
+ to decode the control value:  %s
+INFO_SORTRES_CONTROL_NO_VALUE=Unable to decode the provided control as a \
+ server-side sort response control because it does not include a control value
+INFO_SORTRES_CONTROL_CANNOT_DECODE_VALUE=Unable to process the provided \
+ server-side sort response control because an error occurred while attempting \
+ to decode the control value:  %s
+INFO_SORTREQ_CONTROL_NO_SORT_KEYS=Unable to process the provided \
+ server-side sort request control because it did not contain any sort keys
+INFO_VLVREQ_CONTROL_NO_VALUE=Unable to decode the provided control as a \
+ VLV request control because it does not include a control value
+INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE=Unable to decode the provided \
+ control as a VLV request control because the target element type %s is \
+ invalid
+INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE=Unable to process the provided \
+ VLV request control because an error occurred while attempting to decode the \
+ control value:  %s
+INFO_VLVRES_CONTROL_NO_VALUE=Unable to decode the provided control as a \
+ VLV response control because it does not include a control value
+INFO_VLVRES_CONTROL_CANNOT_DECODE_VALUE=Unable to process the provided \
+ VLV response control because an error occurred while attempting to decode the \
+ control value:  %s
+INFO_GETEFFECTIVERIGHTS_INVALID_AUTHZID=The authorization ID "%s" \
+ contained in the geteffectiverights control is invalid because it does not \
+ start with "dn:" to indicate a user DN
+INFO_GETEFFECTIVERIGHTS_DECODE_ERROR=Cannot decode the provided \
+ geteffectiverights request control:  %s
+ERR_LDAP_FILTER_ENCLOSED_IN_APOSTROPHES=An LDAP filter enclosed in \
+ apostrophes is invalid:  %s
+ERR_LDAP_FILTER_INVALID_CHAR_IN_ATTR_TYPE=The provided search filter \
+ contains an invalid attribute type '%s' with invalid character '%s' at \
+ position %d
+ERR_LDAP_FILTER_EXTENSIBLE_MATCH_NO_AD_OR_MR=The provided search \
+ filter "%s" could not be decoded because the extensible match component \
+ starting at position %d did not include either an attribute description or a \
+ matching rule ID.  At least one of them must be provided
+ERR_SUBTREE_DELETE_INVALID_CONTROL_VALUE=Cannot decode the provided \
+ subtree delete control because it contains a value
+ERR_ASN1_UNEXPECTED_TAG=Encountered unexpected tag while reading \
+ ASN.1 element (expected=0x%02x, got=0x%02x)
+ERR_AUTHZIDREQ_CONTROL_HAS_VALUE=Cannot decode the provided \
+ control as an authorization identity request control because the provided \
+ control had a value but the authorization identity request control should not \
+ have a value
+ERR_MVFILTER_BAD_FILTER_AND=The provided filter \
+ "%s" cannot be used as a matched values filter because "and" filters are \
+ not allowed
+ERR_MVFILTER_BAD_FILTER_OR=The provided filter \
+ "%s" cannot be used as a matched values filter because "or" filters are \
+ not allowed
+ERR_MVFILTER_BAD_FILTER_NOT=The provided filter \
+ "%s" cannot be used as a matched values filter because "not" filters are \
+ not allowed
+ERR_MVFILTER_BAD_FILTER_EXT=The provided filter \
+ "%s" cannot be used as a matched values filter because extensible match \
+ filters requesting DN attributes are not allowed
+ERR_MVFILTER_BAD_FILTER_UNRECOGNIZED=The provided filter \
+ "%s" cannot be used as a matched values filter because filters of type %d are \
+ not allowed
+ERR_ASN1_SEQUENCE_WRITE_NOT_STARTED=Cannot encode the end of the ASN.1 \
+ sequence or set because the start of the sequence was not written
+ERR_NO_SEARCH_RESULT_ENTRIES=The search request succeeded \
+ but did not return any search result entries when one was expected
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=The search request succeeded \
+ but returned %d search result entry when only one was expected
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=The search request succeeded \
+ but returned more than one search result entry when only one was expected
+ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES=The search request succeeded \
+ but returned a search result reference containing the following URI: %s
+#
+# Utility messages
+#
+ERR_BASE64_DECODE_INVALID_LENGTH=The value %s cannot be base64-decoded \
+ because it does not have a length that is a multiple of four bytes
+ERR_BASE64_DECODE_INVALID_CHARACTER=The value %s cannot be \
+ base64-decoded because it contains an illegal character %c that is not \
+ allowed in base64-encoded values
+ERR_HEX_DECODE_INVALID_LENGTH=The value %s cannot be decoded as a \
+ hexadecimal string because it does not have a length that is a multiple of \
+ two bytes
+ERR_HEX_DECODE_INVALID_CHARACTER=The value %s cannot be decoded as a \
+ hexadecimal string because it contains an illegal character %c that is not a \
+ valid hexadecimal digit
+ERR_LDIF_INVALID_LEADING_SPACE=Unable to parse line %d ("%s") from the \
+ LDIF source because the line started with a space but there were no previous \
+ lines in the entry to which this line could be appended
+ERR_LDIF_NO_ATTR_NAME=Unable to parse LDIF entry starting at line %d \
+ because the line "%s" does not include an attribute name
+ERR_LDIF_NO_DN=Unable to parse LDIF entry starting at line %d because \
+ the first line does not contain a DN (the first line was "%s"
+ERR_LDIF_INVALID_DN=Unable to parse LDIF entry starting at line %d \
+ because an error occurred while trying to parse the value of line "%s" as a \
+ distinguished name:  %s
+ERR_LDIF_COULD_NOT_BASE64_DECODE_DN=Unable to parse LDIF entry \
+ starting at line %d because it was not possible to base64-decode the DN on \
+ line "%s":  %s
+ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR=Unable to parse LDIF entry %s \
+ starting at line %d because it was not possible to base64-decode the \
+ value on line "%s":  %s
+ERR_LDIF_INVALID_URL=Unable to parse LDIF entry %s starting at line \
+ %d because the value %s was to be read from a URL but the URL \
+ was invalid:  %s
+ERR_LDIF_URL_IO_ERROR=Unable to parse LDIF entry %s starting at line \
+ %d because the value %s was to be read from URL %s but an error \
+ occurred while trying to read that content:  %s
+ERR_LDAPURL_NO_SCHEME=The provided string "%s" cannot be decoded \
+ as an LDAP URL because it does not contain a protocol scheme
+ERR_LDAPURL_BAD_SCHEME=The provided string "%s" cannot be decoded \
+ as an LDAP URL because the protocol scheme "%s" is invalid. It should be \
+ either "ldap" or "ldaps"
+ERR_LDAPURL_CANNOT_DECODE_PORT=The provided string "%s" cannot be \
+ decoded as an LDAP URL because the port number portion %s cannot be decoded \
+ as an integer
+ERR_LDAPURL_INVALID_PORT=The provided string "%s" cannot be \
+ decoded as an LDAP URL because the provided port number %d is not within the \
+ valid range between 1 and 65535
+ERR_LDAPURL_BAD_PORT=The provided port number %d is not within the \
+ valid range between 1 and 65535
+ERR_LDAPURL_INVALID_DN=The provided string "%s" cannot be \
+ decoded as an LDAP URL because the provided distinguished name could \
+ not be parsed: %s
+ERR_LDAPURL_INVALID_FILTER=The provided string "%s" cannot be \
+ decoded as an LDAP URL because the provided filter could \
+ not be parsed: %s
+ERR_LDAPURL_INVALID_HEX_BYTE=The provided URL component "%s" could \
+ not be decoded because the character at byte %d was not a valid hexadecimal \
+ digit
+ERR_INVALID_ESCAPE_CHAR=The value %s cannot be decoded because %c \
+ is not a valid escape character
+WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND=The provided LDIF \
+ content did not contain any LDIF change records
+WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND=The provided LDIF \
+ content contained multiple LDIF change records, when only one was expected
+WARN_READ_LDIF_RECORD_CHANGE_RECORD_WRONG_TYPE=The provided LDIF \
+ content did not contain an "%s" change record
+WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR=An unexpected IO error \
+ occurred while reading the provided LDIF content: %s
+WARN_READ_LDIF_ENTRY_NO_ENTRY_FOUND=The provided LDIF \
+ content did not contain any entry
+WARN_READ_LDIF_ENTRY_MULTIPLE_ENTRIES_FOUND=The provided LDIF \
+ content contained %d entries, when only one was expected
+#
+# Extension messages
+#
+ERR_PWPSTATE_EXTOP_UNKNOWN_OP_TYPE=The password policy state \
+ extended request included an operation with an invalid or unsupported \
+ operation type of %s
+ERR_PWPSTATE_EXTOP_NO_REQUEST_VALUE=The provided password policy \
+ state extended request did not include a request value
+ERR_PWPSTATE_EXTOP_DECODE_FAILURE=An unexpected error occurred \
+ while attempting to decode password policy state extended request value:  %s
+ERR_EXTOP_CANCEL_NO_REQUEST_VALUE=Unable to process the cancel \
+ request because the extended operation did not include a request value
+ERR_EXTOP_CANCEL_CANNOT_DECODE_REQUEST_VALUE=An error occurred while \
+ attempting to decode the value of the cancel extended request:  %s
+ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST=An unexpected error occurred \
+ while attempting to decode the password modify extended request sequence:  %s
+ERR_GET_SYMMETRIC_KEY_ASN1_DECODE_EXCEPTION=Cannot decode the \
+ provided symmetric key extended request: %s
+ERR_GET_SYMMETRIC_KEY_NO_VALUE=Cannot decode the provided \
+ symmetric key extended operation because it does not have a value
+INFO_SASL_UNSUPPORTED_CALLBACK=An unsupported or unexpected callback was \
+ provided to the SASL server for use during %s authentication:  %s
+ERR_SASL_CONTEXT_CREATE_ERROR=An unexpected error occurred while \
+ trying to create an %s context: %s
+ERR_SASL_PROTOCOL_ERROR=SASL %s protocol error: %s
+#
+# Tools messages
+#
+ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED=An error occurred \
+ while attempting to perform local authentication to the Kerberos realm:  %s
+ERR_ACCTUSABLEREQ_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an account availability request control because it contained \
+ the OID '%s', when '%s' was expected
+ERR_ACCTUSABLERES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an account availability response control because it contained \
+ the OID '%s', when '%s' was expected
+ERR_LDAPASSERT_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an assertion control because it contained \
+ the OID '%s', when '%s' was expected
+ERR_AUTHZIDREQ_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an authorization identity request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_AUTHZIDRESP_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an authorization identity response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_ECN_CONTROL_BAD_OID=Cannot decode the provided \
+ control as an entry change notification response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_ECN_INVALID_PREVIOUS_DN=Cannot decode the provided entry change \
+ notification control because it contains an invalid previous DN: %s
+ERR_GETEFFECTIVERIGHTS_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a get effective rights request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_GETEFFECTIVERIGHTS_INVALID_AUTHZIDDN=Cannot decode the provided \
+ get effective rights request control because it contains an invalid \
+ authorization ID distinguished name: %s
+ERR_GETEFFECTIVERIGHTS_UNKNOWN_ATTRIBUTE=Cannot decode the provided \
+ get effective rights request control because it contained an unrecognized \
+ attribute type: %s
+ERR_MATCHEDVALUES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a matched values request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_LDAP_PAGED_RESULTS_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a simple paged results control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_SUBTREE_DELETE_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a tree delete request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PWEXPIRED_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a Netscape password expired response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PWEXPIRING_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a Netscape password expiring response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PSEARCH_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a persistent search request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PSEARCH_BAD_CHANGE_TYPES=Cannot decode the provided \
+ control as a persistent search request control because it an \
+ invalid changeTypes field '%d', when a value between 0 and 15 was expected
+ERR_ECN_BAD_CHANGE_TYPE=Cannot decode the provided \
+ control as a entry change notification control because it an \
+ invalid changeTypes field '%d', when a value of 1, 2, 4, or 8 was expected
+ERR_POSTREAD_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a post-read control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PREREAD_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a pre-read control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PROXYAUTH1_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a proxy authorization v1 control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PROXYAUTH1_INVALID_AUTHZIDDN=Cannot decode the provided \
+ proxy authorization v1 control because it contains an invalid \
+ authorization distinguished name: %s
+ERR_PROXYAUTH2_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a proxy authorization v2 control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PROXYAUTH2_INVALID_AUTHZID_TYPE=Cannot decode the provided \
+ proxy authorization v2 control because the control value '%s' does not \
+ begin with a valid authorization ID type 'dn:' or 'u:'
+ERR_SORT_KEY_DEFAULT_MRULE_NOT_FOUND=The sort key '%s' could not be \
+ decoded because the attribute description '%s' does not have a default \
+ ordering matching rule
+ERR_SORT_KEY_MRULE_NOT_FOUND=The sort key '%s' could not be decoded \
+ because the ordering matching rule '%s' was not found in the schema
+ERR_SORT_KEY_NO_ATTR_NAME=The sort key '%s' could not be decoded \
+ because it did not contain an attribute description
+ERR_SORT_KEY_NO_MATCHING_RULE=The sort key '%s' could not be decoded \
+ because it contained a colon but no ordering matching rule name
+ERR_SORT_KEY_NO_SORT_KEYS=The list of sort keys '%s' could not be \
+ decoded because it did not contain any sort keys
+ERR_SORTREQ_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a server side sort request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_SORTRES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a server side sort response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_VLVREQ_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a virtual list view request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_VLVRES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a virtual list view response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PWPOLICYREQ_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a password policy request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PWPOLICYRES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a password policy response control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_MANAGEDSAIT_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a ManageDsaIT request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_MANAGEDSAIT_INVALID_CONTROL_VALUE=Cannot decode the provided \
+ ManageDsaIT control because it contains a value
+ERR_SUBENTRIES_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a sub-entries request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_SUBENTRIES_NO_CONTROL_VALUE=Cannot decode the provided \
+ sub-entries control because it does not have a value
+ERR_SUBENTRIES_CANNOT_DECODE_VALUE=Cannot decode \
+ the provided sub-entries control because an error occurred while \
+ attempting to decode the value as an ASN.1 boolean:  %s
+ERR_WHOAMI_INVALID_AUTHZID_TYPE=The provided authorization ID '%s' \
+ does not begin with a valid authorization ID type 'dn:' or 'u:'
+ERR_PERMISSIVE_MODIFY_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a permissive modify request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_PERMISSIVE_MODIFY_INVALID_CONTROL_VALUE=Cannot decode the provided \
+ permissive modify control because it contains a value
+ERR_REAL_ATTRS_ONLY_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a real attributes only request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_REAL_ATTRS_ONLY_INVALID_CONTROL_VALUE=Cannot decode the provided \
+ real attributes only control because it contains a value
+ERR_VIRTUAL_ATTRS_ONLY_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a virtual attributes only request control because it \
+ contained the OID '%s', when '%s' was expected
+ERR_VIRTUAL_ATTRS_ONLY_INVALID_CONTROL_VALUE=Cannot decode the provided \
+ virtual attributes only control because it contains a value
+WARN_CLIENT_DUPLICATE_MESSAGE_ID=The operation was rejected because there is \
+ already another request on the same client connection with the same message \
+ ID of %d
+INFO_CANCELED_BY_ABANDON_REQUEST=The operation was canceled because the client \
+ issued an abandon request (message ID %d) for this operation
+INFO_CANCELED_BY_CANCEL_REQUEST=The operation was canceled because the client \
+ issued a cancel request (message ID %d) for this operation
+INFO_CANCELED_BY_CLIENT_DISCONNECT=The operation was canceled because the \
+ client has disconnected from the server
+INFO_CANCELED_BY_SERVER_DISCONNECT=The operation was canceled because the \
+ server has disconnected from the client
+INFO_CANCELED_BY_CLIENT_ERROR=The operation was canceled because the \
+ client connection failed
+INFO_CLIENT_CONNECTION_CLOSING=The operation was rejected because the \
+ client connection is closing
+ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid attribute syntax description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_ATTRTYPE_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid attribute type description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_MR_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid matching rule description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid matching rule use description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_NAME_FORM_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid name form description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid object class description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_DCR_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid DIT content rule description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_DSR_ILLEGAL_TOKEN1=The provided value "%s"could not \
+ be parsed as a valid DIT structure rule description because it contains an \
+ illegal token "%s"
+ERR_ATTR_SYNTAX_TRUNCATED_VALUE1=the value appears to be truncated
+ERR_ATTR_SYNTAX_ILLEGAL_CHAR_IN_STRING_OID1=the value contains a non-numeric \
+ OID which could not be parsed because it contains an illegal character \
+ '%s' at position %d
+ERR_ATTR_SYNTAX_OID_CONSECUTIVE_PERIODS1=the value contains a numeric \
+ OID which could not be parsed because it contains two consecutive periods at \
+ or near position %d
+ERR_ATTR_SYNTAX_OID_NO_VALUE1=the value did not contain an OID at position %d
+ERR_ATTR_SYNTAX_OID_ILLEGAL_CHARACTER1=the value contains a numeric OID which \
+ could not be parsed because it contains an illegal character '%s' at position %d
+ERR_ATTR_SYNTAX_OID_ENDS_WITH_PERIOD1=the value contains a numeric OID which \
+ could not be parsed because it ends with a period at position %d
+ERR_ATTR_SYNTAX_EXPECTED_QUOTE_AT_POS1=the value should have contained a single \
+ quote at position %d but the character '%s' was found instead
+ERR_ATTR_SYNTAX_RULE_ID_NO_VALUE1=the value did not contain a rule ID at position %d
+ERR_ATTR_SYNTAX_RULE_ID_INVALID1=the value contained an invalid rule ID "%s" \
+ which could not be parsed as a number
+ERR_ATTR_SYNTAX_UNEXPECTED_CLOSE_PARENTHESIS1=the value contained an unexpected \
+ closing parenthesis at position %d
+ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID1=The provided value "%s" could not \
+ be parsed as a valid attribute syntax description for the following reason: %s
+ERR_ATTR_SYNTAX_ATTRTYPE_INVALID1=The provided value "%s" could not \
+ be parsed as a valid attribute type description for the following reason: %s
+ERR_ATTR_SYNTAX_DCR_INVALID1=The provided value "%s" could not \
+ be parsed as a valid DIT content rule description for the following reason: %s
+ERR_ATTR_SYNTAX_DSR_INVALID1=The provided value "%s" could not \
+ be parsed as a valid DIT structure rule description for the following reason: %s
+ERR_ATTR_SYNTAX_MR_INVALID1=The provided value "%s" could not \
+ be parsed as a valid matching rule description for the following reason: %s
+ERR_ATTR_SYNTAX_MRUSE_INVALID1=The provided value "%s" could not \
+ be parsed as a valid matching rule use description for the following reason: %s
+ERR_ATTR_SYNTAX_NAME_FORM_INVALID1=The provided value "%s" could not \
+ be parsed as a valid name form description for the following reason: %s
+ERR_ATTR_SYNTAX_OBJECTCLASS_INVALID1=The provided value "%s" could not \
+ be parsed as a valid object class description for the following reason: %s
+ERR_ATTR_SYNTAX_ATTRTYPE_EMPTY_VALUE1=The provided value "%s" could not be \
+ parsed as a valid attribute type description because it was empty or \
+ contained only whitespace
+WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_ATTRIBUTE_USAGE1=The provided value "%s" \
+ could not be parsed as a valid attribute type description because \
+ it declared that it should have an attribute usage of %s. This is an invalid usage
+ERR_ATTR_SYNTAX_OBJECTCLASS_EMPTY_VALUE1=The provided value "%s" could not \
+ be parsed as a valid object class description because it was empty or \
+ contained only whitespace
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE1=The provided value "%s" could not \
+ be parsed as a valid attribute syntax description because it was empty or \
+ contained only whitespace
+ERR_ATTR_SYNTAX_OBJECTCLASS_EXPECTED_OPEN_PARENTHESIS1=The provided \
+ value "%s" could not be parsed as an object class description because an open \
+ parenthesis was expected at position %d but instead a '%s' character was \
+ found
+WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_SUPERIOR_CLASS1=The object class "%s" \
+ specifies the superior object class "%s" which is not defined in the schema
+WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_REQUIRED_ATTR1=The object class "%s" \
+ specifies the required attribute type "%s" which is not defined in the schema
+WARN_ATTR_SYNTAX_OBJECTCLASS_UNKNOWN_OPTIONAL_ATTR1=The object class "%s" \
+ specifies the optional attribute type "%s" which is not defined in the schema
+ERR_ATTR_SYNTAX_DCR_EMPTY_VALUE1=The provided value "%s" could not be \
+ parsed as a valid DIT content rule description because it was empty or \
+ contained only whitespace
+ERR_ATTR_SYNTAX_DCR_UNKNOWN_STRUCTURAL_CLASS1=The DIT content rule \
+ "%s" is associated with a structural object class %s which is not defined in \
+ the schema
+ERR_ATTR_SYNTAX_DCR_STRUCTURAL_CLASS_NOT_STRUCTURAL1=The DIT content \
+ rule "%s" is associated with the "%s" object class.  This \
+ object class exists in the schema but is defined as %s rather than \
+ structural
+ERR_ATTR_SYNTAX_DCR_UNKNOWN_AUXILIARY_CLASS1=The DIT content rule \
+ "%s" is associated with an auxiliary object class "%s" which is not defined in \
+ the schema
+ERR_ATTR_SYNTAX_DCR_AUXILIARY_CLASS_NOT_AUXILIARY1=The DIT content \
+ rule "%s" is associated with an auxiliary object class "%s".  This object class \
+ exists in the schema but is defined as %s rather than auxiliary
+ERR_ATTR_SYNTAX_NAME_FORM_EMPTY_VALUE1=The provided value "%s" could not \
+ be parsed as a valid name form description because it was empty or contained \
+ only whitespace
+ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_STRUCTURAL_CLASS1=The name form \
+ description "%s" is associated with a structural object class "%s" which is not \
+ defined in the schema
+ERR_ATTR_SYNTAX_NAME_FORM_STRUCTURAL_CLASS_NOT_STRUCTURAL1=The name \
+ form description "%s" is associated with the "%s" object class. \
+ This object class exists in the schema but is defined as %s rather than \
+ structural
+ERR_ATTR_SYNTAX_NAME_FORM_NO_STRUCTURAL_CLASS1=The provided value \
+ "%s" could not be parsed as a name form description because it does not \
+ specify the structural object class with which it is associated
+ERR_ATTR_SYNTAX_MR_EMPTY_VALUE1=The provided value "%s" could not be \
+ parsed as a valid matching rule description because it was empty or contained \
+ only whitespace
+ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE1=The provided value "%s" could not be \
+ parsed as a valid matching rule use description because it was empty or \
+ contained only whitespace
+ERR_ATTR_SYNTAX_DSR_EMPTY_VALUE1=The provided value "%s" could not be \
+ parsed as a valid DIT structure rule description because it was empty or \
+ contained only whitespace
+ERR_ATTR_SYNTAX_GUIDE_NO_OC1=The provided value "%s" could not be \
+ parsed as a guide value because it did not contain an object class name or OID \
+ before the octothorpe (#) character
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SHARP1=The provided value "%s" could \
+ not be parsed as an enhanced guide value because it did not contain an \
+ octothorpe (#) character to separate the object class from the criteria
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_OC1=The provided value "%s" could \
+ not be parsed as an enhanced guide value because it did not contain an \
+ object class name or OID before the octothorpe (#) character
+WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_TYPE1=The object class "%s" is \
+ invalid because it has an object class type of %s which is incompatible with \
+ the object class type %s specified by the superior object class "%s"
+WARN_ATTR_SYNTAX_OBJECTCLASS_STRUCTURAL_SUPERIOR_NOT_TOP1=The object class "%s" \
+ is invalid because it is defined as a structural class but its superior chain \
+  does not include the "top" object class
+ERR_SCHEMA_CONFLICTING_OBJECTCLASS_OID1=Unable to register \
+ object class %s with the server schema because its OID %s conflicts with the \
+ OID of an existing object class %s
+ERR_SCHEMA_CONFLICTING_DIT_CONTENT_RULE1=Unable to register DIT \
+ content rule %s with the server schema because its structural object class %s \
+ conflicts with the structural object class for an existing DIT content rule %s
+WARN_MATCHING_RULE_NOT_IMPLEMENTED1=No implementation found for the matching \
+  rule "%s". The default matching rule "%s" will be used instead
+WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_SUPERIOR_TYPE1=The definition for \
+ the attribute type "%s" declared a superior type "%s" which is not defined \
+ in the schema
+WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_APPROXIMATE_MR1=The definition for \
+ the attribute type "%s" declared that approximate matching should be \
+ performed using the matching rule "%s" which is not defined \
+ in the schema
+WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_EQUALITY_MR1=The definition for \
+ the attribute type "%s" declared that equality matching should be \
+ performed using the matching rule "%s" which is not defined \
+ in the schema
+WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_ORDERING_MR1=The definition for \
+ the attribute type "%s" declared that ordering matching should be \
+ performed using the matching rule "%s" which is not defined \
+ in the schema
+WARN_ATTR_SYNTAX_ATTRTYPE_UNKNOWN_SUBSTRING_MR1=The definition for \
+ the attribute type "%s" declared that substring matching should be \
+ performed using the matching rule "%s" which is not defined \
+ in the schema
+WARN_ATTR_TYPE_NOT_DEFINED1=The definition for the attribute type "%s" \
+ declared that it should use the syntax "%s" which is not defined in the schema. \
+ The default syntax "%s" will be used instead
+ERR_ATTR_SYNTAX_DCR_UNKNOWN_REQUIRED_ATTR1=The DIT content rule "%s" \
+ is associated with a required attribute type "%s" which is not defined in the \
+ schema
+ERR_ATTR_SYNTAX_DCR_UNKNOWN_OPTIONAL_ATTR1=The DIT content rule "%s" \
+ is associated with an optional attribute type "%s" which is not defined in the \
+ schema
+ERR_ATTR_SYNTAX_DCR_UNKNOWN_PROHIBITED_ATTR1=The DIT content rule \
+ "%s" is associated with a prohibited attribute type "%s" which is not defined in \
+ the schema
+ERR_ATTR_SYNTAX_MR_UNKNOWN_SYNTAX1=The matching rule "%s" \
+ is associated with attribute syntax "%s" which is not defined in the schema
+ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_MATCHING_RULE1=The matching rule use "%s" \
+ specifies the matching rule "%s" which is not defined in the schema
+ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_ATTR1=The matching rule use "%s" \
+ specifies the attribute type "%s" which is not defined in the schema
+ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_REQUIRED_ATTR1=The name form "%s" specifies \
+ the required attribute "%s" which is not defined in the schema
+ERR_ATTR_SYNTAX_NAME_FORM_UNKNOWN_OPTIONAL_ATTR1=The name form "%s" specifies \
+ the optional attribute "%s" which is not defined in the schema
+WARN_ATTR_SYNTAX_NOT_IMPLEMENTED1=The "%s" syntax with OID %s is not \
+ implemented. It will be substituted by the default syntax with OID %s
+ERR_LDIF_ENTRY_EXCLUDED_BY_DN=Skipping LDIF entry starting at line %d with \
+ distinguished name "%s" because it is within an excluded branch
+ERR_LDIF_ENTRY_EXCLUDED_BY_FILTER=Skipping LDIF entry starting at line %d with \
+ distinguished name "%s" because it does not meet the filter criteria
+ERR_LDIF_CHANGE_EXCLUDED_BY_DN=Skipping LDIF change record starting at line %d \
+ with distinguished name "%s" because it is within an excluded branch
+ERR_LDIF_NO_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because there was no change type
+ERR_LDIF_MALFORMED_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because it contained a malformed changetype \
+ "%s"
+ERR_LDIF_BAD_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because it contained an unrecognized changetype \
+ "%s". The changetype must be one of add, delete, modify, modrdn, or moddn
+ERR_LDIF_MALFORMED_DELETE=Unable to parse LDIF delete record starting at line %d \
+ with distinguished name "%s" because it contained additional lines after the \
+ changetype when none were expected
+ERR_LDIF_BAD_MODIFICATION_TYPE=Unable to parse LDIF modify record starting at line %d \
+ with distinguished name "%s" because it contained an unrecognized modification type \
+ "%s". The modification type must be one of add, delete, replace, or increment
+ERR_LDIF_MALFORMED_MODIFICATION_TYPE=Unable to parse LDIF modify record starting at line %d \
+ with distinguished name "%s" because it contained a malformed modification type \
+ "%s"
+ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it contains an \
+ unrecognized attribute type "%s"
+ERR_LDIF_MALFORMED_ATTRIBUTE_NAME=Unable to parse LDIF change record \
+ starting at line %d with distinguished name "%s" because it contained a \
+ malformed attribute description "%s"
+ERR_LDIF_UNEXPECTED_BINARY_OPTION=Unable to parse LDIF change record starting \
+ at line %d with distinguished name "%s" because it has an unexpected binary \
+ option for attribute %s
+ERR_LDIF_ATTRIBUTE_NAME_MISMATCH=Unable to parse LDIF change record \
+ starting at line %d with distinguished name "%s" because it contained a \
+ an unexpected attribute "%s" when "%s" was expected
+WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it includes a \
+ duplicate attribute "%s" with value "%s"
+ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it includes multiple \
+ values for single-valued attribute "%s"
+ERR_LDIF_NO_NEW_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because there was no new RDN
+ERR_LDIF_MALFORMED_NEW_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed new RDN "%s"
+ERR_LDIF_NO_DELETE_OLD_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because there was no deleteoldrdn field
+ERR_LDIF_MALFORMED_DELETE_OLD_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed deleteoldrdn "%s"
+ERR_LDIF_MALFORMED_NEW_SUPERIOR=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed newsuperior "%s"
+ERR_ENTRY_SCHEMA_MULTIPLE_STRUCTURAL_CLASSES=Entry "%s" violates the schema \
+ because it contains multiple conflicting structural object classes "%s" and \
+ "%s". Only a single structural object class is allowed in an entry
+ERR_ENTRY_SCHEMA_UNKNOWN_OBJECT_CLASS=Entry "%s" violates the schema \
+ because it contains an unrecognized object class "%s"
+ERR_ENTRY_SCHEMA_NO_STRUCTURAL_CLASS=Entry "%s" violates the schema because \
+ it does not include a structural object class. All entries must contain a \
+ structural object class
+ERR_ENTRY_SCHEMA_DCR_MISSING_MUST_ATTRIBUTES=Entry "%s" violates the schema \
+ because it does not contain attribute "%s" which is required by DIT content rule "%s"
+ERR_ENTRY_SCHEMA_DCR_PROHIBITED_ATTRIBUTES=Entry "%s" violates the schema \
+ because it contains attribute "%s" which is prohibited by DIT content rule "%s"
+ERR_ENTRY_SCHEMA_DCR_PROHIBITED_AUXILIARY_OC=Entry "%s" violates the schema because \
+ it contains auxiliary object class "%s" which is not allowed by DIT content rule "%s"
+ERR_ENTRY_SCHEMA_OC_MISSING_MUST_ATTRIBUTES=Entry "%s" violates the schema \
+ because it does not contain attribute "%s" which is required by object class "%s"
+ERR_ENTRY_SCHEMA_OC_DISALLOWED_ATTRIBUTES=Entry "%s" violates the schema \
+ because it contains attribute "%s" which is not allowed by any of the object \
+ classes in the entry
+ERR_ENTRY_SCHEMA_DCR_DISALLOWED_ATTRIBUTES=Entry "%s" violates the schema \
+ because it contains attribute "%s" which is not allowed by any of the object \
+ classes in the entry nor its DIT content rule "%s"
+ERR_ENTRY_SCHEMA_AT_EMPTY_ATTRIBUTE=Entry "%s" violates the schema \
+ because it contains an empty attribute "%s"
+ERR_ENTRY_SCHEMA_AT_SINGLE_VALUED_ATTRIBUTE=Entry "%s" violates the schema \
+ because it contains multiple values for the single-valued attribute "%s"
+ERR_ENTRY_SCHEMA_NF_MISSING_MUST_ATTRIBUTES=Entry "%s" violates the schema \
+ because its RDN does not contain the attribute "%s" which is required by \
+ name form "%s"
+ERR_ENTRY_SCHEMA_NF_DISALLOWED_ATTRIBUTES=Entry "%s" violates the schema \
+ because its RDN contains attribute "%s" which is not allowed by any the name \
+ form "%s"
+ERR_ENTRY_SCHEMA_DSR_NO_PARENT_OC=Entry "%s" could not be validated against \
+ any DIT structure rules because its parent entry does not appear to \
+ contain a valid structural object class
+ERR_ENTRY_SCHEMA_DSR_PARENT_NOT_FOUND=Entry "%s" could not be validated against \
+ any DIT structure rules because its parent entry could not be retrieved \
+ for the following reason: %s
+ERR_ENTRY_SCHEMA_DSR_ILLEGAL_OC=Entry "%s" violates the schema because DIT \
+ structure rule "%s" does not allow entries of type "%s" to be placed \
+ immediately below entries of type "%s"
+ERR_ENTRY_SCHEMA_DSR_MISSING_DSR=Entry "%s" violates the schema because \
+ there is no DIT structure rule that applies to the entry, but there is a DIT \
+ structure rule "%s" which applies to the parent entry
+WARN_ATTR_SYNTAX_ATTRTYPE_MISSING_SYNTAX_AND_SUPERIOR=The provided value "%s" \
+ could not be parsed as a valid attribute type description because \
+ it does not declare a syntax nor a superior type. Attribute type descriptions \
+ must declare a superior type or a syntax
+WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_SUPERIOR_TYPE=The definition for \
+ the attribute type "%s" declared a superior type "%s" which has been removed \
+ from the schema because it is invalid
+WARN_ATTR_SYNTAX_OBJECTCLASS_INVALID_SUPERIOR_CLASS=The definition for \
+ the object class "%s" declared a superior object class "%s" which has been \
+ removed from the schema because it is invalid
+ERR_CONNECTION_POOL_CLOSING=No connection could be obtained from connection \
+ pool "%s" because it is closing
+REJECTED_CHANGE_FAIL_ADD_DUPE=The entry "%s" could not be added because there \
+ is already an entry with the same name
+REJECTED_CHANGE_FAIL_DELETE=The entry "%s" could not be deleted because the \
+ entry does not exist
+REJECTED_CHANGE_FAIL_MODIFY=The entry "%s" could not be modified because the \
+ entry does not exist
+REJECTED_CHANGE_FAIL_MODIFYDN=The entry "%s" could not be renamed because the \
+ entry does not exist
+REJECTED_CHANGE_FAIL_MODIFYDN_DUPE=The entry "%s" could not be renamed because \
+ there is already an entry with the same name
+FUNCTIONS_TO_INTEGER_FAIL=The provided value "%s" could not be parsed as an \
+ integer
+FUNCTIONS_TO_LONG_FAIL=The provided value "%s" could not be parsed as an \
+ long
+ERR_LDIF_MALFORMED_CONTROL=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because it contained a malformed control \
+ "%s"
+ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE=Unsupported modification type '%s'
+ERR_ENTRY_DUPLICATE_VALUES=Unable to add one or more values to attribute \
+ '%s' because at least one of the values already exists
+ERR_ENTRY_NO_SUCH_VALUE=Unable to remove one or more values from attribute \
+ '%s' because at least one of the attributes does not exist in the entry
+ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE=Unable to increment the value of attribute \
+ '%s' because that attribute does not exist in the entry
+ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT=Unable to increment the value of \
+ attribute '%s' because the provided modification did not have exactly \
+ one value to use as the increment
+ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT=Unable to increment the value of \
+ attribute '%s' because either the current value or the increment could \
+ not be parsed as an integer
+HBCF_CONNECTION_CLOSED_BY_CLIENT=Connection closed by client
+HBCF_HEARTBEAT_FAILED=Heartbeat failed
+HBCF_HEARTBEAT_TIMEOUT=Heartbeat timed out after %d ms
+ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_COUNT=Invalid number of arguments \
+ provided for tag %s on line number %d of the template file:  expected %d, got \
+ %d
+ERR_ENTRY_GENERATOR_TAG_INVALID_ARGUMENT_RANGE_COUNT=Invalid number of \
+ arguments provided for tag %s on line number %d of the template file: \
+ expected between %d and %d, got %d
+ERR_ENTRY_GENERATOR_TAG_UNDEFINED_ATTRIBUTE=Undefined attribute %s \
+ referenced on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_INTEGER_BELOW_LOWER_BOUND=Value %d is below the \
+ lowest allowed value of %d for tag %s on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_INTEGER=Cannot parse value "%s" as \
+ an integer for tag %s on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_INTEGER_ABOVE_UPPER_BOUND=Value %d is above the \
+ largest allowed value of %d for tag %s on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_CANNOT_PARSE_AS_BOOLEAN=Cannot parse value "%s" as \
+ a Boolean value for tag %s on line %d of the template file.  The value must \
+ be either 'true' or 'false'
+ERR_ENTRY_GENERATOR_CANNOT_LOAD_TAG_CLASS=Unable to load class %s for use \
+ as a MakeLDIF tag
+ERR_ENTRY_GENERATOR_CANNOT_INSTANTIATE_TAG=Cannot instantiate class %s as a \
+ MakeLDIF tag
+ERR_ENTRY_GENERATOR_CONFLICTING_TAG_NAME=Cannot register the tag defined in \
+ class %s because the tag name %s conflicts with the name of another tag that \
+ has already been registered
+WARN_ENTRY_GENERATOR_WARNING_UNDEFINED_CONSTANT=Possible reference to an \
+ undefined constant '%s' on line %d
+ERR_ENTRY_GENERATOR_DEFINE_MISSING_EQUALS=The constant definition on line \
+ %d is missing an equal sign to delimit the constant name from the value
+ERR_ENTRY_GENERATOR_DEFINE_NAME_EMPTY=The constant definition on line %d \
+ does not include a name for the constant
+ERR_ENTRY_GENERATOR_WARNING_DEFINE_VALUE_EMPTY=Constant '%s' defined on line \
+ %d has not been assigned a value
+ERR_ENTRY_GENERATOR_CONFLICTING_BRANCH_DN=The branch definition '%s' starting \
+ on line %d conflicts with an earlier branch definition contained in the \
+ template file
+ERR_ENTRY_GENERATOR_CONFLICTING_TEMPLATE_NAME=The template definition %s \
+ starting on line %d conflicts with an earlier template definition contained \
+ in the template file
+ERR_ENTRY_GENERATOR_UNEXPECTED_TEMPLATE_FILE_LINE=Unexpected template line \
+ "%s" encountered on line %d of the template file
+ERR_ENTRY_GENERATOR_UNDEFINED_TEMPLATE_SUBORDINATE=The template named %s \
+ references a subordinate template named %s which is not defined in the \
+ template file
+ERR_ENTRY_GENERATOR_CANNOT_DECODE_BRANCH_DN=Unable to decode branch DN "%s" \
+ on line %d of the template file
+ERR_ENTRY_GENERATOR_UNDEFINED_BRANCH_SUBORDINATE=The branch with entry DN \
+ '%s' references a subordinate template named '%s' which is not defined in the \
+ template file
+WARN_ENTRY_GENERATOR_SUBORDINATE_ZERO_ENTRIES=Subordinate template \
+ definition on line %d for %s %s specifies that zero entries of type %s \
+ should be generated
+ERR_ENTRY_GENERATOR_SUBORDINATE_CANT_PARSE_NUMENTRIES=Unable to \
+ parse the number of entries for template %s as an integer for the subordinate \
+ template definition on line %d for %s %s
+ERR_ENTRY_GENERATOR_TEMPLATE_MISSING_RDN_ATTR=The template named %s \
+ includes RDN attribute %s that is not assigned a value in that template
+ERR_ENTRY_GENERATOR_NO_COLON_IN_TEMPLATE_LINE=There is no colon to separate \
+ the attribute name from the value pattern on line %d of the template file in \
+ the definition for %s %s
+ERR_ENTRY_GENERATOR_NO_ATTR_IN_TEMPLATE_LINE=There is no attribute name \
+ before the colon on line %d of the template file in the definition for \
+ template %s %s
+WARN_ENTRY_GENERATOR_NO_VALUE_IN_TEMPLATE_LINE=The value pattern for line \
+ %d of the template file in the definition for %s %s is empty
+ERR_ENTRY_GENERATOR_NO_SUCH_TAG=An undefined tag %s is referenced on line \
+ %d of the template file
+ERR_ENTRY_GENERATOR_CANNOT_INSTANTIATE_NEW_TAG=An unexpected error occurred \
+ while trying to create a new instance of tag %s referenced on line %d of the \
+ template file:  %s
+ERR_ENTRY_GENERATOR_TAG_INVALID_FORMAT_STRING=Cannot parse value "%s" as an \
+ valid format string for tag %s on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_NO_RANDOM_TYPE_ARGUMENT=The random tag on line %d \
+ of the template file does not include an argument to specify the type of \
+ random value that should be generated
+WARN_ENTRY_GENERATOR_TAG_WARNING_EMPTY_VALUE=The value generated from the \
+ random tag on line %d of the template file will always be an empty string
+ERR_ENTRY_GENERATOR_TAG_UNKNOWN_RANDOM_TYPE=The random tag on line %d of \
+ the template file references an unknown random type of %s
+ERR_ENTRY_GENERATOR_COULD_NOT_FIND_TEMPLATE_FILE=Could not find template \
+ file %s
+ERR_ENTRY_GENERATOR_COULD_NOT_FIND_NAME_FILE=Could not find names resource \
+ file %s
+ERR_ENTRY_GENERATOR_TAG_CANNOT_FIND_FILE=Cannot find file %s referenced by \
+ tag %s on line %d of the template file
+ERR_ENTRY_GENERATOR_TAG_INVALID_FILE_ACCESS_MODE=Invalid file access mode \
+ %s for tag %s on line %d of the template file.  It must be either \
+ "sequential" or "random"
+ERR_ENTRY_GENERATOR_TAG_CANNOT_READ_FILE=An error occurred while trying to \
+ read file %s referenced by tag %s on line %d of the template file:  %s
+ERR_ENTRY_GENERATOR_INCOMPLETE_TAG=Line %d of the template file contains an \
+ incomplete tag that starts with either '<' or '{' but does get closed
+ERR_ENTRY_GENERATOR_TAG_NOT_ALLOWED_IN_BRANCH=Tag %s referenced on line %d \
+ of the template file is not allowed for use in branch definitions
+ERR_ENTRY_GENERATOR_TEMPLATE_INVALID_PARENT_TEMPLATE=The parent template %s \
+ referenced on line %d for template %s is invalid because the referenced \
+ parent template is not defined before the template that extends it
+ERR_ENTRY_GENERATOR_TAG_LIST_NO_ARGUMENTS=The list tag on line %d of the \
+ template file does not contain any arguments to specify the list values.  At \
+ least one list value must be provided
+WARN_ENTRY_GENERATOR_TAG_LIST_INVALID_WEIGHT=The list tag on line %d of \
+ the template file contains item '%s' that includes a semicolon but that \
+ semicolon is not followed by an integer.  The semicolon will be assumed to be \
+ part of the value and not a delimiter to separate the value from its relative \
+ weight
+ERR_ENTRY_GENERATOR_EXCEPTION_DURING_PARSE=An error occurred while \
+ attempting to parse the template file:  %s
+ERR_ADDRESSMASK_PREFIX_DECODE_ERROR=Cannot decode the provided \
+ address mask prefix because an invalid value was specified. The permitted \
+ values for IPv4are 0 to32 and for IPv6 0 to128
+ERR_ADDRESSMASK_WILDCARD_DECODE_ERROR=Cannot decode the provided \
+ address mask because an prefix mask was specified with an wild card "*" match \
+ character
+ERR_ADDRESSMASK_FORMAT_DECODE_ERROR=Cannot decode the provided \
+ address mask because the it has an invalid format
+WARN_ATTR_CONFLICTING_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because more than \
+ one time units are not allowed
+WARN_ATTR_INVALID_RELATIVE_TIME_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because the \
+ character '%c' is not allowed. The acceptable values are s (second), m (minute), \
+ h (hour), d (day) and w (week)
+WARN_ATTR_DUPLICATE_SECOND_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for s (second) specification
+WARN_ATTR_DUPLICATE_MINUTE_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for m (minute) specification
+WARN_ATTR_DUPLICATE_HOUR_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for h (hour) specification
+WARN_ATTR_INVALID_DAY_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid day specification
+WARN_ATTR_DUPLICATE_DAY_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for DD (day of month) specification
+WARN_ATTR_INVALID_MONTH_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid month specification
+WARN_ATTR_INVALID_MINUTE_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid minute specification
+WARN_ATTR_INVALID_HOUR_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid hour specification
+WARN_ATTR_INVALID_SECOND_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid second specification
+WARN_ATTR_DUPLICATE_MONTH_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for MM (month) specification
+WARN_ATTR_INVALID_YEAR_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because "%d" is not \
+ a valid year specification
+WARN_ATTR_DUPLICATE_YEAR_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because there is \
+ conflicting value "%d" for YYYY (year) specification
+WARN_ATTR_INVALID_PARTIAL_TIME_ASSERTION_FORMAT=The provided \
+ value "%s" could not be parsed as a valid assertion value because the \
+ character '%c' is not allowed. The acceptable values are s (second), \
+ m (minute), h (hour), D (date), M (month) and Y (year)
+ERR_INVALID_COMPACTED_UNSIGNED_INT=Expected a compacted unsigned int (value less than %d), \
+ but got a compacted unsigned long: %d
+ERR_TRANSACTION_ID_CONTROL_BAD_OID=Cannot decode the provided \
+ control as a transaction id control because it contained the OID '%s', \
+ when '%s' was expected
+ERR_TRANSACTION_ID_CONTROL_DECODE_NULL=Cannot decode the provided ASN.1 \
+ element as a transaction id control because it did not have a value, when \
+ a value must always be provided
+ERR_TEMPLATE_FILE_INVALID_LEADING_SPACE=Unable to parse line %d ("%s") from the \
+ template file because the line started with a space but there were no previous \
+ lines in the entry to which this line could be appended
+
+# Labels for generated documentation
+DOC_LOCALE_TAG=Code tag: %s
+DOC_LOCALE_OID=Collation order object identifier: %s
+DOC_LOCALE_SECTION_TITLE=Directory Support For Locales and Language Subtypes
+DOC_LOCALE_SECTION_INFO=OpenDJ software supports the following locales \
+  with their associated language and country codes \
+  and their collation order object identifiers. \
+  Locale support depends on the Java Virtual Machine used at run time. \
+  The following list reflects all supported locales.
+DOC_SUPPORTED_LOCALES_TITLE=Supported Locales
+DOC_SUPPORTED_LOCALES_INDEXTERM=Locales
+DOC_SUPPORTED_SUBTYPES_TITLE=Supported Language Subtypes
+DOC_SUPPORTED_SUBTYPES_INDEXTERM=Language subtypes
+DOC_LANGUAGE_SH=Serbo-Croatian
+
+LDAP_CONNECTION_CONNECT_TIMEOUT=The connection attempt to server %s has failed \
+ because the connection timeout period of %d ms was exceeded
+LOAD_BALANCER_EVENT_LISTENER_LOG_ONLINE=Connection factory '%s' is now operational
+LOAD_BALANCER_EVENT_LISTENER_LOG_OFFLINE=Connection factory '%s' is no longer operational: %s
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_de.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_de.properties
new file mode 100644
index 0000000..6d0ae76
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_de.properties
@@ -0,0 +1,87 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=Der angegebene Wert "%s" ist keine g\u00fcltige L\u00e4nderzeichenkette, da die L\u00e4nge nicht exakt zwei Zeichen betr\u00e4gt
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=Der angegebene Wert "%s" ist keine g\u00fcltige Liefermethode, da er keine Elemente enth\u00e4lt
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=Der angegebene Wert "%s" ist keine g\u00fcltige Liefermethode, da "%s" keine g\u00fcltige Methode ist
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da das Zeichen '%c' an Position '%d' in einem Attributnamen unzul\u00e4ssig ist
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da er einen RDN mit leerem Attributnamen enth\u00e4lt
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da das letzte nicht-Leerzeichen Teil des Attributnamens '%s' ist
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da das n\u00e4chste nicht-Leerzeichen nach dem Attributnamen "%s" ein Gleichheitszeichen sein m\u00fcsste, stattdessen aber '%c' ist
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da ein Attributwert zwar mit einem Rautezeichen (#) beginnt, diesem aber kein positives Vielfaches von zwei hexadezimalen Ziffern folgt
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da ein Attributwert zwar mit einem Rautezeichen (#) beginnt, aber auch das Zeichen %c enth\u00e4lt, welches keine g\u00fcltige hexadezimale Ziffer darstellt
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da ein unerwarteter Fehler beim Versuch aufgetreten ist, einen Attributwert von einem der RDN-Komponenten zu parsen: "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da einer der RDN-Komponenten einen Wert mit Anf\u00fchrungszeichen enth\u00e4lt, bei dem das schlie\u00dfende Anf\u00fchrungszeichen fehlt
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name (DN) geparst werden, da einer der RDN-Komponenten einen Wert mit einer vereinzelten hexadezimalen Ziffer enth\u00e4lt, auf die keine zweite hexadezimale Ziffer folgt
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=Der angegebene Wert "%s" kann nicht als Attributtypbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=Der angegebene Wert ist keine g\u00fcltige Telefonnummer, da er leer oder Null ist
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=Der angegebene Wert "%s" ist keine g\u00fcltige Telefonnummer, da die strikte Telefonnummerpr\u00fcfung aktiviert ist und der Wert nicht in \u00dcbereinstimmung mit der ITU-T E.123-Spezifikation mit einem Pluszeichen beginnt
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=Der angegebene Wert "%s" ist keine g\u00fcltige Telefonnummer, da die strikte Telefonnummerpr\u00fcfung aktiviert ist und das Zeichen %s an Position %d gem\u00e4\u00df der ITU-T E.123-Spezifikation unzul\u00e4ssig ist
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=Der angegebene Wert "%s" ist keine g\u00fcltige Telefonnummer, da er keine numerischen Ziffern enth\u00e4lt
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=Der angegebene Wert ist keine g\u00fcltige numerische Zeichenkette, da er keine Zeichen enth\u00e4lt.  Ein numerischer Zeichenkettenwert muss mindestens eine numerische Ziffer oder ein Leerzeichen enthalten
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=Der angegebene Wert "%s" kann nicht als Attributsyntaxbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=Der angegebene Wert %s ist zu kurz, um ein g\u00fcltiger UTC-Zeitwert zu sein
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da das Zeichen %s im Jahrhundert bzw. in der Jahresangabe unzul\u00e4ssig ist
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltige Monatsangabe ist 
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltige Tagesangabe ist 
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltige Stundenangabe ist 
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltige Minutenangabe ist 
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da er das unzul\u00e4ssige Zeichen %s an Position %d enth\u00e4lt
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltige Sekundenangabe ist 
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=Der angegebene Wert %s ist kein g\u00fcltiger UTC-Zeitwert, da %s keine g\u00fcltiger GMT-Offset ist 
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=Der angegebene Wert %s kann nicht als g\u00fcltige UTC-Zeit geparst werden: %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=Der angegebene Wert "%s" kann nicht als DIT-Inhaltsregelbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=Der angegebene Wert "%s" kann nicht als Namensformbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%c' gefunden wurde
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=Der angegebene Wert "%s" kann nicht als \u00dcbereinstimmungsregelbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=Der angegebene Wert "%s" kann nicht als \u00dcbereinstimmungsregelbeschreibung geparst werden, da er nicht die Attributsyntax angibt, der er zugeordnet ist
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=Der angegebene Wert "%s" kann nicht als Verwendungsbeschreibung der \u00dcbereinstimmungsregel geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=Der angegebene Wert "%s" kann nicht als Verwendungsbeschreibung der \u00dcbereinstimmungsregel geparst werden, da er nicht den Satz von Attributtypen angibt, die mit dem zugeordneten OID verwendet werden k\u00f6nnen
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=Der angegebene Wert "%s" kann nicht als DIT-Strukturregelbeschreibung geparst werden, da eine \u00f6ffnende Klammer an Position %d erwartet, stattdessen aber das Zeichen '%s' gefunden wurde
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=Der angegebene Wert "%s" kann nicht als DIT-Strukturregelbeschreibung geparst werden, da er auf die unbekannte Namensform %s verweist
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=Der angegebene Wert "%s" kann nicht als DIT-Strukturregelbeschreibung geparst werden, da er auf die unbekannte Regel-ID %d f\u00fcr eine \u00fcbergeordnete DIT-Strukturregel verweist
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=Der angegebene Wert "%s" kann nicht als DIT-Strukturregelbeschreibung geparst werden, da er nicht die Namensform f\u00fcr die Regel angibt
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=Der angegebene Wert "%s" ist zu kurz, um ein g\u00fcltiger Telexnummerwert zu sein
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=Der angegebene Wert "%s" enth\u00e4lt keine g\u00fcltige Telexnummer, da das Zeichen %s an Position %d kein g\u00fcltiges druckbares Zeichenkettenzeichen ist
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=Der angegebene Wert "%s" enth\u00e4lt keine g\u00fcltige Telexnummer, da das Zeichen %s an Position %d weder ein g\u00fcltiges druckbares Zeichenkettenzeichen noch ein Dollarzeichen enh\u00e4lt, das die Telexnummerkomponenten trennt
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=Der angegebene Wert "%s" enth\u00e4lt keine g\u00fcltige Telexnummer, da das Ende des Werts gefunden wurde, bevor eine durch drei Dollarzeichen getrennte druckbare Zeichenkette gelesen werden konnte
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=Der angegebene Wert kann nicht als g\u00fcltige Faxnummer geparst werden, da er leer ist
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=Der angegebene Wert "%s" kann nicht als g\u00fcltige Faxnummer geparst werden, da das Zeichen %s an Position %d kein g\u00fcltiges druckbares Zeichenkettenzeichen ist
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=Der angegebene Wert "%s" kann nicht als g\u00fcltige Faxnummer geparst werden, da er auf ein Dollarzeichen endet, diesem aber ein Faxparameter folgen m\u00fcsste
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=Der angegebene Wert "%s" kann nicht als g\u00fcltige Faxnummer geparst werden, da die Zeichenkette %s zwischen den Positionen %d und %d kein g\u00fcltiger Faxparameter ist
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Namens- und optionaler UID-Wert geparst werden, da ein Fehler aufgetreten ist beim Versuch, den DN-Teil zu parsen: %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Namens- und optionaler UID-Wert geparst werden, da der UID-Teil die unzul\u00e4ssige bin\u00e4re Ziffer %s an Position %d enth\u00e4lt
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=Der angegebene Wert kann nicht als g\u00fcltige Teletexendger\u00e4te-ID geparst werden, da er leer ist
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=Der angegebene Wert "%s" kann nicht als g\u00fcltige Teletexendger\u00e4te-ID geparst werden, da das Zeichen %s an Position %d kein g\u00fcltiges druckbares Zeichenkettenzeichen ist
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=Der angegebene Wert "%s" kann nicht als g\u00fcltige Teletexendger\u00e4te-ID geparst werden, da er auf ein Dollarzeichen endet, diesem aber ein TTX-Parameter folgen m\u00fcsste
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=Der angegebene Wert "%s" kann nicht als g\u00fcltige Teletexendger\u00e4te-ID geparst werden, da die Parameterzeichenkette keinen Doppelpunkt enth\u00e4lt, der den Namen vom Wert trennt
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=Der angegebene Wert "%s" kann nicht als g\u00fcltige Teletexendger\u00e4te-ID geparst werden, da die Zeichenkette "%s" kein g\u00fcltiger TTX-Parametername ist
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=Der angegebene Wert kann nicht als sonstiger Mailboxwert geparst werden, da er leer ist
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=Der angegebene Wert "%s" kann nicht als sonstiger Mailboxwert geparst werden, da kein Mailboxtyp vor dem Dollarzeichen angegeben ist
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=Der angegebene Wert "%s" kann nicht als sonstiger Mailboxwert geparst werden, da der Mailboxtyp das unzul\u00e4ssige Zeichen %s an Position %d enth\u00e4lt
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=Der angegebene Wert "%s" kann nicht als sonstiger Mailboxwert geparst werden, da keine Mailbox hinter dem Dollarzeichen angegeben ist
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=Der angegebene Wert "%s" kann nicht als sonstiger Mailboxwert geparst werden, da die Mailbox das unzul\u00e4ssige Zeichen %s an Position %d enth\u00e4lt
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da der Kriterienteil %s das unzul\u00e4ssige Zeichen %c an Position %d enth\u00e4lt
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da beim Kriterienteil %s die zur \u00f6ffnenden Klammer geh\u00f6rende schlie\u00dfende Klammer fehlt
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da der Kriterienteil %s mit einem Fragezeichen beginnt, diesem aber als Zeichenkette weder "true" noch "false" folgen
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da der Kriterienteil %s kein Dollarzeichen enth\u00e4lt, das den Attributtyp vom \u00dcbereinstimmungstyp trennt
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da im Kriterienteil %s kein Attributtyp vor dem Dollarzeichen festgelegt ist
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da im Kriterienteil %s kein \u00dcbereinstimmungstyp nach dem Dollarzeichen festgelegt ist
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=Der angegebene Wert "%s" kann nicht als Richtwert geparst werden, da im Kriterienteil %s ein ung\u00fcltiger \u00dcbereinstimmungstyp an Position %d festgelegt ist
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=Der angegebene Wert "%s" kann nicht als verbesserter Richtwert geparst werden, da er kein Rautzeichen (#) enth\u00e4lt, das das Kriterium vom Umfang trennt
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=Der angegebene Wert "%s" kann nicht als verbesserter Richtwert geparst werden, da kein Umfang nach dem Rautezeichen (#) angegeben ist
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=Der angegebene Wert "%s" kann nicht als verbesserter Richtwert geparst werden, da der angegebene Umfang %s ung\u00fcltig ist
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=Der angegebene Wert "%s" kann nicht als verbesserter Richtwert geparst werden, da keine Kriterien zwischen den Rautezeichen (#) festgelegt sind
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=Die DIT-Inhaltsregel "%s" ist ung\u00fcltig, da sie die Verwendung des Attributtyps %s verbietet, der von der zugeordneten strukturellen Objektklasse %s erfordert wird
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=Die DIT-Inhaltsregel "%s" ist ung\u00fcltig, da sie die Verwendung des Attributtyps %s verbietet, der von der zugeordneten Hilfsobjektklasse %s erfordert wird
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=Der angegebene Wert "%s" kann nicht als g\u00fcltiger Distinguished Name geparst werden, da ein Attributswert mit einem Zeichen an Position '%d' vermieden werden muss
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_es.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_es.properties
new file mode 100644
index 0000000..ca7fa20
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_es.properties
@@ -0,0 +1,86 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=El valor proporcionado "%s" no es una cadena de pa\u00edses v\u00e1lida porque no tiene una longitud de dos caracteres. 
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=El valor proporcionado "%s" no es un valor de m\u00e9todo de entrega v\u00e1lido porque no contiene elementos
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=El valor proporcionado "%s" no es un valor de m\u00e9todo de entrega v\u00e1lido porque "%s" no es un m\u00e9todo v\u00e1lido
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque no se permite el car\u00e1cter '%c'  en la posici\u00f3n %d en un nombre de atributo
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque conten\u00eda un RDN que a su vez conten\u00eda un nombre de atributo vac\u00edo
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque el \u00faltimo car\u00e1cter distinto a un espacio formaba parte del nombre de atributo '%s'
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque el siguiente car\u00e1cter distinto a un espacio detr\u00e1s del nombre de atributo "%s" deber\u00eda haber sido un signo igual en lugar de '%c'
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque un valor de atributo comenzaba con un signo de almohadilla (#), sin embargo, no estaba seguido por un m\u00faltiplo positivo de dos d\u00edgitos hexadecimales
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque un valor de atributo comenzaba con un signo de almohadilla (#) sin embargo conten\u00eda un car\u00e1cter %c que no era un d\u00edgito hexadecimal v\u00e1lido
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque se ha producido un fallo inesperado al intentar analizar un valor de atributo desde uno de los componentes de RDN: "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque uno de los componentes de RDN inclu\u00eda un valor citado que no ten\u00eda la comilla de cierre correspondiente
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque uno de los componentes de RDN inclu\u00eda un valor con un d\u00edgito hexadecimal escapado que no estaba seguido por un segundo d\u00edgito hexadecimal
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de tipo de atributo porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar del par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=El valor proporcionado no es un n\u00famero de tel\u00e9fono v\u00e1lido porque est\u00e1 vac\u00edo o es nulo
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=El valor proporcionado "%s" no es un n\u00famero de tel\u00e9fono v\u00e1lido porque est\u00e1 habilitada la comprobaci\u00f3n estricta de n\u00famero de tel\u00e9fono y el valor no comienza con un signo m\u00e1s (+) en cumplimiento de la especificaci\u00f3n E.123 de ITU-T
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=El valor proporcionado "%s" no es un n\u00famero de tel\u00e9fono v\u00e1lido porque est\u00e1 habilitada la comprobaci\u00f3n estricta de n\u00famero de tel\u00e9fono y la especificaci\u00f3n E.123 de ITU-T no permite el car\u00e1cter %s en la posici\u00f3n %d
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=El valor proporcionado "%s" no es un n\u00famero de tel\u00e9fono v\u00e1lido porque no contiene d\u00edgitos num\u00e9ricos
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=El valor proporcionado no era una cadena num\u00e9rica v\u00e1lida porque no conten\u00eda caracteres.  Un valor de cadena num\u00e9rico debe contener un espacio o un d\u00edgito num\u00e9rico como m\u00ednimo
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de sintaxis de atributo porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar del par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=El valor proporcionado %s es demasiado corto para considerarse un valor de tiempo UTC v\u00e1lido
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=El valor proporcionado %s  no es un valor de tiempo UTC v\u00e1lido porque no se permite el car\u00e1cter %s en la especificaci\u00f3n de a\u00f1o o siglo
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es una especificaci\u00f3n de mes v\u00e1lida
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es una especificaci\u00f3n de d\u00eda v\u00e1lida
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es una especificaci\u00f3n de hora v\u00e1lida
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es una especificaci\u00f3n de minuto v\u00e1lida
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque contiene un car\u00e1cter %s no v\u00e1lido en la posici\u00f3n %d
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es una especificaci\u00f3n de segundo v\u00e1lida
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=El valor proporcionado %s no es un valor de tiempo UTC v\u00e1lido porque %s no es un ajuste GMT v\u00e1lido
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=No se pudo analizar el valor proporcionado %s como tiempo UTC v\u00e1lido: %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de regla de contenido del DIT porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar del par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de formato de nombre porque se encontr\u00f3 un car\u00e1cter '%3$c' en lugar de un par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de regla de coincidencia porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar de un par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=No se pudo analizar el valor proporcionado "%s" como descripci\u00f3n de regla de coincidencia porque no especifica la sintaxis de atributo con la que est\u00e1 asociada
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de uso de regla de coincidencia porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar de un par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=No se pudo analizar el valor proporcionado "%s" como descripci\u00f3n de regla de coincidencia porque no especifica el conjunto de tipos de atributo que se pueden utilizar con el OID asociado
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=No se pudo analizar el valor proporcionado "%1$s" como descripci\u00f3n de regla de estructura del DIT porque se encontr\u00f3 un car\u00e1cter '%3$s' en lugar del par\u00e9ntesis de apertura en la posici\u00f3n %2$d
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=No se pudo analizar el valor proporcionado "%s" como descripci\u00f3n de regla de estructura del DIT porque hac\u00eda referencia a un formato de nombre desconocido %s
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=No se pudo analizar el valor proporcionado "%s" como descripci\u00f3n de regla de estructura del DIT porque hac\u00eda referencia a un ID de regla desconocido %d para una regla de estructura del DIT superior
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=No se pudo analizar el valor proporcionado "%s" como descripci\u00f3n de regla de estructura del DIT porque no especific\u00f3 el formato de nombre para la regla
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=El valor proporcionado "%s" es demasiado corto para considerarse un valor de n\u00famero de t\u00e9lex v\u00e1lido
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=El valor proporcionado "%s" no tiene un n\u00famero de t\u00e9lex v\u00e1lido porque un car\u00e1cter %s en la posici\u00f3n %d no era un car\u00e1cter de cadena imprimible v\u00e1lido
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=El valor proporcionado "%s" no tiene un n\u00famero de t\u00e9lex v\u00e1lido porque un car\u00e1cter %s en la posici\u00f3n %d no era ni un car\u00e1cter de cadena imprimible v\u00e1lido ni un signo de d\u00f3lar para separar los componentes de n\u00famero de t\u00e9lex
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=El valor proporcionado "%s" no tiene un n\u00famero de t\u00e9lex v\u00e1lido porque se encontr\u00f3 el final del valor antes de que se pudieran leer tres cadenas imprimibles delimitadas por el s\u00edmbolo del d\u00f3lar
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=No se pudo analizar el valor proporcionado como n\u00famero de tel\u00e9fono de facs\u00edmil v\u00e1lido porque estaba vac\u00edo
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=No se pudo analizar el valor proporcionado "%s" como n\u00famero de tel\u00e9fono de facs\u00edmil v\u00e1lido porque un car\u00e1cter %s en la posici\u00f3n %d no era un car\u00e1cter de cadena imprimible v\u00e1lido
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=No se pudo analizar el valor proporcionado "%s" como n\u00famero de tel\u00e9fono de facs\u00edmil v\u00e1lido porque finaliza con un signo de d\u00f3lar que deber\u00eda estar seguido por un par\u00e1metro de fax
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=No se pudo analizar el valor proporcionado "%s" como n\u00famero de tel\u00e9fono de facs\u00edmil v\u00e1lido porque la cadena "%s" entre las posiciones %d y %d no era un par\u00e1metro de fax v\u00e1lido
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=No se pudo analizar el valor proporcionado "%s" como nombre v\u00e1lido y valor UID opcional porque se produjo un error al intentar analizar la parte de ND: %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=No se pudo analizar el valor proporcionado "%s" como nombre v\u00e1lido y valor UID opcional porque la parte de UID conten\u00eda un d\u00edgito binario no v\u00e1lido %s en la posici\u00f3n %d
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=No se pudo analizar el valor proporcionado como identificador de terminal de teletexto v\u00e1lido porque estaba vac\u00edo
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=No se pudo analizar el valor proporcionado "%s" como identificador de terminal de teletexto v\u00e1lido porque un car\u00e1cter %s en la posici\u00f3n %d no era un car\u00e1cter de cadena imprimible v\u00e1lido
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=No se pudo analizar el valor proporcionado "%s" como identificador de terminal de teletexto v\u00e1lido porque finaliza con un signo de d\u00f3lar que deber\u00eda ser seguido por un par\u00e1metro de TTX
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=No se pudo analizar el valor proporcionado "%s" como identificador de terminal de teletexto v\u00e1lido porque la cadena de par\u00e1metros no conten\u00eda un signo de dos puntos para separar el nombre del valor
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=No se pudo analizar el valor proporcionado "%s" como identificador de terminal de teletexto v\u00e1lido porque la cadena "%s" no es un nombre de par\u00e1metro TTX v\u00e1lido
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=No se pudo analizar el valor proporcionado como otro valor de buz\u00f3n porque estaba vac\u00edo
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=No se pudo analizar el valor proporcionado "%s" como otro valor de buz\u00f3n porque no hab\u00eda ning\u00fan tipo de buz\u00f3n delante del signo de d\u00f3lar
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=No se pudo analizar el valor proporcionado "%s" como otro valor de buz\u00f3n porque el tipo de buz\u00f3n conten\u00eda un car\u00e1cter no v\u00e1lido %s en la posici\u00f3n %d. 
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=No se pudo analizar el valor proporcionado "%s" como otro valor de buz\u00f3n porque no hab\u00eda ning\u00fan buz\u00f3n detr\u00e1s del signo de d\u00f3lar
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=No se pudo analizar el valor proporcionado "%s" como otro valor de buz\u00f3n porque el buz\u00f3n conten\u00eda un car\u00e1cter no v\u00e1lido %s en la posici\u00f3n %d. 
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s conten\u00eda un car\u00e1cter no v\u00e1lido %c en la posici\u00f3n %d
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s no conten\u00eda un par\u00e9ntesis de cierre que correspond\u00eda al par\u00e9ntesis de apertura inicial
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s comenzaba con un signo de interrogaci\u00f3n aunque no seguido por la cadena "true" (verdadero) o "false" (falso)
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s no conten\u00eda un signo de d\u00f3lar para separar el tipo de atributo del tipo de coincidencia
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s no especificaba un tipo de atributo delante del signo de d\u00f3lar
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s no especificaba un tipo de coincidencia detr\u00e1s del signo de d\u00f3lar
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda porque la parte de criterios %s ten\u00eda un tipo de coincidencia no v\u00e1lido que comenzaba en la posici\u00f3n %d
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda mejorado porque no ten\u00eda un car\u00e1cter de almohadilla (#) para separar los criterios del \u00e1mbito
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda mejorado porque no se proporcion\u00f3 ning\u00fan \u00e1mbito detr\u00e1s del car\u00e1cter almohadilla (#) final
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda mejorado porque el \u00e1mbito %s especificado no era v\u00e1lido
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=No se pudo analizar el valor proporcionado "%s" como valor de gu\u00eda mejorado porque no se especific\u00f3 ning\u00fan criterio entre los caracteres de almohadilla (#). 
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=La regla de contenido del DIT "%s" no es v\u00e1lida porque proh\u00edbe el uso del tipo de atributo %s que necesita la clase de objeto estructural asociada %s
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=La regla de contenido del DIT "%s" no es v\u00e1lida porque proh\u00edbe el uso del tipo de atributo %s que necesita la clase de objeto auxiliar asociada %s
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=No se pudo analizar el valor proporcionado "%s" como nombre \u00fanico v\u00e1lido porque un valor de atributo empezaba con un car\u00e1cter en la posici\u00f3n %d al que hay que aplicarle caracteres de escape
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
new file mode 100644
index 0000000..2b044c5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
@@ -0,0 +1,90 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions copyright 2013 ForgeRock AS.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=La valeur indiqu\u00e9e "%s" n'est pas une cha\u00eene de pays valide car elle n'a pas une longueur de deux caract\u00e8res exactement
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=La valeur indiqu\u00e9e "%s" n'est pas une m\u00e9thode de distribution valide car elle ne contient aucun \u00e9l\u00e9ment
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=La valeur indiqu\u00e9e "%s" n'est pas une m\u00e9thode de distribution valide car "%s" n'est pas une m\u00e9thode valide
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car le caract\u00e8re '%c' \u00e0 la position %d n'est pas autoris\u00e9 dans un nom d'attribut
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car il contient un NRD contenant un nom d'attribut vide
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car le dernier caract\u00e8re autre qu'un espace fait partie du nom d'attribut '%s'
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car le caract\u00e8re suivant autre qu'un espace apr\u00e8s le nom d'attribut "%s" devrait \u00eatre un signe \u00e9gal au lieu de '%c'
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car une valeur d'attribut commence par un di\u00e8se (#) sans \u00eatre suivie d'un multiple positif de deux chiffres hexad\u00e9cimaux 
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car une valeur d'attribut commence par un di\u00e8se (#) mais contient un caract\u00e8re %c qui n'est pas un chiffre hexad\u00e9cimal valide
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car un \u00e9chec inattendu s'est produit lors d'une tentative d'analyse d'une valeur d'attribut de l'un des composants du NRD\u00a0:  "%s" 
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car un des composants du NRD inclut une valeur cit\u00e9e sans le guillemet de fermeture correspondant
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom distinctif valide car un des composants du NRD inclut une valeur contenant un chiffre hexad\u00e9cimal neutralis\u00e9 qui n'est pas suivi d'un second chiffre hexad\u00e9cimal
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de type d'attribut car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=La valeur indiqu\u00e9e n'est pas un num\u00e9ro de t\u00e9l\u00e9phone valide car elle est vide ou null
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=La valeur indiqu\u00e9e "%s" n'est pas un num\u00e9ro de t\u00e9l\u00e9phone valide car la v\u00e9rification stricte des num\u00e9ros de t\u00e9l\u00e9phone est activ\u00e9e et la valeur ne commence pas par un signe plus respectant la sp\u00e9cification ITU-T E.123
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=La valeur indiqu\u00e9e "%s" n'est pas un num\u00e9ro de t\u00e9l\u00e9phone valide car la v\u00e9rification stricte des num\u00e9ros de t\u00e9l\u00e9phone est activ\u00e9e et le caract\u00e8re %s \u00e0 la position %d n'est pas autoris\u00e9 par la sp\u00e9cification ITU-T E.123
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=La valeur indiqu\u00e9e "%s" n'est pas un num\u00e9ro de t\u00e9l\u00e9phone valide car il ne contient aucun chiffre num\u00e9rique
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=La valeur indiqu\u00e9e n'est pas une cha\u00eene num\u00e9rique valide car elle ne contient aucun caract\u00e8re.  Une valeur de cha\u00eene num\u00e9rique doit contenir au moins un chiffre num\u00e9rique ou un espace
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de syntaxe d'attribut car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=La valeur indiqu\u00e9e %s est trop courte pour \u00eatre une valeur de temps UTC valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car le caract\u00e8re '%s' n'est pas autoris\u00e9 dans la sp\u00e9cification de si\u00e8cle ou d'ann\u00e9e
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas une sp\u00e9cification de mois valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas une sp\u00e9cification de jour valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas une sp\u00e9cification d'heure valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas une sp\u00e9cification de minute valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car elle contient un caract\u00e8re invalide %s \u00e0 la position %d
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas une sp\u00e9cification de seconde valide
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=La valeur indiqu\u00e9e %s n'est pas une valeur de temps UTC valide car %s n'est pas un d\u00e9calage GMT valide
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=Impossible d'analyser la valeur indiqu\u00e9e %s en tant que temps UTC valide\u00a0:  %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de contenu DIT car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de formulaire de nom car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%c'
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de correspondance car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de correspondance car elle ne sp\u00e9cifie par la syntaxe d'attribut avec laquelle elle est associ\u00e9e
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description d'utilisation d'une r\u00e8gle de correspondance car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de correspondance car elle ne sp\u00e9cifie pas l'ensemble de types d'attributs \u00e0 utiliser avec l'OID associ\u00e9
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de structure DIT car une parenth\u00e8se ouverte devrait se trouver \u00e0 la position %d au lieu du caract\u00e8re '%s'
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de structure DIT car elle fait r\u00e9f\u00e9rence \u00e0 une forme de nom inconnu %s
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de structure DIT car elle fait r\u00e9f\u00e9rence \u00e0 un ID %d de r\u00e8gle inconnu pour une r\u00e8gle de structure DIT sup\u00e9rieure
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que description de r\u00e8gle de structure DIT car elle ne sp\u00e9cifie par le formulaire de nom pour la r\u00e8gle
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=La valeur indiqu\u00e9e "%s" est trop courte pour \u00eatre une valeur de num\u00e9ro de t\u00e9lex valide
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=La valeur indiqu\u00e9e "%s" ne contient pas un num\u00e9ro de t\u00e9lex valide car un caract\u00e8re %s \u00e0 la position %d n'est pas une cha\u00eene de caract\u00e8res imprimable valide
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=La valeur indiqu\u00e9e "%s" ne contient pas un num\u00e9ro de t\u00e9lex valide car un caract\u00e8re %s \u00e0 la position %d n'est ni une cha\u00eene de caract\u00e8res imprimable valide, ni le signe du dollar pour s\u00e9parer les composants du num\u00e9ro de t\u00e9lex
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=La valeur indiqu\u00e9e "%s" ne contient pas un num\u00e9ro de t\u00e9lex valide car la fin de la valeur a \u00e9t\u00e9 atteinte avant que trois cha\u00eenes imprimables d\u00e9limit\u00e9s par des dollars puissent \u00eatre lues
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=Impossible d'analyser la valeur indiqu\u00e9e en tant que num\u00e9ro de t\u00e9l\u00e9copie valide car elle est vide
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que num\u00e9ro de t\u00e9l\u00e9copie valide car le caract\u00e8re %s \u00e0 la position %d n'est pas une cha\u00eene de caract\u00e8res imprimable valide
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que num\u00e9ro de t\u00e9l\u00e9copie valide car elle termine par un signe dollar, mais ce signe devrait \u00eatre suivi d'un param\u00e8tre de fax
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que num\u00e9ro de t\u00e9l\u00e9copie valide car la cha\u00eene "%s" entre les positions %d et %d n'est pas un param\u00e8tre de fax valide
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom et valeur UID facultative valides car une erreur s'est produite lors de la tentative d'analyse de la partie DN\u00a0:  %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que nom et valeur UID facultative valides car la partie UID contient un chiffre binaire ill\u00e9gal %s \u00e0 la position %d
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=Impossible d'analyser la valeur indiqu\u00e9e en tant qu'identifiant de r\u00e9cepteur de teletexte valide car elle est vide
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'identifiant de r\u00e9cepteur de teletexte valide car le caract\u00e8re %s \u00e0 la position %d n'est pas une cha\u00eene de caract\u00e8res imprimable valide
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'identifiant de r\u00e9cepteur de teletexte valide car elle se termine par le symbole dollar, mais ce symbole devrait \u00eatre suivi d'un param\u00e8tre TTX
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'identifiant de r\u00e9cepteur de teletexte valide car la cha\u00eene de param\u00e8tres ne contient pas deux-points pour s\u00e9parer le nom de la valeur
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'identifiant de r\u00e9cepteur de teletexte valide car "%s" n'est pas un nom de param\u00e8tre TTX valide
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=Impossible d'analyser la valeur indiqu\u00e9e en tant qu'autre valeur de bo\u00eete aux lettres car elle est vide 
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'autre valeur de bo\u00eete aux lettres car il n'y a pas de type de bo\u00eete aux lettres avant le symbole dollar
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'autre valeur de bo\u00eete aux lettres car le type de bo\u00eete aux lettres contient un caract\u00e8re ill\u00e9gal %s \u00e0 la position %d
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'autre valeur de bo\u00eete aux lettres car il n'y a pas de bo\u00eete aux lettres apr\u00e8s le symbole dollar
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant qu'autre valeur de bo\u00eete aux lettres car la bo\u00eete aux lettres contient un caract\u00e8re ill\u00e9gal %s \u00e0 la position %d
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s contient un caract\u00e8re ill\u00e9gal %c \u00e0 la position %d
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s ne contient pas de parenth\u00e8se ferm\u00e9e correspondant \u00e0 la parenth\u00e8se ouverte initiale
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s commence par une interrogation mais n'est pas suivie de la cha\u00eene "true" ou "false" 
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s ne contient pas de signe dollar pour s\u00e9parer le type d'attribut du type de correspondance
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s ne sp\u00e9cifiait pas de type d'attribut avant le signe dollar
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res  %s ne sp\u00e9cifie pas de type de correspondance apr\u00e8s le symbole dollar
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide car la partie crit\u00e8res %s a un type de correspondance ill\u00e9gal commen\u00e7ant \u00e0 la position %d
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide optimis\u00e9e car elle n'a pas de caract\u00e8re di\u00e8se (#) pour s\u00e9parer les crit\u00e8res du domaine
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide optimis\u00e9e car aucun domaine n'est indiqu\u00e9 apr\u00e8s le caract\u00e8re di\u00e8se (#) final 
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide optimis\u00e9e car le domaine sp\u00e9cifi\u00e9 %s n'est pas valide
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=Impossible d'analyser la valeur indiqu\u00e9e "%s" en tant que valeur guide optimis\u00e9e car elle ne sp\u00e9cifie pas de crit\u00e8res entre les catact\u00e8res di\u00e8se (#)
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle associ\u00e9e %s
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle auxiliaire %s
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=Impossible d'analyser la valeur fournie "%s" en tant que nom valide distinctif car la valeur de l'attribut commence avec un caract\u00e8re en position %d qui doit \u00eatre \u00e9vit\u00e9
+ERR_NO_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais n'a retourn\u00e9 aucune entr\u00e9e alors qu'une entr\u00e9e \u00e9tait attendue
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 %s entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 plusieurs entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ja.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ja.properties
new file mode 100644
index 0000000..a6c1faa
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ja.properties
@@ -0,0 +1,86 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u3001\u9577\u3055\u304c\u6b63\u78ba\u306b 2 \u6587\u5b57\u3067\u306f\u306a\u3044\u305f\u3081\u3001\u6709\u52b9\u306a\u56fd\u6587\u5b57\u5217\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u3001\u8981\u7d20\u304c\u4e00\u5207\u542b\u307e\u308c\u3066\u3044\u306a\u3044\u305f\u3081\u3001\u6709\u52b9\u306a\u5b9f\u65bd\u30e1\u30bd\u30c3\u30c9\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12="%2$s" \u306f\u6709\u52b9\u306a\u30e1\u30bd\u30c3\u30c9\u3067\u306f\u306a\u3044\u305f\u3081\u3001\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u306f\u6709\u52b9\u306a\u5b9f\u65bd\u30e1\u30bd\u30c3\u30c9\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 '%2$c' \u3092\u5c5e\u6027\u540d\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306b\u306f\u7a7a\u306e\u5c5e\u6027\u540d\u3092\u542b\u3080 RDN \u304c\u5b58\u5728\u3057\u3066\u3044\u305f\u305f\u3081\u3001\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6700\u5f8c\u306e\u7a7a\u767d\u4ee5\u5916\u306e\u6587\u5b57\u304c\u5c5e\u6027\u540d '%s' \u306e\u4e00\u90e8\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u5c5e\u6027\u540d "%s" \u306e\u6b21\u306e\u7a7a\u767d\u4ee5\u5916\u306e\u6587\u5b57\u306f\u7b49\u53f7\u3067\u3042\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%c' \u3067\u3057\u305f
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u5c5e\u6027\u5024\u306f\u30b7\u30e3\u30fc\u30d7\u8a18\u53f7 (#) \u3067\u59cb\u307e\u3063\u3066\u3044\u307e\u3059\u304c\u3001\u7d9a\u304f\u6587\u5b57\u304c 2 \u6841\u306e 16 \u9032\u6570\u306e\u8907\u6570\u306e\u7d44\u307f\u5408\u308f\u305b\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u5c5e\u6027\u5024\u306f\u30b7\u30e3\u30fc\u30d7\u8a18\u53f7 (#) \u3067\u59cb\u307e\u3063\u3066\u3044\u307e\u3059\u304c\u3001\u6709\u52b9\u306a 16 \u9032\u6570\u3067\u306a\u3044\u6587\u5b57 %c \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3044\u305a\u308c\u304b\u306e RDN \u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304b\u3089\u306e\u5c5e\u6027\u5024\u3092\u89e3\u6790\u4e2d\u306b\u4e88\u671f\u3057\u306a\u3044\u969c\u5bb3\u304c\u767a\u751f\u3057\u307e\u3057\u305f:  "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3044\u305a\u308c\u304b\u306e RDN \u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u3067\u3001\u5bfe\u5fdc\u3059\u308b\u9589\u3058\u5f15\u7528\u7b26\u3092\u6301\u305f\u306a\u3044\u5f15\u7528\u7b26\u4ed8\u304d\u306e\u5024\u304c\u3042\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3044\u305a\u308c\u304b\u306e RDN \u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u5024\u3067\u3001\u30a8\u30b9\u30b1\u30fc\u30d7\u3055\u308c\u305f 16 \u9032\u6570\u306e\u3042\u3068\u306b 2 \u756a\u76ee\u306e 16 \u9032\u6570\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u5c5e\u6027\u578b\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=\u6307\u5b9a\u3055\u308c\u305f\u5024\u306f\u7a7a\u307e\u305f\u306f NULL \u3067\u3042\u308b\u305f\u3081\u3001\u6709\u52b9\u306a\u96fb\u8a71\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u6709\u52b9\u306a\u96fb\u8a71\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u53b3\u683c\u306a\u96fb\u8a71\u756a\u53f7\u30c1\u30a7\u30c3\u30af\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u304a\u308a\u3001\u5024\u306e\u5148\u982d\u304c ITU-T E.123 \u4ed5\u69d8\u306b\u6e96\u62e0\u3057\u305f\u30d7\u30e9\u30b9\u8a18\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u306f\u6709\u52b9\u306a\u96fb\u8a71\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u53b3\u683c\u306a\u96fb\u8a71\u756a\u53f7\u30c1\u30a7\u30c3\u30af\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u304a\u308a\u3001\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 %2$s \u306f ITU-T E.123 \u4ed5\u69d8\u3067\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u3001\u6570\u5b57\u304c\u4e00\u5207\u542b\u307e\u308c\u3066\u3044\u306a\u3044\u305f\u3081\u6709\u52b9\u306a\u96fb\u8a71\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=\u6307\u5b9a\u3055\u308c\u305f\u5024\u306f\u3001\u6587\u5b57\u304c\u4e00\u5207\u542b\u307e\u308c\u3066\u3044\u306a\u3044\u305f\u3081\u306b\u6709\u52b9\u306a\u6570\u5024\u6587\u5b57\u5217\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002  \u6570\u5b57\u6587\u5b57\u5217\u306e\u5024\u306b\u306f\u30011 \u3064\u4ee5\u4e0a\u306e\u6570\u5b57\u307e\u305f\u306f\u7a7a\u767d\u3092\u542b\u3081\u3066\u304f\u3060\u3055\u3044
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u5c5e\u6027\u69cb\u6587\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u77ed\u3059\u304e\u308b\u305f\u3081\u3001\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u6587\u5b57\u3092\u4e16\u7d00\u3084\u5e74\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u3092\u6708\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u3092\u65e5\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u3092\u6642\u9593\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u3092\u5206\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=\u6307\u5b9a\u3055\u308c\u305f\u5024 %1$s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u4f4d\u7f6e %3$d \u306b\u7121\u52b9\u306a\u6587\u5b57 %2$s \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3059
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u3092\u79d2\u306e\u6307\u5b9a\u306b\u4f7f\u7528\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u306f\u6709\u52b9\u306a UTC \u6642\u9593\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002%s \u306f\u6709\u52b9\u306a GMT \u30aa\u30d5\u30bb\u30c3\u30c8\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=\u6307\u5b9a\u3055\u308c\u305f\u5024 %s \u3092\u6709\u52b9\u306a UTC \u6642\u9593\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f:  %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f DIT \u30b3\u30f3\u30c6\u30f3\u30c4\u898f\u5247\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u540d\u524d\u66f8\u5f0f\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%c' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30de\u30c3\u30c1\u30f3\u30b0\u30eb\u30fc\u30eb\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30de\u30c3\u30c1\u30f3\u30b0\u30eb\u30fc\u30eb\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u95a2\u9023\u4ed8\u3051\u3089\u308c\u305f\u5c5e\u6027\u69cb\u6587\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30de\u30c3\u30c1\u30f3\u30b0\u30eb\u30fc\u30eb\u306e\u4f7f\u7528\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30de\u30c3\u30c1\u30f3\u30b0\u30eb\u30fc\u30eb\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u95a2\u9023\u4ed8\u3051\u3089\u308c\u305f OID \u3067\u4f7f\u7528\u53ef\u80fd\u306a\u5c5e\u6027\u578b\u306e\u30bb\u30c3\u30c8\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f DIT \u69cb\u9020\u898f\u5247\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %d \u306b\u306f\u958b\u304d\u62ec\u5f27\u304c\u5b58\u5728\u3059\u308b\u3079\u304d\u3067\u3059\u304c\u3001'%s' \u6587\u5b57\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092 DIT \u69cb\u9020\u898f\u5247\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4e0d\u660e\u306a\u540d\u524d\u66f8\u5f0f %s \u304c\u53c2\u7167\u3055\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092 DIT \u69cb\u9020\u898f\u5247\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4e0a\u4f4d\u306e DIT \u69cb\u9020\u898f\u5247\u3068\u3057\u3066\u4e0d\u660e\u306a\u898f\u5247 ID %d \u304c\u53c2\u7167\u3055\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092 DIT \u69cb\u9020\u898f\u5247\u306e\u8aac\u660e\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u898f\u5247\u306e\u540d\u524d\u66f8\u5f0f\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306f\u77ed\u3059\u304e\u308b\u305f\u3081\u3001\u6709\u52b9\u306a\u30c6\u30ec\u30c3\u30af\u30b9\u756a\u53f7\u5024\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u306b\u306f\u6709\u52b9\u306a\u30c6\u30ec\u30c3\u30af\u30b9\u756a\u53f7\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 %2$s \u306f\u6709\u52b9\u306a\u30d7\u30ea\u30f3\u30c8\u53ef\u80fd\u6587\u5b57\u5217\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u306b\u306f\u6709\u52b9\u306a\u30c6\u30ec\u30c3\u30af\u30b9\u756a\u53f7\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 %2$s \u306f\u3001\u6709\u52b9\u306a\u30d7\u30ea\u30f3\u30c8\u53ef\u80fd\u6587\u5b57\u5217\u3067\u3082\u3001\u30c6\u30ec\u30c3\u30af\u30b9\u756a\u53f7\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u3092\u533a\u5207\u308b\u305f\u3081\u306e\u30c9\u30eb\u8a18\u53f7\u3067\u3082\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u306b\u306f\u6709\u52b9\u306a\u30c6\u30ec\u30c3\u30af\u30b9\u756a\u53f7\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30eb\u8a18\u53f7\u3067\u533a\u5207\u3089\u308c\u305f 3 \u3064\u306e\u30d7\u30ea\u30f3\u30c8\u53ef\u80fd\u6587\u5b57\u5217\u3092\u8aad\u307f\u53d6\u308b\u524d\u306b\u3001\u5024\u306e\u672b\u5c3e\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=\u6307\u5b9a\u3055\u308c\u305f\u5024\u306f\u3001\u7a7a\u3067\u3042\u308b\u305f\u3081\u306b\u6709\u52b9\u306a FAX \u756a\u53f7\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u6709\u52b9\u306a FAX \u756a\u53f7\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 %2$s \u306f\u6709\u52b9\u306a\u30d7\u30ea\u30f3\u30c8\u53ef\u80fd\u6587\u5b57\u5217\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a FAX \u756a\u53f7\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u672b\u5c3e\u304c\u30c9\u30eb\u8a18\u53f7\u3067\u3057\u305f\u304c\u3001\u30c9\u30eb\u8a18\u53f7\u306e\u3042\u3068\u306b FAX \u30d1\u30e9\u30e1\u30fc\u30bf\u304c\u8a18\u8ff0\u3055\u308c\u3066\u3044\u308b\u3079\u304d\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u6709\u52b9\u306a FAX \u756a\u53f7\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %3$d \u3068 %4$d \u306e\u9593\u306e\u6587\u5b57\u5217 "%2$s" \u306f\u6709\u52b9\u306a FAX \u30d1\u30e9\u30e1\u30fc\u30bf\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u540d\u524d\u304a\u3088\u3073\u30aa\u30d7\u30b7\u30e7\u30f3 UID \u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002DN \u90e8\u3092\u89e3\u6790\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f:  %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u6709\u52b9\u306a\u540d\u524d\u304a\u3088\u3073\u30aa\u30d7\u30b7\u30e7\u30f3 UID \u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002UID \u90e8\u306e\u4f4d\u7f6e %3$d \u306b\u4e0d\u6b63\u306a 2 \u9032\u6570 %2$s \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=\u6307\u5b9a\u3055\u308c\u305f\u5024\u306f\u3001\u7a7a\u3067\u3042\u308b\u305f\u3081\u306b\u6709\u52b9\u306a\u30c6\u30ec\u30c6\u30c3\u30af\u30b9\u7aef\u672b\u8b58\u5225\u5b50\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u6709\u52b9\u306a\u30c6\u30ec\u30c6\u30c3\u30af\u30b9\u7aef\u672b\u8b58\u5225\u5b50\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4f4d\u7f6e %3$d \u306e\u6587\u5b57 %2$s \u306f\u6709\u52b9\u306a\u30d7\u30ea\u30f3\u30c8\u53ef\u80fd\u6587\u5b57\u5217\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u30c6\u30ec\u30c6\u30c3\u30af\u30b9\u7aef\u672b\u8b58\u5225\u5b50\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u672b\u5c3e\u304c\u30c9\u30eb\u8a18\u53f7\u3067\u3057\u305f\u304c\u3001\u30c9\u30eb\u8a18\u53f7\u306e\u3042\u3068\u306b TTX \u30d1\u30e9\u30e1\u30fc\u30bf\u304c\u8a18\u8ff0\u3055\u308c\u3066\u3044\u308b\u3079\u304d\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u30c6\u30ec\u30c6\u30c3\u30af\u30b9\u7aef\u672b\u8b58\u5225\u5b50\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30d1\u30e9\u30e1\u30fc\u30bf\u6587\u5b57\u5217\u306b\u540d\u524d\u3068\u5024\u3092\u533a\u5207\u308b\u30b3\u30ed\u30f3\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u30c6\u30ec\u30c6\u30c3\u30af\u30b9\u7aef\u672b\u8b58\u5225\u5b50\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6587\u5b57\u5217 "%s" \u306f\u6709\u52b9\u306a TTX \u30d1\u30e9\u30e1\u30fc\u30bf\u540d\u3067\u306f\u3042\u308a\u307e\u305b\u3093
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=\u6307\u5b9a\u3055\u308c\u305f\u5024\u306f\u3001\u7a7a\u3067\u3042\u308b\u305f\u3081\u306b\u5225\u306e\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u5225\u306e\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c9\u30eb\u8a18\u53f7\u306e\u524d\u306b\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u578b\u304c\u8a18\u8ff0\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u5225\u306e\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u578b\u306e\u4f4d\u7f6e %3$d \u306b\u4e0d\u6b63\u306a\u6587\u5b57 %2$s \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u5225\u306e\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c9\u30eb\u8a18\u53f7\u306e\u3042\u3068\u306b\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u304c\u8a18\u8ff0\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u5225\u306e\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30e1\u30fc\u30eb\u30dc\u30c3\u30af\u30b9\u306e\u4f4d\u7f6e %3$d \u306b\u4e0d\u6b63\u306a\u6587\u5b57 %2$s \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%1$s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %2$s \u306e\u4f4d\u7f6e %4$d \u306b\u4e0d\u6b63\u306a\u6587\u5b57 %3$c \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u306b\u3001\u6700\u521d\u306e\u958b\u304d\u62ec\u5f27\u306b\u5bfe\u5fdc\u3059\u308b\u9589\u3058\u62ec\u5f27\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u306e\u5148\u982d\u306f\u7591\u554f\u7b26\u3067\u3057\u305f\u304c\u3001\u305d\u306e\u3042\u3068\u306b\u6587\u5b57\u5217 "true" \u307e\u305f\u306f "false" \u304c\u7d9a\u304d\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u306b\u3001\u5c5e\u6027\u578b\u3068\u4e00\u81f4\u578b\u3092\u533a\u5207\u308b\u30c9\u30eb\u8a18\u53f7\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u3067\u30c9\u30eb\u8a18\u53f7\u306e\u524d\u306b\u5c5e\u6027\u578b\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u3067\u30c9\u30eb\u8a18\u53f7\u306e\u3042\u3068\u306b\u4e00\u81f4\u578b\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u90e8 %s \u306b\u3001\u4f4d\u7f6e %d \u304b\u3089\u59cb\u307e\u308b\u7121\u52b9\u306a\u4e00\u81f4\u578b\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3057\u305f
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u62e1\u5f35\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6761\u4ef6\u3068\u6709\u52b9\u7bc4\u56f2\u3092\u533a\u5207\u308b\u30b7\u30e3\u30fc\u30d7\u8a18\u53f7 (#) \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u62e1\u5f35\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6700\u5f8c\u306e\u30b7\u30e3\u30fc\u30d7\u8a18\u53f7 (#) \u306e\u3042\u3068\u306b\u6709\u52b9\u7bc4\u56f2\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u62e1\u5f35\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6307\u5b9a\u3055\u308c\u305f\u6709\u52b9\u7bc4\u56f2 %s \u304c\u7121\u52b9\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u62e1\u5f35\u30ac\u30a4\u30c9\u5024\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30b7\u30e3\u30fc\u30d7\u8a18\u53f7 (#) \u306e\u9593\u306b\u6761\u4ef6\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3067\u3057\u305f
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=DIT \u30b3\u30f3\u30c6\u30f3\u30c4\u898f\u5247 "%1$s" \u306f\u6709\u52b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u95a2\u9023\u4ed8\u3051\u3089\u308c\u305f\u69cb\u9020\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u30af\u30e9\u30b9 %3$s \u3067\u5fc5\u8981\u3068\u3055\u308c\u308b\u5c5e\u6027\u578b %2$s \u306e\u4f7f\u7528\u304c\u7981\u6b62\u3055\u308c\u3066\u3044\u307e\u3059
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=DIT \u30b3\u30f3\u30c6\u30f3\u30c4\u898f\u5247 "%1$s" \u306f\u6709\u52b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u95a2\u9023\u4ed8\u3051\u3089\u308c\u305f\u88dc\u52a9\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u30af\u30e9\u30b9 %3$s \u3067\u5fc5\u8981\u3068\u3055\u308c\u308b\u5c5e\u6027\u578b %2$s \u306e\u4f7f\u7528\u304c\u7981\u6b62\u3055\u308c\u3066\u3044\u307e\u3059
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=\u6307\u5b9a\u3055\u308c\u305f\u5024 "%s" \u3092\u6709\u52b9\u306a\u8b58\u5225\u540d\u3068\u3057\u3066\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u5c5e\u6027\u5024\u306e\u5148\u982d\u6587\u5b57 (\u4f4d\u7f6e %d) \u306f\u30a8\u30b9\u30b1\u30fc\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u305f\u3081\u3067\u3059
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ko.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ko.properties
new file mode 100644
index 0000000..f83ca9e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_ko.properties
@@ -0,0 +1,86 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=\uae38\uc774\uac00 \uc815\ud655\ud788 \ub450 \ubb38\uc790\uac00 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uad6d\uac00 \ubb38\uc790\uc5f4\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=\uc694\uc18c\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uc804\ub2ec \ubc29\ubc95\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=\"%2$s\"\uc774(\uac00) \uc720\ud6a8\ud55c \ubc29\ubc95\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uc804\ub2ec \ubc29\ubc95\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=\uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 '%2$c'\uc774(\uac00) \uc18d\uc131 \uc774\ub984\uc5d0 \ud5c8\uc6a9\ub418\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=\ube48 \uc18d\uc131 \uc774\ub984\uc744 \uac00\uc9c4 RDN\uc774 \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=\uacf5\ubc31\uc774 \uc544\ub2cc \ub9c8\uc9c0\ub9c9 \ubb38\uc790\uac00 \uc18d\uc131 \uc774\ub984 '%2$s'\uc758 \uc77c\ubd80\uc774\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=\uc18d\uc131 \uc774\ub984 \"%2$s\" \ub4a4\uc5d0 \uacf5\ubc31\uc774 \uc544\ub2cc \ub2e4\uc74c \ubb38\uc790\uac00 \ub4f1\ud638 \uae30\ud638\uc5ec\uc57c \ud558\ub294\ub370 '%3$c'\uc774\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=\uc18d\uc131 \uac12\uc774 # \uae30\ud638\ub85c \uc2dc\uc791\ud558\uc9c0\ub9cc \ub4a4\uc5d0 \ub450 16\uc9c4\uc218 \uc22b\uc790\uc758 \uc591\uc758 \ubc30\uc218\uac00 \uc624\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=\uc18d\uc131 \uac12\uc774 # \uae30\ud638\ub85c \uc2dc\uc791\ud558\uc9c0\ub9cc \uc720\ud6a8\ud558\uc9c0 \uc54a\uc740 16\uc9c4\uc218 \uc22b\uc790\uc778 %2$c \ubb38\uc790\ub97c \ud3ec\ud568\ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\" \uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=RDN \uad6c\uc131\uc694\uc18c \uc911 \ud558\ub098\uc5d0\uc11c \uc18d\uc131 \uac12\uc744 \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \ub3d9\uc548 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4: "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=RDN \uad6c\uc131\uc694\uc18c \uc911 \ud558\ub098\uac00 \ud574\ub2f9\ud558\ub294 \ub2eb\ub294 \uc778\uc6a9 \ubd80\ud638\uac00 \uc5c6\ub294 \uc778\uc6a9 \uac12\uc744 \ud3ec\ud568\ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=RDN \uad6c\uc131\uc694\uc18c \uc911 \ud558\ub098\uac00 \uc774\uc2a4\ucf00\uc774\ud504 \ucc98\ub9ac\ub41c 16\uc9c4\uc218 \uc22b\uc790\uac00 \uc788\ub294 \uac12\uc744 \ud3ec\ud568\ud558\uc9c0\ub9cc \ub4a4\uc5d0 \ub450 \ubc88\uc9f8 16\uc9c4\uc218 \uc22b\uc790\uac00 \uc624\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uace0\uc720 \uc774\ub984\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc18d\uc131 \uc720\ud615 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=\ube44\uc5b4 \uc788\uac70\ub098 null\uc774\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12\uc740 \uc720\ud6a8\ud55c \uc804\ud654 \ubc88\ud638\uac00 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=\uc5c4\uaca9\ud55c \uc804\ud654 \ubc88\ud638 \ud655\uc778\uc774 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uace0 \uac12\uc774 ITU-T E.123 \uc0ac\uc591\uc5d0 \ub9de\uac8c \ub354\ud558\uae30 \uae30\ud638\ub85c \uc2dc\uc791\ud558\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uc804\ud654 \ubc88\ud638\uac00 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=\uc5c4\uaca9\ud55c \uc804\ud654 \ubc88\ud638 \ud655\uc778\uc774 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uace0 \uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 %2$s\uc774(\uac00) ITU-T E.123 \uc0ac\uc591\uc5d0 \ud5c8\uc6a9\ub418\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uc804\ud654 \ubc88\ud638\uac00 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=\uc22b\uc790\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \uc804\ud654 \ubc88\ud638\uac00 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=\ubb38\uc790\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12\uc740 \uc720\ud6a8\ud55c \uc22b\uc790 \ubb38\uc790\uc5f4\uc774 \uc544\ub2d9\ub2c8\ub2e4.  \uc22b\uc790 \ubb38\uc790\uc5f4 \uac12\uc740 \ud558\ub098 \uc774\uc0c1\uc758 \uc22b\uc790\ub098 \uacf5\ubc31\uc744 \ud3ec\ud568\ud574\uc57c \ud569\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc18d\uc131 \uad6c\ubb38 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=\uc81c\uacf5\ub41c \uac12 %s\uc740(\ub294) \ub108\ubb34 \uc9e7\uc544 \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \ub420 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=\uc138\uae30 \ub610\ub294 \uc5f0\ub3c4 \uc0ac\uc591\uc5d0 '%2$s' \ubb38\uc790\uac00 \ud5c8\uc6a9\ub418\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c \uc6d4 \uc0ac\uc591\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c \uc77c \uc0ac\uc591\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c \uc2dc\uac04 \uc0ac\uc591\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c \ubd84 \uc0ac\uc591\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=\uc704\uce58 %3$d\uc5d0 \uc798\ubabb\ub41c \ubb38\uc790 %2$s\uc774(\uac00) \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c \ucd08\u00b7\uc0ac\uc591\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=%2$s\uc774(\uac00) \uc720\ud6a8\ud55c GMT \uc624\ud504\uc14b\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 %1$s\uc740(\ub294) \uc720\ud6a8\ud55c UTC \uc2dc\uac04 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=\uc81c\uacf5\ub41c \uac12 %s\uc744(\ub97c) \uc720\ud6a8\ud55c UTC \uc2dc\uac04\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4: %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) DIT \ucee8\ud150\ud2b8 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$c' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc774\ub984 \ud615\uc2dd \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc77c\uce58 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=\uc5f0\uacb0\ub418\ub294 \uc18d\uc131 \uad6c\ubb38\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uc558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc77c\uce58 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc77c\uce58 \uaddc\uce59 \uc0ac\uc6a9 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=\uc5f0\uacb0\ub41c OID\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc18d\uc131 \uc720\ud615 \uc9d1\ud569\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uc558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc77c\uce58 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=\uc704\uce58 %2$d\uc5d0 \uc5ec\ub294 \uad04\ud638\uac00 \uc788\uc5b4\uc57c \ud558\ub294\ub370 '%3$s' \ubb38\uc790\uac00 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) DIT \uad6c\uc870 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=\uc54c \uc218 \uc5c6\ub294 \uc774\ub984 \ud615\uc2dd %2$s\uc744(\ub97c) \ucc38\uc870\ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) DIT \uad6c\uc870 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=\uc0c1\uc704 DIT \uad6c\uc870 \uaddc\uce59\uc5d0 \ub300\ud574 \uc54c \uc218 \uc5c6\ub294 \uaddc\uce59 \uc544\uc774\ub514 %2$d\uc744(\ub97c) \ucc38\uc870\ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) DIT \uad6c\uc870 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=\uaddc\uce59\uc758 \uc774\ub984 \ud615\uc2dd\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uc558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) DIT \uad6c\uc870 \uaddc\uce59 \uc124\uba85\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=\uc81c\uacf5\ub41c \uac12 "\%s\"\uc740(\ub294) \ub108\ubb34 \uc9e7\uc544 \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ubc88\ud638 \uac12\uc774 \ub420 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=\uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 %2$s\uc774(\uac00) \uc778\uc1c4\ud560 \uc218 \uc788\ub294 \uc720\ud6a8\ud55c \ubb38\uc790\uc5f4\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ubc88\ud638\ub97c \ud3ec\ud568\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=\uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 %2$s\uc774(\uac00) \uc778\uc1c4\ud560 \uc218 \uc788\ub294 \uc720\ud6a8\ud55c \ubb38\uc790\uc5f4\ub3c4 \uc544\ub2c8\uace0 \ud154\ub809\uc2a4 \ubc88\ud638 \uad6c\uc131\uc694\uc18c\ub97c \uad6c\ubd84\ud558\ub294 \ub2ec\ub7ec \uae30\ud638\ub3c4 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ubc88\ud638\ub97c \ud3ec\ud568\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=\ub2ec\ub7ec\ub85c \uad6c\ubd84\ub41c \uc138 \uac1c\uc758 \uc778\uc1c4\ud560 \uc218 \uc788\ub294 \ubb38\uc790\uc5f4\uc744 \uc77d\uae30 \uc804\uc5d0 \uac12\uc758 \ub05d\uc5d0 \ub3c4\ub2ec\ud588\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc740(\ub294) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ubc88\ud638\ub97c \ud3ec\ud568\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=\ube44\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12\uc744 \uc720\ud6a8\ud55c \ud329\uc2a4 \ubc88\ud638\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=\uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 %2$s\uc774(\uac00) \uc778\uc1c4\ud560 \uc218 \uc788\ub294 \uc720\ud6a8\ud55c \ubb38\uc790\uc5f4\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud329\uc2a4 \ubc88\ud638\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=\ub2ec\ub7ec \uae30\ud638\ub85c \ub05d\ub098\uc9c0\ub9cc \ub2ec\ub7ec \uae30\ud638 \ub4a4\uc5d0 \ud329\uc2a4 \ub9e4\uac1c \ubcc0\uc218\uac00 \uc640\uc57c \ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud329\uc2a4 \ubc88\ud638\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=\uc704\uce58 %3$d \ubc0f %4$d \uc0ac\uc774\uc5d0 \uc788\ub294 \ubb38\uc790\uc5f4 \"%2$s\"\uc774(\uac00) \uc720\ud6a8\ud55c \ud329\uc2a4 \ub9e4\uac1c \ubcc0\uc218\uac00 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud329\uc2a4 \ubc88\ud638\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=DN \ubd80\ubd84\uc744 \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \uc774\ub984 \ubc0f \uc120\ud0dd\uc801 UID \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4: %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=\uc704\uce58 %3$d\uc5d0\uc11c UID \ubd80\ubd84\uc5d0 \uc798\ubabb\ub41c \uc774\uc9c4 \uc22b\uc790 %2$s\uc774(\uac00) \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 "%s"\uc744(\ub97c) \uc720\ud6a8\ud55c \uc774\ub984 \ubc0f \uc120\ud0dd\uc801 UID \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=\ube44\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12\uc744 \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ud130\ubbf8\ub110 \uc2dd\ubcc4\uc790\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=\uc704\uce58 %3$d\uc5d0 \uc788\ub294 \ubb38\uc790 %2$s\uc774(\uac00) \uc778\uc1c4\ud560 \uc218 \uc788\ub294 \uc720\ud6a8\ud55c \ubb38\uc790\uc5f4\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ud130\ubbf8\ub110 \uc2dd\ubcc4\uc790\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=\ub2ec\ub7ec \uae30\ud638\ub85c \ub05d\ub098\uc9c0\ub9cc \ud574\ub2f9 \ub2ec\ub7ec \uae30\ud638 \ub4a4\uc5d0 TTX \ub9e4\uac1c \ubcc0\uc218\uac00 \uc640\uc57c \ud558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ud130\ubbf8\ub110 \uc2dd\ubcc4\uc790\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=\ub9e4\uac1c \ubcc0\uc218 \ubb38\uc790\uc5f4\uc5d0 \uc774\ub984\uacfc \uac12\uc744 \uad6c\ubd84\ud558\ub294 \ucf5c\ub860\uc774 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ud130\ubbf8\ub110 \uc2dd\ubcc4\uc790\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=\ubb38\uc790\uc5f4 \"%2$s\"\uc774(\uac00) \uc720\ud6a8\ud55c TTX \ub9e4\uac1c \ubcc0\uc218 \uc774\ub984\uc774 \uc544\ub2c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uc720\ud6a8\ud55c \ud154\ub809\uc2a4 \ud130\ubbf8\ub110 \uc2dd\ubcc4\uc790\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=\ube44\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12\uc744 \ub2e4\ub978 \uba54\uc77c\ud568 \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=\ub2ec\ub7ec \uae30\ud638 \uc55e\uc5d0 \uba54\uc77c\ud568 \uc720\ud615\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ub2e4\ub978 \uba54\uc77c\ud568 \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=\uc704\uce58 %3$d\uc5d0\uc11c \uba54\uc77c\ud568 \uc720\ud615\uc5d0 \uc798\ubabb\ub41c \ubb38\uc790 %2$s\uc774(\uac00) \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \ub2e4\ub978 \uba54\uc77c\ud568 \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=\ub2ec\ub7ec \uae30\ud638 \ub4a4\uc5d0 \uba54\uc77c\ud568\uc774 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ub2e4\ub978 \uba54\uc77c\ud568 \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=\uc704\uce58 %3$d\uc5d0\uc11c \uba54\uc77c\ud568\uc5d0 \uc798\ubabb\ub41c \ubb38\uc790 %2$s\uc774(\uac00) \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \ub2e4\ub978 \uba54\uc77c\ud568 \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=\uc704\uce58 %4$d\uc5d0\uc11c \uae30\uc900 \ubd80\ubd84 %2$s\uc5d0 \uc798\ubabb\ub41c \ubb38\uc790 %2$c\uc774(\uac00) \ud3ec\ud568\ub418\uc5b4 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=\uae30\uc900 \ubd80\ubd84 %2$s\uc5d0 \uccab \ubc88\uc9f8 \uc5ec\ub294 \uad04\ud638\uc5d0 \ud574\ub2f9\ud558\ub294 \ub2eb\ub294 \uad04\ud638\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=\uae30\uc900 \ubd80\ubd84 %2$s\uc774(\uac00) \ubb3c\uc74c\ud45c\ub85c \uc2dc\uc791\ub418\ub294\ub370 \ub4a4\uc5d0 \ubb38\uc790\uc5f4 "true" \ub610\ub294 "false"\uac00 \uc5c6\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=\uae30\uc900 \ubd80\ubd84 %2$s\uc774(\uac00) \uc18d\uc131 \uc720\ud615\uacfc \uc77c\uce58 \uc720\ud615\uc744 \uad6c\ubd84\ud558\ub294 \ub2ec\ub7ec \uae30\ud638\ub97c \ud3ec\ud568\ud558\uace0 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=\uae30\uc900 \ubd80\ubd84 %2$s\uc774(\uac00) \ub2ec\ub7ec \uae30\ud638 \uc55e\uc5d0 \uc18d\uc131 \uc720\ud615\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=\uae30\uc900 \ubd80\ubd84 %2$s\uc774(\uac00) \ub2ec\ub7ec \uae30\ud638 \ub4a4\uc5d0 \uc77c\uce58 \uc720\ud615\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=\uae30\uc900 \ubd80\ubd84 %2$s\uc5d0 \uc704\uce58 %3$d\uc5d0\uc11c \uc2dc\uc791\ub418\ub294 \uc798\ubabb\ub41c \uc77c\uce58 \uc720\ud615\uc774 \uc788\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%1$s\"\uc744(\ub97c) \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=\uae30\uc900\uacfc \ubc94\uc704\ub97c \uad6c\ubd84\ud558\ub294 # \ubb38\uc790\uac00 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ud5a5\uc0c1\ub41c \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=\ub9c8\uc9c0\ub9c9 # \ubb38\uc790 \ub4a4\uc5d0 \ubc94\uc704\uac00 \uc81c\uacf5\ub418\uc5b4 \uc788\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ud5a5\uc0c1\ub41c \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=\uc9c0\uc815\ub41c \ubc94\uc704 %2$s\uc774(\uac00) \uc798\ubabb\ub418\uc5c8\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ud5a5\uc0c1\ub41c \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=# \ubb38\uc790 \uc0ac\uc774\uc5d0 \uae30\uc900\uc744 \uc9c0\uc815\ud558\uc9c0 \uc54a\uc558\uae30 \ub54c\ubb38\uc5d0 \uc81c\uacf5\ub41c \uac12 \"%s\"\uc744(\ub97c) \ud5a5\uc0c1\ub41c \uac00\uc774\ub4dc \uac12\uc73c\ub85c \uad6c\ubb38 \ubd84\uc11d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=\uc5f0\uacb0\ub41c \uad6c\uc870\uc801 \uac1d\uccb4 \ud074\ub798\uc2a4 %3$s\uc5d0 \ud544\uc694\ud55c \uc18d\uc131 \uc720\ud615 %2$s \uc0ac\uc6a9\uc744 \uae08\uc9c0\ud558\uae30 \ub54c\ubb38\uc5d0 DIT \ucee8\ud150\ud2b8 \uaddc\uce59 \"%s\"\uc774(\uac00) \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=\uc5f0\uacb0\ub41c \ubcf4\uc870 \uac1d\uccb4 \ud074\ub798\uc2a4 %3$s\uc5d0 \ud544\uc694\ud55c \uc18d\uc131 \uc720\ud615 %2$s \uc0ac\uc6a9\uc744 \uae08\uc9c0\ud558\uae30 \ub54c\ubb38\uc5d0 DIT \ucee8\ud150\ud2b8 \uaddc\uce59 \"%s\"\uc774(\uac00) \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=The provided value "%s" could not be parsed as a valid distinguished name because an attribute value started with a character at position %d that needs to be escaped
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_CN.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_CN.properties
new file mode 100644
index 0000000..3b6273c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_CN.properties
@@ -0,0 +1,86 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u662f\u6709\u6548\u7684\u56fd\u5bb6/\u5730\u533a\u5b57\u7b26\u4e32\uff0c\u56e0\u4e3a\u957f\u5ea6\u5e76\u975e\u6070\u597d\u4e3a\u4e24\u4e2a\u5b57\u7b26
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u662f\u6709\u6548\u7684\u4f20\u9001\u65b9\u6cd5\u503c\uff0c\u56e0\u4e3a\u5b83\u4e0d\u5305\u542b\u4efb\u4f55\u5143\u7d20
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u662f\u6709\u6548\u7684\u4f20\u9001\u65b9\u6cd5\u503c\uff0c\u56e0\u4e3a "%s" \u4e0d\u662f\u6709\u6548\u7684\u65b9\u6cd5
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5c5e\u6027\u540d\u79f0\u4e2d\u4e0d\u5141\u8bb8\u4f7f\u7528\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 '%2$c'
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5b83\u5305\u542b\u7684 RDN \u5177\u6709\u7a7a\u5c5e\u6027\u540d\u79f0
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u6700\u540e\u4e00\u4e2a\u975e\u7a7a\u683c\u5b57\u7b26\u662f\u5c5e\u6027\u540d\u79f0 '%s' \u7684\u4e00\u90e8\u5206
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5c5e\u6027\u540d\u79f0 "%s" \u540e\u9762\u7684\u4e0b\u4e00\u4e2a\u975e\u7a7a\u683c\u5b57\u7b26\u5e94\u8be5\u4e3a\u7b49\u53f7\uff0c\u800c\u5b9e\u9645\u4e3a '%c'
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5c5e\u6027\u503c\u4ee5\u4e95\u53f7 (#) \u5f00\u5934\uff0c\u4f46\u6ca1\u6709\u540e\u8ddf\u4e24\u4e2a\u5341\u516d\u8fdb\u5236\u6570\u5b57\u7684\u6b63\u500d\u6570
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5c5e\u6027\u503c\u4ee5\u4e95\u53f7 (#) \u5f00\u5934\uff0c\u4f46\u5305\u542b\u7684\u5b57\u7b26 %c \u4e0d\u662f\u6709\u6548\u7684\u5341\u516d\u8fdb\u5236\u6570\u5b57
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u5728\u5c1d\u8bd5\u89e3\u6790\u67d0\u4e2a RDN \u7ec4\u4ef6\u4e2d\u7684\u5c5e\u6027\u503c\u65f6\u51fa\u73b0\u610f\u5916\u5931\u8d25: "%s"
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u67d0\u4e2a RDN \u7ec4\u4ef6\u5305\u542b\u4e00\u4e2a\u7528\u5f15\u53f7\u5f15\u8d77\u6765\u7684\u503c\uff0c\u4f46\u8be5\u503c\u6ca1\u6709\u76f8\u5e94\u7684\u53f3\u5f15\u53f7
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u67d0\u4e2a RDN \u7ec4\u4ef6\u5305\u542b\u4e00\u4e2a\u5e26\u6709\u8f6c\u4e49\u5341\u516d\u8fdb\u5236\u6570\u5b57\u7684\u503c\uff0c\u4f46\u6ca1\u6709\u540e\u8ddf\u7b2c\u4e8c\u4e2a\u5341\u516d\u8fdb\u5236\u6570\u5b57
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5c5e\u6027\u7c7b\u578b\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=\u63d0\u4f9b\u7684\u503c\u4e0d\u662f\u6709\u6548\u7684\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u5b83\u4e3a\u7a7a\u6216 Null
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u662f\u6709\u6548\u7684\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u542f\u7528\u4e86\u4e25\u683c\u7535\u8bdd\u53f7\u7801\u68c0\u67e5\uff0c\u8be5\u503c\u6ca1\u6709\u6309\u7167 ITU-T E.123 \u89c4\u8303\u4ee5\u52a0\u53f7\u5f00\u5934
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=\u63d0\u4f9b\u7684\u503c "%1$s" \u4e0d\u662f\u6709\u6548\u7684\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u542f\u7528\u4e86\u4e25\u683c\u7535\u8bdd\u53f7\u7801\u68c0\u67e5\uff0cITU-T E.123 \u89c4\u8303\u4e0d\u5141\u8bb8\u4f7f\u7528\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 %2$s
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u662f\u6709\u6548\u7684\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u5b83\u4e0d\u5305\u542b\u4efb\u4f55\u6570\u5b57
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=\u63d0\u4f9b\u7684\u503c\u4e0d\u662f\u6709\u6548\u7684\u6570\u5b57\u5b57\u7b26\u4e32\uff0c\u56e0\u4e3a\u5b83\u4e0d\u5305\u542b\u4efb\u4f55\u5b57\u7b26\u3002\u6570\u5b57\u5b57\u7b26\u4e32\u503c\u5fc5\u987b\u81f3\u5c11\u5305\u542b\u4e00\u4e2a\u6570\u5b57\u6216\u7a7a\u683c
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5c5e\u6027\u8bed\u6cd5\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=\u63d0\u4f9b\u7684\u503c %s \u592a\u77ed\u800c\u65e0\u6cd5\u4f5c\u4e3a\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a\u4e0d\u5141\u8bb8\u5728\u4e16\u7eaa\u6216\u5e74\u4efd\u89c4\u8303\u4e2d\u4f7f\u7528 %s \u5b57\u7b26
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684\u6708\u4efd\u89c4\u8303
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684\u65e5\u671f\u89c4\u8303
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684\u5c0f\u65f6\u89c4\u8303
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684\u5206\u949f\u89c4\u8303
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=\u63d0\u4f9b\u7684\u503c %1$s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a\u5b83\u5728\u4f4d\u7f6e %3$d \u5904\u5305\u542b\u65e0\u6548\u5b57\u7b26 %2$s
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684\u79d2\u949f\u89c4\u8303
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u65f6\u95f4\u503c\uff0c\u56e0\u4e3a %s \u4e0d\u662f\u6709\u6548\u7684 GMT \u504f\u79fb
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c %s \u89e3\u6790\u4e3a\u6709\u6548\u7684 UTC \u65f6\u95f4: %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a DIT \u5185\u5bb9\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u540d\u79f0\u683c\u5f0f\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%c' \u5b57\u7b26
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5339\u914d\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5339\u914d\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5b83\u672a\u6307\u5b9a\u5173\u8054\u7684\u5c5e\u6027\u8bed\u6cd5
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5339\u914d\u89c4\u5219\u7528\u6cd5\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5339\u914d\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5b83\u672a\u6307\u5b9a\u53ef\u7528\u4e8e\u5173\u8054 OID \u7684\u5c5e\u6027\u7c7b\u578b\u96c6
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a DIT \u7ed3\u6784\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5e94\u8be5\u5728\u4f4d\u7f6e %d \u5904\u5305\u542b\u5de6\u5706\u62ec\u53f7\uff0c\u4f46\u627e\u5230\u7684\u662f '%s' \u5b57\u7b26
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a DIT \u7ed3\u6784\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5b83\u5f15\u7528\u4e86\u672a\u77e5\u7684\u540d\u79f0\u683c\u5f0f %s
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a DIT \u7ed3\u6784\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5b83\u5f15\u7528\u4e86\u4e0a\u7ea7 DIT \u7ed3\u6784\u89c4\u5219\u7684\u672a\u77e5\u89c4\u5219 ID %d
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a DIT \u7ed3\u6784\u89c4\u5219\u63cf\u8ff0\uff0c\u56e0\u4e3a\u5b83\u672a\u6307\u5b9a\u89c4\u5219\u7684\u540d\u79f0\u683c\u5f0f
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=\u63d0\u4f9b\u7684\u503c "%s" \u592a\u77ed\u800c\u65e0\u6cd5\u4f5c\u4e3a\u6709\u6548\u7684\u7535\u62a5\u53f7\u7801\u503c
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=\u63d0\u4f9b\u7684\u503c "%1$s" \u4e0d\u5305\u542b\u6709\u6548\u7684\u7535\u62a5\u53f7\u7801\uff0c\u56e0\u4e3a\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u6253\u5370\u5b57\u7b26\u4e32\u5b57\u7b26
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=\u63d0\u4f9b\u7684\u503c "%1$s" \u4e0d\u5305\u542b\u6709\u6548\u7684\u7535\u62a5\u53f7\u7801\uff0c\u56e0\u4e3a\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 %2$s \u65e2\u4e0d\u662f\u6709\u6548\u7684\u53ef\u6253\u5370\u5b57\u7b26\u4e32\u5b57\u7b26\uff0c\u4e5f\u4e0d\u662f\u7528\u4e8e\u5206\u9694\u7535\u62a5\u53f7\u7801\u7ec4\u6210\u90e8\u5206\u7684\u7f8e\u5143\u7b26\u53f7
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=\u63d0\u4f9b\u7684\u503c "%s" \u4e0d\u5305\u542b\u6709\u6548\u7684\u7535\u62a5\u53f7\u7801\uff0c\u56e0\u4e3a\u5728\u8bfb\u53d6\u4e09\u4e2a\u4ee5\u7f8e\u5143\u7b26\u53f7\u5206\u9694\u7684\u53ef\u6253\u5370\u5b57\u7b26\u4e32\u4e4b\u524d\u5230\u8fbe\u503c\u672b\u5c3e
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c\u89e3\u6790\u4e3a\u6709\u6548\u7684\u4f20\u771f\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u5b83\u4e3a\u7a7a
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u4f20\u771f\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u6253\u5370\u5b57\u7b26\u4e32\u5b57\u7b26
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u4f20\u771f\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u5b83\u4ee5\u7f8e\u5143\u7b26\u53f7\u7ed3\u5c3e\uff0c\u4f46\u8be5\u7f8e\u5143\u7b26\u53f7\u5e94\u540e\u8ddf\u4f20\u771f\u53c2\u6570
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u4f20\u771f\u7535\u8bdd\u53f7\u7801\uff0c\u56e0\u4e3a\u4f4d\u7f6e %3$d \u548c %4$d \u4e4b\u95f4\u7684\u5b57\u7b26\u4e32 "%2$s" \u4e0d\u662f\u6709\u6548\u7684\u4f20\u771f\u53c2\u6570
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u540d\u79f0\u548c\u53ef\u9009 UID \u503c\uff0c\u56e0\u4e3a\u5728\u5c1d\u8bd5\u89e3\u6790 DN \u90e8\u5206\u65f6\u51fa\u73b0\u9519\u8bef: %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u540d\u79f0\u548c\u53ef\u9009 UID \u503c\uff0c\u56e0\u4e3a UID \u90e8\u5206\u5728\u4f4d\u7f6e %3$d \u5904\u5305\u542b\u975e\u6cd5\u4e8c\u8fdb\u5236\u6570\u5b57 %2$s
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c\u89e3\u6790\u4e3a\u6709\u6548\u7684\u667a\u80fd\u7528\u6237\u7535\u62a5\u7ec8\u7aef\u6807\u8bc6\u7b26\uff0c\u56e0\u4e3a\u5b83\u4e3a\u7a7a
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u667a\u80fd\u7528\u6237\u7535\u62a5\u7ec8\u7aef\u6807\u8bc6\u7b26\uff0c\u56e0\u4e3a\u4f4d\u7f6e %3$d \u5904\u7684\u5b57\u7b26 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u6253\u5370\u5b57\u7b26\u4e32\u5b57\u7b26
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u667a\u80fd\u7528\u6237\u7535\u62a5\u7ec8\u7aef\u6807\u8bc6\u7b26\uff0c\u56e0\u4e3a\u5b83\u4ee5\u7f8e\u5143\u7b26\u53f7\u7ed3\u5c3e\uff0c\u4f46\u8be5\u7f8e\u5143\u7b26\u53f7\u5e94\u540e\u8ddf TTX \u53c2\u6570
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u667a\u80fd\u7528\u6237\u7535\u62a5\u7ec8\u7aef\u6807\u8bc6\u7b26\uff0c\u56e0\u4e3a\u53c2\u6570\u5b57\u7b26\u4e32\u4e0d\u5305\u542b\u5c06\u540d\u79f0\u4e0e\u503c\u9694\u5f00\u7684\u5192\u53f7
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u667a\u80fd\u7528\u6237\u7535\u62a5\u7ec8\u7aef\u6807\u8bc6\u7b26\uff0c\u56e0\u4e3a\u5b57\u7b26\u4e32 "%s" \u4e0d\u662f\u6709\u6548\u7684 TTX \u53c2\u6570\u540d\u79f0
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c\u89e3\u6790\u4e3a\u5176\u4ed6\u90ae\u7bb1\u503c\uff0c\u56e0\u4e3a\u5b83\u4e3a\u7a7a
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5176\u4ed6\u90ae\u7bb1\u503c\uff0c\u56e0\u4e3a\u7f8e\u5143\u7b26\u53f7\u524d\u9762\u6ca1\u6709\u90ae\u7bb1\u7c7b\u578b
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u5176\u4ed6\u90ae\u7bb1\u503c\uff0c\u56e0\u4e3a\u90ae\u7bb1\u7c7b\u578b\u5728\u4f4d\u7f6e %3$d \u5904\u5305\u542b\u975e\u6cd5\u5b57\u7b26 %2$s
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u5176\u4ed6\u90ae\u7bb1\u503c\uff0c\u56e0\u4e3a\u7f8e\u5143\u7b26\u53f7\u540e\u9762\u6ca1\u6709\u90ae\u7bb1
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u5176\u4ed6\u90ae\u7bb1\u503c\uff0c\u56e0\u4e3a\u90ae\u7bb1\u5728\u4f4d\u7f6e %3$d \u5904\u5305\u542b\u975e\u6cd5\u5b57\u7b26 %2$s
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%1$s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %2$s \u5728\u4f4d\u7f6e %4$d \u5904\u5305\u542b\u975e\u6cd5\u5b57\u7b26 %3$c
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u4e0d\u5305\u542b\u4e0e\u521d\u59cb\u5de6\u5706\u62ec\u53f7\u5bf9\u5e94\u7684\u53f3\u5706\u62ec\u53f7
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u4ee5\u95ee\u53f7\u5f00\u5934\uff0c\u4f46\u6ca1\u6709\u540e\u8ddf\u5b57\u7b26\u4e32\u201c\u771f\u201d\u6216\u201c\u5047\u201d
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u4e0d\u5305\u542b\u5c06\u5c5e\u6027\u7c7b\u578b\u4e0e\u5339\u914d\u7c7b\u578b\u9694\u5f00\u7684\u7f8e\u5143\u7b26\u53f7
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u672a\u5728\u7f8e\u5143\u7b26\u53f7\u524d\u9762\u6307\u5b9a\u5c5e\u6027\u7c7b\u578b
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u672a\u5728\u7f8e\u5143\u7b26\u53f7\u540e\u9762\u6307\u5b9a\u5339\u914d\u7c7b\u578b
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6761\u4ef6\u90e8\u5206 %s \u5177\u6709\u65e0\u6548\u7684\u5339\u914d\u7c7b\u578b\uff08\u4ece\u4f4d\u7f6e %d \u5904\u5f00\u59cb\uff09
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u589e\u5f3a\u7684\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u5b83\u4e0d\u5305\u542b\u5c06\u6761\u4ef6\u4e0e\u8303\u56f4\u9694\u5f00\u7684\u4e95\u53f7 (#) \u5b57\u7b26
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u589e\u5f3a\u7684\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u672a\u5728\u6700\u7ec8\u4e95\u53f7 (#) \u5b57\u7b26\u540e\u9762\u63d0\u4f9b\u4efb\u4f55\u8303\u56f4
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u589e\u5f3a\u7684\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u6307\u5b9a\u7684\u8303\u56f4 %s \u65e0\u6548
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u589e\u5f3a\u7684\u6307\u5bfc\u503c\uff0c\u56e0\u4e3a\u5b83\u672a\u5728\u4e95\u53f7 (#) \u5b57\u7b26\u4e4b\u95f4\u6307\u5b9a\u4efb\u4f55\u6761\u4ef6
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=DIT \u5185\u5bb9\u89c4\u5219 "%1$s" \u65e0\u6548\uff0c\u56e0\u4e3a\u5b83\u7981\u6b62\u4f7f\u7528\u5173\u8054\u7ed3\u6784\u5316\u5bf9\u8c61\u7c7b %3$s \u6240\u9700\u7684\u5c5e\u6027\u7c7b\u578b %2$s
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=DIT \u5185\u5bb9\u89c4\u5219 "%1$s" \u65e0\u6548\uff0c\u56e0\u4e3a\u5b83\u7981\u6b62\u4f7f\u7528\u5173\u8054\u8f85\u52a9\u5bf9\u8c61\u7c7b %3$s \u6240\u9700\u7684\u5c5e\u6027\u7c7b\u578b %2$s
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=\u65e0\u6cd5\u5c06\u63d0\u4f9b\u7684\u503c "%s" \u89e3\u6790\u4e3a\u6709\u6548\u7684\u6807\u8bc6\u540d\uff0c\u56e0\u4e3a\u4e00\u4e2a\u5c5e\u6027\u503c\u4ee5\u5728\u9700\u8981\u907f\u5f00\u7684\u4f4d\u7f6e %d \u5904\u7684\u4e00\u4e2a\u5b57\u7b26\u5f00\u59cb
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_TW.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_TW.properties
new file mode 100644
index 0000000..0f214c1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core_zh_TW.properties
@@ -0,0 +1,86 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u570b\u5bb6/\u5730\u5340\u5b57\u4e32\uff0c\u56e0\u70ba\u5176\u9577\u5ea6\u4e0d\u662f\u6b63\u597d\u5169\u500b\u5b57\u5143
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u50b3\u9001\u65b9\u6cd5\u503c\uff0c\u56e0\u70ba\u5176\u4e2d\u4e0d\u5305\u542b\u4efb\u4f55\u5143\u7d20
+ERR_ATTR_SYNTAX_DELIVERY_METHOD_INVALID_ELEMENT_12=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u50b3\u9001\u65b9\u6cd5\u503c\uff0c\u56e0\u70ba\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u65b9\u6cd5
+ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR_28=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5c6c\u6027\u540d\u7a31\u4e2d\u4e0d\u5f97\u6709\u4f4d\u7f6e %3$d \u7684\u5b57\u5143\u300c%2$c\u300d
+ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME_33=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5176\u4e2d\u6240\u542b\u7684 RDN \u542b\u6709\u7a7a\u7684\u5c6c\u6027\u540d\u7a31
+ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME_35=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u6700\u5f8c\u4e00\u500b\u975e\u7a7a\u683c\u5b57\u5143\u662f\u5c6c\u6027\u540d\u7a31\u300c%s\u300d\u7684\u4e00\u90e8\u5206
+ERR_ATTR_SYNTAX_DN_NO_EQUAL_36=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5c6c\u6027\u540d\u7a31\u300c%s\u300d\u5f8c\u7684\u4e0b\u4e00\u500b\u975e\u7a7a\u683c\u5b57\u5143\u61c9\u70ba\u7b49\u865f\uff0c\u4f46\u5be6\u969b\u4e0a\u662f\u300c%c\u300d
+ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT_38=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5c6c\u6027\u503c\u662f\u4ee5\u4e95\u5b57\u865f (#) \u958b\u982d\uff0c\u4f46\u4e26\u672a\u7dca\u63a5\u8457\u5169\u500b\u5341\u516d\u9032\u5236\u6578\u5b57\u7684\u6b63\u500d\u6578
+ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT_39=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5c6c\u6027\u503c\u662f\u4ee5\u4e95\u5b57\u865f (#) \u958b\u982d\uff0c\u4f46\u5176\u4e2d\u542b\u6709\u4e26\u975e\u6709\u6548\u5341\u516d\u9032\u5236\u6578\u5b57\u7684\u5b57\u5143 %c
+ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE_40=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5728\u5617\u8a66\u5f9e\u5176\u4e2d\u4e00\u500b RDN \u5143\u4ef6\u5256\u6790\u5c6c\u6027\u503c\u6642\uff0c\u767c\u751f\u672a\u9810\u671f\u7684\u5931\u6557:\u300c%s\u300d
+ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE_41=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5176\u4e2d\u4e00\u500b RDN \u5143\u4ef6\u6240\u542b\u4e4b\u52a0\u4e0a\u5f15\u865f\u7684\u503c\uff0c\u6c92\u6709\u5c0d\u61c9\u7684\u53f3\u96d9\u5f15\u865f
+ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID_42=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u8fa8\u5225\u540d\u7a31\uff0c\u56e0\u70ba\u5176\u4e2d\u4e00\u500b RDN \u5143\u4ef6\u6240\u5305\u542b\u7684\u503c\uff0c\u542b\u6709\u672a\u7dca\u63a5\u8457\u7b2c\u4e8c\u500b\u5341\u516d\u9032\u5236\u6578\u5b57\u7684\u9000\u51fa\u5341\u516d\u9032\u5236\u6578\u5b57
+ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS_53=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5c6c\u6027\u985e\u578b\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_TELEPHONE_EMPTY_85=\u63d0\u4f9b\u7684\u503c\u4e0d\u662f\u6709\u6548\u7684\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u8a72\u503c\u662f\u7a7a\u7684\u6216\u7a7a\u503c
+ERR_ATTR_SYNTAX_TELEPHONE_NO_PLUS_86=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u5df2\u555f\u7528\u56b4\u683c\u7684\u96fb\u8a71\u865f\u78bc\u6aa2\u67e5\uff0c\u4e14\u8a72\u503c\u4e26\u975e\u4ee5\u52a0\u865f\u958b\u982d\uff0c\u800c\u4e0d\u7b26\u5408 ITU-T E.123 \u898f\u683c
+ERR_ATTR_SYNTAX_TELEPHONE_ILLEGAL_CHAR_87=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u4e0d\u662f\u6709\u6548\u7684\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u5df2\u555f\u7528\u56b4\u683c\u7684\u96fb\u8a71\u865f\u78bc\u6aa2\u67e5\uff0c\u800c ITU-T E.123 \u898f\u683c\u4e0d\u5141\u8a31\u4f4d\u7f6e %3$d \u7684\u5b57\u5143 %2$s
+ERR_ATTR_SYNTAX_TELEPHONE_NO_DIGITS_88=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u5176\u4e2d\u4e0d\u5305\u542b\u4efb\u4f55\u6578\u5b57
+ERR_ATTR_SYNTAX_NUMERIC_STRING_EMPTY_VALUE_91=\u63d0\u4f9b\u7684\u503c\u4e0d\u662f\u6709\u6548\u7684\u6578\u503c\u5b57\u4e32\uff0c\u56e0\u70ba\u5176\u4e2d\u4e0d\u5305\u542b\u4efb\u4f55\u5b57\u5143\u3002\u6578\u503c\u5b57\u4e32\u503c\u5fc5\u9808\u5305\u542b\u81f3\u5c11\u4e00\u500b\u6578\u5b57\u6216\u7a7a\u683c
+ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS_93=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5c6c\u6027\u8a9e\u6cd5\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_UTC_TIME_TOO_SHORT_109=\u63d0\u4f9b\u7684\u503c %s \u592a\u77ed\uff0c\u7121\u6cd5\u505a\u70ba\u6709\u6548\u7684 UTC \u6642\u9593\u503c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_YEAR_110=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba\u4e16\u7d00\u6216\u5e74\u4efd\u898f\u683c\u4e2d\u4e0d\u5141\u8a31 %s \u5b57\u5143
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MONTH_111=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684\u6708\u4efd\u898f\u683c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_DAY_112=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684\u65e5\u671f\u898f\u683c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_HOUR_113=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684\u5c0f\u6642\u898f\u683c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_MINUTE_114=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684\u5206\u9418\u898f\u683c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_CHAR_115=\u63d0\u4f9b\u7684\u503c %1$s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba\u8a72\u503c\u5728\u4f4d\u7f6e %3$d \u542b\u6709\u7121\u6548\u7684\u5b57\u5143 %2$s
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_SECOND_116=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684\u79d2\u9418\u898f\u683c
+ERR_ATTR_SYNTAX_UTC_TIME_INVALID_OFFSET_117=\u63d0\u4f9b\u7684\u503c %s \u4e0d\u662f\u6709\u6548\u7684 UTC \u6642\u9593\u503c\uff0c\u56e0\u70ba %s \u4e0d\u662f\u6709\u6548\u7684 GMT \u504f\u79fb\u91cf
+ERR_ATTR_SYNTAX_UTC_TIME_CANNOT_PARSE_118=\u63d0\u4f9b\u7684\u503c %s \u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 UTC \u6642\u9593: %s
+ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS_120=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba DIT \u5167\u5bb9\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS_136=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u540d\u7a31\u8868\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%c\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS_150=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u76f8\u7b26\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_MR_NO_SYNTAX_158=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u76f8\u7b26\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u8a72\u503c\u672a\u6307\u5b9a\u5176\u76f8\u95dc\u806f\u7684\u5c6c\u6027\u8a9e\u6cd5
+ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS_161=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u76f8\u7b26\u898f\u5247\u4f7f\u7528\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_MRUSE_NO_ATTR_170=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u76f8\u7b26\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u8a72\u503c\u672a\u6307\u5b9a\u53ef\u8207\u76f8\u95dc OID \u4e00\u8d77\u4f7f\u7528\u7684\u5c6c\u6027\u985e\u578b\u96c6
+ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS_173=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba DIT \u7d50\u69cb\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %d \u61c9\u6709\u5de6\u62ec\u5f27\uff0c\u4f46\u627e\u5230\u7684\u537b\u662f\u300c%s\u300d\u5b57\u5143
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_NAME_FORM_178=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba DIT \u7d50\u69cb\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u8a72\u503c\u53c3\u7167\u4e86\u4e0d\u660e\u7684\u540d\u7a31\u8868 %s
+ERR_ATTR_SYNTAX_DSR_UNKNOWN_RULE_ID_179=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba DIT \u7d50\u69cb\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u8a72\u503c\u53c3\u7167\u4e86\u4e0a\u5c64 DIT \u7d50\u69cb\u898f\u5247\u7684\u4e0d\u660e\u898f\u5247 ID %d
+ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM_180=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba DIT \u7d50\u69cb\u898f\u5247\u8aaa\u660e\uff0c\u56e0\u70ba\u8a72\u503c\u672a\u6307\u5b9a\u898f\u5247\u7684\u540d\u7a31\u8868
+ERR_ATTR_SYNTAX_TELEX_TOO_SHORT_185=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u592a\u77ed\uff0c\u7121\u6cd5\u505a\u70ba\u6709\u6548\u7684\u96fb\u5831\u865f\u78bc\u503c
+ERR_ATTR_SYNTAX_TELEX_NOT_PRINTABLE_186=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u4e0d\u542b\u6709\u6548\u7684\u96fb\u5831\u865f\u78bc\uff0c\u56e0\u70ba\u4f4d\u7f6e %3$d \u7684\u5b57\u5143 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u5217\u5370\u5b57\u4e32\u5b57\u5143
+ERR_ATTR_SYNTAX_TELEX_ILLEGAL_CHAR_187=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u4e0d\u542b\u6709\u6548\u7684\u96fb\u5831\u865f\u78bc\uff0c\u56e0\u70ba\u4f4d\u7f6e %3$d \u7684\u5b57\u5143 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u5217\u5370\u5b57\u4e32\u5b57\u5143\uff0c\u4e5f\u4e0d\u662f\u53ef\u5206\u9694\u96fb\u5831\u865f\u78bc\u5143\u4ef6\u7684\u8ca8\u5e63\u7b26\u865f
+ERR_ATTR_SYNTAX_TELEX_TRUNCATED_188=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u4e0d\u542b\u6709\u6548\u7684\u96fb\u5831\u865f\u78bc\uff0c\u56e0\u70ba\u5728\u53ef\u8b80\u53d6\u4e09\u500b\u4ee5\u8ca8\u5e63\u7b26\u865f\u5206\u9694\u7684\u53ef\u5217\u5370\u5b57\u4e32\u4e4b\u524d\uff0c\u767c\u73fe\u8a72\u503c\u7684\u7d50\u5c3e
+ERR_ATTR_SYNTAX_FAXNUMBER_EMPTY_189=\u63d0\u4f9b\u7684\u503c\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u50b3\u771f\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u8a72\u503c\u662f\u7a7a\u7684
+ERR_ATTR_SYNTAX_FAXNUMBER_NOT_PRINTABLE_190=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u50b3\u771f\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u4f4d\u7f6e %3$d \u7684\u5b57\u5143 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u5217\u5370\u5b57\u4e32\u5b57\u5143
+ERR_ATTR_SYNTAX_FAXNUMBER_END_WITH_DOLLAR_191=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u50b3\u771f\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u8a72\u503c\u7684\u7d50\u5c3e\u662f\u8ca8\u5e63\u7b26\u865f\uff0c\u4f46\u8a72\u8ca8\u5e63\u7b26\u865f\u539f\u61c9\u7dca\u63a5\u8457\u50b3\u771f\u53c3\u6578
+ERR_ATTR_SYNTAX_FAXNUMBER_ILLEGAL_PARAMETER_192=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u50b3\u771f\u96fb\u8a71\u865f\u78bc\uff0c\u56e0\u70ba\u5728\u4f4d\u7f6e %3$d \u8207 %4$d \u4e4b\u9593\u7684\u5b57\u4e32\u300c%2$s\u300d\u4e0d\u662f\u6709\u6548\u7684\u50b3\u771f\u53c3\u6578
+ERR_ATTR_SYNTAX_NAMEANDUID_INVALID_DN_193=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u540d\u7a31\u8207\u9078\u64c7\u6027 UID \u503c\uff0c\u56e0\u70ba\u5728\u5617\u8a66\u5256\u6790 DN \u90e8\u5206\u6642\u767c\u751f\u932f\u8aa4: %s
+ERR_ATTR_SYNTAX_NAMEANDUID_ILLEGAL_BINARY_DIGIT_194=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684\u540d\u7a31\u8207\u9078\u64c7\u6027 UID \u503c\uff0c\u56e0\u70ba OID \u90e8\u5206\u5728\u4f4d\u7f6e %3$d \u542b\u6709\u975e\u6cd5\u4e8c\u9032\u4f4d\u6578\u5b57 %2$s
+ERR_ATTR_SYNTAX_TELETEXID_EMPTY_195=\u63d0\u4f9b\u7684\u503c\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 teletex \u7d42\u7aef\u6a5f\u8b58\u5225\u78bc\uff0c\u56e0\u70ba\u8a72\u503c\u662f\u7a7a\u7684
+ERR_ATTR_SYNTAX_TELETEXID_NOT_PRINTABLE_196=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 teletex \u7d42\u7aef\u6a5f\u8b58\u5225\u78bc\uff0c\u56e0\u70ba\u4f4d\u7f6e %3$d \u7684\u5b57\u5143 %2$s \u4e0d\u662f\u6709\u6548\u7684\u53ef\u5217\u5370\u5b57\u4e32\u5b57\u5143
+ERR_ATTR_SYNTAX_TELETEXID_END_WITH_DOLLAR_197=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 teletex \u7d42\u7aef\u6a5f\u8b58\u5225\u78bc\uff0c\u56e0\u70ba\u8a72\u503c\u7684\u7d50\u5c3e\u662f\u8ca8\u5e63\u7b26\u865f\uff0c\u4f46\u8a72\u8ca8\u5e63\u7b26\u865f\u539f\u61c9\u7dca\u63a5\u8457 TTX \u53c3\u6578
+ERR_ATTR_SYNTAX_TELETEXID_PARAM_NO_COLON_198=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 teletex \u7d42\u7aef\u6a5f\u8b58\u5225\u78bc\uff0c\u56e0\u70ba\u53c3\u6578\u5b57\u4e32\u4e0d\u5305\u542b\u7528\u4ee5\u5340\u9694\u540d\u7a31\u8207\u503c\u7684\u5192\u865f
+ERR_ATTR_SYNTAX_TELETEXID_ILLEGAL_PARAMETER_199=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u6709\u6548\u7684 teletex \u7d42\u7aef\u6a5f\u8b58\u5225\u78bc\uff0c\u56e0\u70ba\u5b57\u4e32\u300c%s\u300d\u4e0d\u662f\u6709\u6548\u7684 TTX \u53c3\u6578\u540d\u7a31
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_EMPTY_VALUE_200=\u63d0\u4f9b\u7684\u503c\u7121\u6cd5\u5256\u6790\u70ba\u5176\u4ed6\u96fb\u5b50\u4fe1\u7bb1\u503c\uff0c\u56e0\u70ba\u8a72\u503c\u662f\u7a7a\u7684
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MBTYPE_201=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5176\u4ed6\u96fb\u5b50\u4fe1\u7bb1\u503c\uff0c\u56e0\u70ba\u5728\u8ca8\u5e63\u7b26\u865f\u4e4b\u524d\u6c92\u6709\u4efb\u4f55\u96fb\u5b50\u4fe1\u7bb1\u985e\u578b
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MBTYPE_CHAR_202=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5176\u4ed6\u96fb\u5b50\u4fe1\u7bb1\u503c\uff0c\u56e0\u70ba\u96fb\u5b50\u4fe1\u7bb1\u985e\u578b\u5728\u4f4d\u7f6e %3$d \u542b\u6709\u975e\u6cd5\u5b57\u5143 %2$s
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_NO_MAILBOX_203=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5176\u4ed6\u96fb\u5b50\u4fe1\u7bb1\u503c\uff0c\u56e0\u70ba\u5728\u8ca8\u5e63\u7b26\u865f\u4e4b\u5f8c\u6c92\u6709\u4efb\u4f55\u96fb\u5b50\u4fe1\u7bb1
+ERR_ATTR_SYNTAX_OTHER_MAILBOX_ILLEGAL_MB_CHAR_204=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5176\u4ed6\u96fb\u5b50\u4fe1\u7bb1\u503c\uff0c\u56e0\u70ba\u96fb\u5b50\u4fe1\u7bb1\u5728\u4f4d\u7f6e %3$d \u542b\u6709\u975e\u6cd5\u5b57\u5143 %2$s
+ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR_206=\u63d0\u4f9b\u7684\u503c\u300c%1$s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %2$s \u5728\u4f4d\u7f6e %4$d \u542b\u6709\u975e\u6cd5\u5b57\u5143 %3$c
+ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN_207=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u4e0d\u5305\u542b\u8207\u521d\u59cb\u5de6\u62ec\u5f27\u5c0d\u61c9\u7684\u53f3\u62ec\u5f27
+ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK_208=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u662f\u4ee5\u554f\u865f\u958b\u982d\uff0c\u4f46\u4e26\u672a\u7dca\u63a5\u8457\u5b57\u4e32\u300ctrue\u300d\u6216\u300cfalse\u300d
+ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR_209=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u4e0d\u5305\u542b\u7528\u4ee5\u5340\u9694\u5c6c\u6027\u985e\u578b\u8207\u76f8\u7b26\u985e\u578b\u7684\u8ca8\u5e63\u7b26\u865f
+ERR_ATTR_SYNTAX_GUIDE_NO_ATTR_210=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u672a\u5728\u8ca8\u5e63\u7b26\u865f\u4e4b\u524d\u6307\u5b9a\u5c6c\u6027\u985e\u578b
+ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE_211=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u672a\u5728\u8ca8\u5e63\u7b26\u865f\u4e4b\u5f8c\u6307\u5b9a\u76f8\u7b26\u985e\u578b
+ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE_212=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u689d\u4ef6\u90e8\u5206 %s \u5f9e\u4f4d\u7f6e %d \u8d77\u6709\u7121\u6548\u7684\u76f8\u7b26\u985e\u578b
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_FINAL_SHARP_218=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u589e\u5f37\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u8a72\u503c\u6c92\u6709\u7528\u4ee5\u5340\u9694\u689d\u4ef6\u8207\u7bc4\u570d\u7684\u4e95\u5b57\u865f (#) \u5b57\u5143
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_SCOPE_219=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u589e\u5f37\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u5728\u6700\u7d42\u7684\u4e95\u5b57\u865f (#) \u5b57\u5143\u4e4b\u5f8c\u672a\u63d0\u4f9b\u7bc4\u570d
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_INVALID_SCOPE_220=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u589e\u5f37\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u6307\u5b9a\u7684\u7bc4\u570d %s \u7121\u6548
+ERR_ATTR_SYNTAX_ENHANCEDGUIDE_NO_CRITERIA_221=\u63d0\u4f9b\u7684\u503c\u300c%s\u300d\u7121\u6cd5\u5256\u6790\u70ba\u589e\u5f37\u5f15\u5c0e\u503c\uff0c\u56e0\u70ba\u8a72\u503c\u5728\u4e95\u5b57\u865f (#) \u5b57\u5143\u4e4b\u9593\u672a\u6307\u5b9a\u4efb\u4f55\u689d\u4ef6
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=DIT \u5167\u5bb9\u898f\u5247\u300c%1$s\u300d\u7121\u6548\uff0c\u56e0\u70ba\u8a72\u898f\u5247\u7981\u6b62\u4f7f\u7528\u76f8\u95dc\u7d50\u69cb\u7269\u4ef6\u985e\u5225 %3$s \u6240\u9700\u7684\u5c6c\u6027\u985e\u578b %2$s
+ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=DIT \u5167\u5bb9\u898f\u5247\u300c%1$s\u300d\u7121\u6548\uff0c\u56e0\u70ba\u8a72\u898f\u5247\u7981\u6b62\u4f7f\u7528\u76f8\u95dc\u8f14\u52a9\u7269\u4ef6\u985e\u5225 %3$s \u6240\u9700\u7684\u5c6c\u6027\u985e\u578b %2$s
+ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=The provided value "%s" could not be parsed as a valid distinguished name because an attribute value started with a character at position %d that needs to be escaped
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template
new file mode 100644
index 0000000..09085e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template
@@ -0,0 +1,32 @@
+define suffix=dc=example,dc=com
+define maildomain=example.com
+
+branch: [suffix]
+
+branch: ou=People,[suffix]
+subordinateTemplate: person
+
+template: person
+rdnAttr: uid
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: <first>
+sn: <last>
+cn: {givenName} {sn}
+initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}
+employeeNumber: <sequential:0>
+uid: user.{employeeNumber}
+mail: {uid}@[maildomain]
+userPassword: password
+telephoneNumber: <random:telephone>
+homePhone: <random:telephone>
+pager: <random:telephone>
+mobile: <random:telephone>
+street: <random:numeric:5> <file:streets> Street
+l: <file:cities>
+st: <file:states>
+postalCode: <random:numeric:5>
+postalAddress: {cn}${street}${l}, {st}  {postalCode}
+description: This is the description for {cn}.
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/cities b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/cities
new file mode 100644
index 0000000..78f740e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/cities
@@ -0,0 +1,232 @@
+Abilene
+Albany
+Albuquerque
+Alexandria
+Alpena
+Altoona
+Amarillo
+Anchorage
+Anniston
+Ardmore
+Atlanta
+Augusta
+Austin
+Bakersfield
+Baltimore
+Bangor
+Baton Rouge
+Beaumont
+Bend
+Billings
+Biloxi
+Binghamton
+Birmingham
+Bismarck
+Bloomington
+Bluefield
+Boise
+Boston
+Bowling Green
+Bryan
+Buffalo
+Burlington
+Butte
+Cadillac
+Casper
+Cedar Rapids
+Champaign
+Charleston
+Charlotte
+Charlottesville
+Chattanooga
+Cheyenne
+Chicago
+Chico
+Cincinnati
+Clarksburg
+Cleveland
+College Station
+Colorado Springs
+Columbia
+Columbus
+Corpus Christi
+Dallas
+Davenport
+Dayton
+Denver
+Des Moines
+Detroit
+Dothan
+Duluth
+Durham
+Eau Claire
+Elmira
+El Paso
+Erie
+Eugene
+Eureka
+Evansville
+Fairbanks
+Fargo
+Flint
+Florence
+Fort Myers
+Fort Smith
+Fort Wayne
+Fort Worth
+Fresno
+Gainesville
+Glendive
+Grand Junction
+Grand Rapids
+Great Falls
+Green Bay
+Greenville
+Hampton Roads
+Harlingen
+Harrisburg
+Harrisonburg
+Hartford
+Hattiesburg
+Helena
+Honolulu
+Houston
+Huntington
+Huntsville
+Idaho Falls
+Indianapolis
+Jackson
+Jacksonville
+Jefferson City
+Johnstown
+Jonesboro
+Joplin
+Kansas City
+Kirksville
+Klamath Falls
+Knoxville
+La Crosse
+Lafayette
+Lake Charles
+Lansing
+Laredo
+Las Vegas
+Lawton
+Lexington
+Lima
+Lincoln
+Little Rock
+Los Angeles
+Louisville
+Lubbock
+Lynchburg
+Macon
+Madison
+Mankato
+Marquette
+Mason City
+Medford
+Memphis
+Meridian
+Miami
+Milwaukee
+Minneapolis
+Missoula
+Mobile
+Moline
+Monroe
+Monterey Bay Area
+Montgomery
+Naples
+Nashville
+New Haven
+New Orleans
+New York
+North Platte
+Odessa
+Oklahoma City
+Omaha
+Orlando
+Ottumwa
+Paducah
+Palm Springs
+Panama City
+Parkersburg
+Pensacola
+Peoria
+Philadelphia
+Phoenix
+Pittsburgh
+Pocatello
+Port Arthur
+Portland
+Presque Isle
+Providence
+Pueblo
+Quincy
+Raleigh
+Rapid City
+Redding
+Reno
+Rhinelander
+Richmond
+Riverton
+Roanoke
+Rochester
+Rockford
+Sacramento
+Saginaw
+Saint Joseph
+Saint Louis
+Saint Paul
+Salem
+Salisbury
+Salt Lake City
+San Angelo
+San Antonio
+San Diego
+Santa Barbara
+Santa Fe
+Savannah
+Scranton
+Seattle
+Shreveport
+Sioux City
+Sioux Falls
+South Bend
+Spartanburg
+Spokane
+Springfield
+Steubenville
+Superior
+Syracuse
+Tallahassee
+Tampa Bay
+Terre Haute
+Toledo
+Topeka
+Traverse City
+Tucson
+Tulsa
+Tupelo
+Tuscaloosa
+Twin Falls
+Tyler
+Urbana
+Utica
+Victoria
+Waterloo
+Watertown
+Wausau
+Weston
+West Palm Beach
+Wheeling
+Wichita
+Wichita Falls
+Wichita FallsLawton
+Wilkes Barre
+Wilmington
+Winston
+Youngstown
+Yuma
+Zanesville
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/example.template b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/example.template
new file mode 100644
index 0000000..3f5b1e3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/example.template
@@ -0,0 +1,38 @@
+define suffix=dc=example,dc=com
+define maildomain=example.com
+define numusers=10000
+
+branch: [suffix]
+objectClass: top
+objectClass: domainComponent
+
+branch: ou=People,[suffix]
+objectClass: top
+objectClass: organizationalUnit
+subordinateTemplate: person:[numusers]
+
+template: person
+rdnAttr: uid
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: <first>
+sn: <last>
+cn: {givenName} {sn}
+initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}
+employeeNumber: <sequential:0>
+uid: user.{employeeNumber}
+mail: {uid}@[maildomain]
+userPassword: password
+telephoneNumber: <random:telephone>
+homePhone: <random:telephone>
+pager: <random:telephone>
+mobile: <random:telephone>
+street: <random:numeric:5> <file:streets> Street
+l: <file:cities>
+st: <file:states>
+postalCode: <random:numeric:5>
+postalAddress: {cn}${street}${l}, {st}  {postalCode}
+description: This is the description for {cn}.
+
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/first.names b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/first.names
new file mode 100644
index 0000000..0b2179b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/first.names
@@ -0,0 +1,8605 @@
+Aaccf
+Aaren
+Aarika
+Aaron
+Aartjan
+Abagael
+Abagail
+Abahri
+Abbas
+Abbe
+Abbey
+Abbi
+Abbie
+Abby
+Abbye
+Abdalla
+Abdallah
+Abdul
+Abdullah
+Abe
+Abel
+Abigael
+Abigail
+Abigale
+Abra
+Abraham
+Abu
+Access
+Accounting
+Achal
+Achamma
+Action
+Ada
+Adah
+Adaline
+Adam
+Adan
+Adara
+Adda
+Addi
+Addia
+Addie
+Addons
+Addy
+Adel
+Adela
+Adelaida
+Adelaide
+Adele
+Adelheid
+Adelia
+Adelice
+Adelina
+Adelind
+Adeline
+Adella
+Adelle
+Adena
+Adeniyi
+Adey
+Adi
+Adiana
+Adie
+Adina
+Aditya
+Admin
+Adnan
+Adora
+Adore
+Adoree
+Adorne
+Adrea
+Adri
+Adria
+Adriaens
+Adrian
+Adriana
+Adriane
+Adrianna
+Adrianne
+Adrie
+Adrien
+Adriena
+Adrienne
+Advance
+Aeriel
+Aeriela
+Aeriell
+Afif
+Afke
+Afton
+Afzal
+Ag
+Agace
+Agata
+Agatha
+Agathe
+Agenia
+Aggi
+Aggie
+Aggy
+Agna
+Agnella
+Agnes
+Agnese
+Agnesse
+Agneta
+Agnola
+Agretha
+Ahmad
+Ahmed
+Ahmet
+Aida
+Aidan
+Aideen
+Aiden
+Aigneis
+Aila
+Aile
+Ailee
+Aileen
+Ailene
+Ailey
+Aili
+Ailina
+Ailis
+Ailsun
+Ailyn
+Aime
+Aimee
+Aimil
+Aindrea
+Ainslee
+Ainsley
+Ainslie
+Air
+Ajay
+Ajit
+Ajmal
+Ajoy
+Akemi
+Akihiko
+Akin
+Akio
+Akira
+Akram
+Akshay
+Al
+Aladin
+Alain
+Alaine
+Alameda
+Alan
+Alana
+Alanah
+Alane
+Alanna
+Alasdair
+Alastair
+Alayne
+Alb
+Albert
+Alberta
+Albertina
+Albertine
+Albina
+Albrecht
+Aldo
+Alec
+Alecia
+Aleda
+Aleece
+Aleen
+Alejandra
+Alejandrina
+Alena
+Alene
+Alese
+Alessandra
+Aleta
+Alethea
+Alev
+Alex
+Alexa
+Alexander
+Alexandra
+Alexandrina
+Alexandru
+Alexi
+Alexia
+Alexina
+Alexine
+Alexis
+Alf
+Alfi
+Alfie
+Alfons
+Alfonso
+Alfonzo
+Alfred
+Alfreda
+Alfredo
+Alfy
+Ali
+Alia
+Alica
+Alice
+Alicea
+Alicia
+Alida
+Alidia
+Alie
+Alika
+Alikee
+Alina
+Aline
+Alis
+Alisa
+Alisha
+Alison
+Alissa
+Alisun
+Alix
+Aliza
+Alka
+Alkarim
+Alla
+Allan
+Alleen
+Allegra
+Allen
+Allene
+Alli
+Allianora
+Allie
+Allina
+Allis
+Allisan
+Allison
+Allissa
+Allister
+Allix
+Allsun
+Allx
+Ally
+Allyce
+Allyn
+Allys
+Allyson
+Alma
+Almeda
+Almeria
+Almerinda
+Almeta
+Almira
+Almire
+Alnoor
+Aloise
+Aloisia
+Alok
+Alora
+Aloysia
+Alp
+Alparslan
+Alphen
+Alphonso
+Alpine
+Alstine
+Alta
+Altay
+Althea
+Alvaro
+Alvera
+Alverta
+Alvin
+Alvina
+Alvinia
+Alvira
+Alwyn
+Aly
+Alyce
+Alyda
+Alys
+Alysa
+Alyse
+Alysia
+Alyson
+Alyss
+Alyssa
+Amabel
+Amabelle
+Amalea
+Amalee
+Amaleta
+Amalia
+Amalie
+Amalita
+Amalle
+Amand
+Amanda
+Amandi
+Amandie
+Amandip
+Amando
+Amandy
+Amant
+Amara
+Amargo
+Amarjit
+Amata
+Amato
+Amber
+Amberly
+Ambur
+Ame
+Amelia
+Amelie
+Amelina
+Ameline
+Amelita
+America
+Ami
+Amie
+Amii
+Amil
+Amina
+Amir
+Amit
+Amitie
+Amity
+Amjad
+Ammamaria
+Ammar
+Amnish
+Amnon
+Amos
+Amour
+Amparo
+Amrik
+Amrish
+Amy
+Amye
+An
+Ana
+Anabal
+Anabel
+Anabella
+Anabelle
+Anader
+Analiese
+Analise
+Anallese
+Anallise
+Anand
+Anantha
+Anastasia
+Anastasie
+Anastassia
+Anatola
+Anatoli
+Anatoly
+Anda
+Andaree
+Andee
+Andeee
+Anderea
+Anders
+Anderson
+Andi
+Andie
+Andra
+Andras
+Andre
+Andrea
+Andreana
+Andreas
+Andree
+Andrei
+Andrejs
+Andres
+Andrew
+Andria
+Andriana
+Andriette
+Andromache
+Andrzej
+Andy
+Anestassia
+Anet
+Anett
+Anetta
+Anette
+Ange
+Angel
+Angela
+Angele
+Angeles
+Angelia
+Angelica
+Angelie
+Angeliek
+Angelika
+Angelina
+Angeline
+Angelique
+Angelita
+Angelle
+Angelo
+Angie
+Angil
+Angus
+Angy
+Anhtuan
+Ania
+Anibal
+Anica
+Aniko
+Anil
+Anissa
+Anita
+Anitra
+Anja
+Anjanette
+Anje
+Anjela
+Anker
+Anki
+Ankie
+Anky
+Ann
+Ann-Hoon
+Ann-Lorrain
+Ann-Marie
+Anna
+Anna-Marie
+Anna-diana
+Anna-diane
+Anna-maria
+Annabal
+Annabel
+Annabela
+Annabell
+Annabella
+Annabelle
+Annadiana
+Annadiane
+Annalee
+Annaliese
+Annalise
+Annamaria
+Annamarie
+Annarbor
+Anne
+Anne Marie
+Anne-Lise
+Anne-Marie
+Anne-corinne
+Annecorinne
+Anneke
+Anneliese
+Annelise
+Annemarie
+Annemarijke
+Annemie
+Annet
+Annetta
+Annette
+Anni
+Annice
+Annick
+Annie
+Annis
+Annissa
+Annmaria
+Annmarie
+Annnora
+Annora
+Anny
+Ans
+Anselma
+Ansley
+Anstice
+Anthe
+Anthea
+Anthia
+Anthiathia
+Anthony
+Antoine
+Antoinette
+Anton
+Anton-Phuoc
+Antonella
+Antonetta
+Antoni
+Antonia
+Antonie
+Antonietta
+Antonina
+Antonio
+Anup
+Anurag
+Anver
+Anwar
+Anya
+Aparna
+Api-Ecm
+Apollo
+Appolonia
+April
+Aprilette
+Apryle
+Apurve
+Ara
+Arabel
+Arabela
+Arabele
+Arabella
+Arabelle
+Arch
+Archie
+Arda
+Ardath
+Ardavan
+Ardeen
+Ardelia
+Ardelis
+Ardella
+Ardelle
+Arden
+Ardene
+Ardenia
+Ardie
+Ardine
+Ardis
+Ardisj
+Ardith
+Ardra
+Ardyce
+Ardys
+Ardyth
+Aretha
+Ari
+Ariadne
+Ariana
+Aridatha
+Ariel
+Ariela
+Ariella
+Arielle
+Arif
+Arina
+Aris
+Aristides
+Arjun
+Arlan
+Arlana
+Arlee
+Arleen
+Arlen
+Arlena
+Arlene
+Arleta
+Arlette
+Arleyne
+Arlie
+Arliene
+Arlina
+Arlinda
+Arline
+Arluene
+Arly
+Arlyn
+Arlyne
+Armand
+Armando
+Armelle
+Armin
+Armine
+Arn
+Arne
+Arnett
+Arnie
+Arnis
+Arno
+Arnold
+Arsavir
+Arshad
+Art
+Arthur
+Arts
+Arturo
+Arun
+Aruna
+Arvin
+Arvind
+Aryn
+Arzu
+Asan
+Asghar
+Ash
+Ashely
+Ashia
+Ashien
+Ashil
+Ashla
+Ashlan
+Ashlee
+Ashleigh
+Ashlen
+Ashley
+Ashli
+Ashlie
+Ashly
+Ashok
+Ashoka
+Ashraf
+Ashu
+Asia
+Asif
+Asmar
+Asnat
+Astra
+Astrid
+Astrix
+Atalanta
+Athar
+Athena
+Athene
+Atique
+Atl
+Atl-Sales
+Atlanta
+Atlante
+Atmane
+Atsuo
+Atsushi
+Atta
+Attilio
+Attilla
+Atul
+Auberta
+Aubine
+Aubree
+Aubrette
+Aubrey
+Aubrie
+Aubry
+Audi
+Audie
+Audivox
+Audra
+Audre
+Audrey
+Audrie
+Audry
+Audrye
+Audy
+Augusta
+Auguste
+Augustin
+Augustina
+Augustine
+Augusto
+Aundrea
+Aura
+Aurea
+Aurel
+Aurelea
+Aurelia
+Aurelie
+Auria
+Aurie
+Aurilia
+Aurlie
+Auro
+Auroora
+Aurora
+Aurore
+Austin
+Austina
+Austine
+Auto
+Ava
+Avaz
+Avedis
+Aveline
+Averil
+Averyl
+Avie
+Avinash
+Avis
+Aviva
+Avivah
+Avril
+Avrit
+Avtar
+Axel
+Ayako
+Ayaz
+Aybars
+Ayda
+Ayn
+Azam
+Azar
+Azhar
+Aziz
+Azmeena
+Azmina
+Azra
+Bab
+Babak
+Babara
+Babb
+Babbette
+Babbie
+Babette
+Babita
+Babs
+Bachittar
+Badri
+Baets
+Baha
+Bahadir
+Bahram
+Bailey
+Baines
+Bakel
+Bakoury
+Bal
+Balaji
+Balakrishna
+Baldev
+Baljinder
+Bam
+Bambi
+Bambie
+Bamby
+Bang
+Bao
+BaoMinh
+Barb
+Barbabra
+Barbara
+Barbara-anne
+Barbaraanne
+Barbe
+Barbee
+Barbette
+Barbey
+Barbi
+Barbie
+Barbra
+Barby
+Bari
+Baris
+Barlas
+Barnes
+Barney
+Barrie
+Barry
+Barsha
+Bart
+Barton
+Baruk
+Base
+Basheer
+Basia
+Basil
+Bassam
+Bathsheba
+Batsheva
+Bawn
+Bcs
+Bcspatch
+Bea
+Beana
+Beata
+Beate
+Beatrice
+Beatrisa
+Beatrix
+Beatriz
+Beau
+Beaumont
+Beb
+Bebe
+Becca
+Becka
+Becki
+Beckie
+Becky
+Bedford
+Bee
+Begum
+Behdad
+Behnam
+Behrouz
+Behzad
+Beilul
+Beitris
+Bekki
+Bel
+Bela
+Belen
+Belia
+Belicia
+Belinda
+Belissa
+Belita
+Bell
+Bella
+Bellanca
+Belle
+Belleville
+Bellina
+Bello
+Belva
+Belvia
+Ben
+Bendite
+Benedetta
+Benedicta
+Benedikta
+Benefits
+Benetta
+Bengt
+Benita
+Benjamin
+Benne
+Bennesa
+Bennet
+Bennett
+Benni
+Bennie
+Benny
+Benoit
+Benoite
+Benthem
+Bep
+Beppie
+Berangere
+Berenice
+Beret
+Berger
+Berget
+Berna
+Bernadene
+Bernadette
+Bernadina
+Bernadine
+Bernard
+Bernardina
+Bernardine
+Bernardo
+Bernd
+Bernelle
+Berneta
+Bernete
+Bernetta
+Bernette
+Bernhard
+Berni
+Bernice
+Bernie
+Bernita
+Berny
+Berri
+Berrie
+Berry
+Bert
+Berta
+Berte
+Bertha
+Berthe
+Berti
+Bertie
+Bertina
+Bertine
+Berton
+Bertrand
+Berty
+Beryl
+Beryle
+Bess
+Bessie
+Bessy
+Beth
+Bethanne
+Bethany
+Bethena
+Bethina
+Betsey
+Betsy
+Betta
+Bette
+Bette-ann
+Betteann
+Betteanne
+Betti
+Bettie
+Bettina
+Bettine
+Bettink
+Betty
+Betty-Ann
+Betty-Anne
+Bettye
+Beulah
+Bev
+Beverie
+Beverlee
+Beverley
+Beverlie
+Beverly
+Bevvy
+Bevyn
+Bhagvat
+Bhal
+Bhanu
+Bharat
+Bhupendra
+Bhupinder
+Bianca
+Bianka
+Bibbie
+Bibby
+Bibbye
+Bibi
+Biddie
+Biddy
+Bidget
+Bihari
+Bijan
+Bili
+Bill
+Billi
+Billie
+Billy
+Billye
+Bin
+Bina
+Bing
+Binh
+Binni
+Binnie
+Binny
+Biplab
+Bird
+Birdie
+Birendra
+Birgit
+Birgitta
+Birgitte
+Birmingham
+Biswajit
+Bjorn
+Blaine
+Blair
+Blaire
+Blaise
+Blake
+Blakelee
+Blakeley
+Blanca
+Blanch
+Blancha
+Blanche
+Blinni
+Blinnie
+Blinny
+Bliss
+Blisse
+Blithe
+Blondell
+Blondelle
+Blondie
+Blondy
+Blythe
+Bnr
+Bnrecad
+Bnrtor
+Bo
+Bob
+Bobb
+Bobbe
+Bobbee
+Bobbette
+Bobbi
+Bobbie
+Bobby
+Bobbye
+Bobette
+Bobina
+Bobine
+Bobinette
+Bodo
+Boer
+Bogdan
+Bonita
+Bonnar
+Bonnee
+Bonni
+Bonnibelle
+Bonnie
+Bonny
+Bora
+Boris
+Bosiljka
+Bqb
+Brad
+Bradley
+Brahmananda
+Bram
+Bran
+Brana
+Brand
+Brandais
+Brande
+Brandea
+Brandi
+Brandice
+Brandie
+Brandise
+Brandon
+Brandy
+Brant
+Breanne
+Brear
+Brechtje
+Bree
+Breena
+Bregitte
+Brekel
+Bren
+Brena
+Brend
+Brenda
+Brendan
+Brenn
+Brenna
+Brennan
+Brent
+Brenton
+Bret
+Breton
+Brett
+Bria
+Brian
+Briana
+Brianna
+Brianne
+Bride
+Bridget
+Bridgette
+Bridie
+Brien
+Brier
+Brietta
+Brigid
+Brigida
+Brigit
+Brigitta
+Brigitte
+Brina
+Briney
+Brinn
+Brinna
+Briny
+Brit
+Brita
+Britney
+Britni
+Britt
+Britta
+Brittan
+Brittaney
+Brittani
+Brittany
+Britte
+Britteny
+Brittne
+Brittney
+Brittni
+Brock
+Brook
+Brooke
+Brooks
+Bruce
+Brunhilda
+Brunhilde
+Bruno
+Bryan
+Bryana
+Bryant
+Bryce
+Bryn
+Bryna
+Brynn
+Brynna
+Brynne
+Bryon
+Bse
+Buck
+Bucklin
+Bud
+Buda
+Buddy
+Budi
+Bue
+Buffy
+Buford
+Bui
+Building
+Bulent
+Bulletin
+Bunni
+Bunnie
+Bunny
+Burgess
+Burt
+Burton
+Business
+Buster
+Butch
+Bvworks
+Byron
+Cacilia
+Cacilie
+Cad
+Cahra
+Caine
+Cairistiona
+Caitlin
+Caitrin
+Cal
+Calida
+Calla
+Calley
+Calli
+Callida
+Callie
+Cally
+Calvin
+Calypso
+Cam
+Camala
+Camel
+Camella
+Camellia
+Cameron
+Camey
+Cami
+Camila
+Camile
+Camilla
+Camille
+Camino
+Cammi
+Cammie
+Cammy
+Canadian
+Candace
+Candee
+Candi
+Candice
+Candida
+Candide
+Candie
+Candis
+Candra
+Candy
+Cang
+Cantrell
+Canute
+Caprice
+Car
+Cara
+Caralie
+Career
+Careers
+Caren
+Carena
+Caresa
+Caressa
+Caresse
+Carey
+Cari
+Caria
+Caridad
+Carie
+Caril
+Carilyn
+Carin
+Carina
+Carine
+Cariotta
+Carissa
+Carita
+Caritta
+Cark
+Carl
+Carla
+Carlee
+Carleen
+Carlen
+Carlene
+Carley
+Carlie
+Carlin
+Carlina
+Carline
+Carling
+Carlis
+Carlisle
+Carlita
+Carlo
+Carlos
+Carlota
+Carlotta
+Carlton
+Carly
+Carlye
+Carlyn
+Carlynn
+Carlynne
+Carm
+Carma
+Carmel
+Carmela
+Carmelia
+Carmelina
+Carmelita
+Carmella
+Carmelle
+Carmelo
+Carmen
+Carmencita
+Carmina
+Carmine
+Carmita
+Carmody
+Carmon
+Caro
+Carol
+Carol-jean
+Carola
+Carolan
+Carolann
+Carole
+Carolee
+Carolien
+Carolin
+Carolina
+Caroline
+Caroljean
+Carolle
+Carolyn
+Carolyne
+Carolynn
+Caron
+Carran
+Carree
+Carri
+Carrie
+Carrissa
+Carroll
+Carry
+Carson
+Carsten
+Cart
+Carter
+Cary
+Caryl
+Caryn
+Casandra
+Casey
+Casi
+Casie
+Cass
+Cassandra
+Cassandre
+Cassandry
+Cassaundra
+Cassey
+Cassi
+Cassie
+Cassondra
+Cassy
+Cat
+Catarina
+Cate
+Caterina
+Catha
+Cathal
+Catharina
+Catharine
+Cathe
+Cathee
+Catherin
+Catherina
+Catherine
+Cathi
+Cathie
+Cathleen
+Cathlene
+Cathrin
+Cathrine
+Cathryn
+Cathy
+Cathyleen
+Cati
+Catie
+Catina
+Catja
+Catlaina
+Catlee
+Catlin
+Catrina
+Catriona
+Caty
+Cavin
+Caye
+Cayla
+Caz
+Cecco
+Cecelia
+Cecil
+Cecile
+Ceciley
+Cecilia
+Cecilla
+Cecily
+Cedric
+Cefee
+Ceil
+Cele
+Celene
+Celesta
+Celeste
+Celestia
+Celestina
+Celestine
+Celestyn
+Celestyna
+Celia
+Celie
+Celina
+Celinda
+Celine
+Celinka
+Celisse
+Celka
+Celle
+Celyne
+Cen
+Ceriel
+Cesar
+Cesare
+Cesya
+Cezary
+Chabane
+Chabert
+Chad
+Chahram
+Chai
+Chak-Hong
+Champathon
+Chan
+Chand
+Chanda
+Chandal
+Chander
+Chandra
+Chandrakant
+Chandran
+Chanh
+Channa
+Chantal
+Chantalle
+Charangit
+Charee
+Charene
+Charil
+Charin
+Charis
+Charissa
+Charisse
+Charita
+Charity
+Charla
+Charlean
+Charleen
+Charlena
+Charlene
+Charles
+Charleton
+Charley
+Charlie
+Charline
+Charlot
+Charlotta
+Charlotte
+Charlsey
+Charly
+Charmain
+Charmaine
+Charman
+Charmane
+Charmian
+Charmine
+Charmion
+Charo
+Charyl
+Chastity
+Chatri
+Chau
+Chawki
+Chee-Yin
+Chee-Yong
+Chellappan
+Chelsae
+Chelsea
+Chelsey
+Chelsie
+Chelsy
+Chen
+Chen-Chen
+Chen-Jung
+Cheng
+Cher
+Chere
+Cherey
+Cheri
+Cherianne
+Cherice
+Cherida
+Cherie
+Cherilyn
+Cherilynn
+Cherin
+Cherise
+Cherish
+Cherlyn
+Cherri
+Cherrita
+Cherry
+Chery
+Cherye
+Cheryl
+Cheslie
+Chesteen
+Chester
+Chet
+Cheuk
+Chi
+Chi-Keung
+Chi-Kwan
+Chi-Man
+Chi-Vien
+Chi-Yin
+Chi-ho
+Chiarra
+Chick
+Chickie
+Chicky
+Chie
+Chin
+ChinFui
+Ching-Long
+Chip
+Chiquia
+Chiquita
+Chitra
+Chiu
+Chlo
+Chloe
+Chloette
+Chloris
+Cho
+Cho-Kuen
+Cho-Lun
+Chocs
+Chok
+Chong
+Chong-Lai
+Choon-Lin
+Chris
+Chrissie
+Chrissy
+Christa
+Christabel
+Christabella
+Christal
+Christalle
+Christan
+Christean
+Christel
+Christelle
+Christen
+Christer
+Christi
+Christian
+Christiana
+Christiane
+Christianne
+Christie
+Christie-Anne
+Christin
+Christina
+Christine
+Christoph
+Christophe
+Christopher
+Christy
+Christye
+Christyna
+Chrysa
+Chrysler
+Chrystal
+Chryste
+Chrystel
+Chu-Chay
+Chuan
+Chuck
+Chun
+Chung
+Chung-Cheung
+Chung-Wo
+Chung-Yo
+Chungsik
+Chunmeng
+Chye-Lian
+Ciaran
+Cicely
+Cicily
+Ciel
+Cilka
+Cinda
+Cindee
+Cindelyn
+Cinderella
+Cindi
+Cindie
+Cindra
+Cindy
+Cinnamon
+Ciriaco
+Cissiee
+Cissy
+Clair
+Claire
+Clara
+Clarabelle
+Clare
+Clarence
+Claresta
+Clareta
+Claretta
+Clarette
+Clarey
+Clari
+Claribel
+Clarice
+Clarie
+Clarinda
+Clarine
+Clarissa
+Clarisse
+Clarita
+Clark
+Clarke
+Clary
+Class
+Claude
+Claudelle
+Claudetta
+Claudette
+Claudia
+Claudie
+Claudina
+Claudine
+Claus
+Clay
+Clayton
+Clea
+Clem
+Clemence
+Clement
+Clemente
+Clementia
+Clementina
+Clementine
+Clemie
+Clemmie
+Clemmy
+Cleo
+Cleopatra
+Clerissa
+Clestell
+Cleto
+Cleve
+Cleveland
+Clevon
+Cliff
+Clifford
+Clifton
+Clint
+Clinton
+Clio
+Clive
+Clo
+Cloe
+Cloris
+Clotilda
+Clovis
+Clyde
+Co
+Co-Op
+Cocos
+Code
+Codee
+Codi
+Codie
+Cody
+Coila
+Cole
+Coleen
+Coleman
+Colene
+Coletta
+Colette
+Colin
+Colleen
+Collen
+Collete
+Collette
+Colli
+Collie
+Colline
+Colly
+Colm
+Colman
+Con
+Concetta
+Concettina
+Conchita
+Concordia
+Condell
+Cong
+Conni
+Connie
+Conny
+Conrad
+Conserving
+Consolata
+Constance
+Constancia
+Constancy
+Constanta
+Constantia
+Constantin
+Constantina
+Constantine
+Consuela
+Consuelo
+Conway
+Cookie
+Cooney
+Coop
+Cooper
+Coord
+Coors
+Cora
+Corabel
+Corabella
+Corabelle
+Coral
+Coralie
+Coraline
+Coralyn
+Cordelia
+Cordelie
+Cordey
+Cordi
+Cordie
+Cordula
+Cordy
+Core
+Coreen
+Corella
+Corena
+Corenda
+Corene
+Coretta
+Corette
+Corey
+Cori
+Corie
+Corilla
+Corina
+Corine
+Corinna
+Corinne
+Coriss
+Corissa
+Corkstown
+Corliss
+Corly
+Cornel
+Cornela
+Cornelia
+Cornelis
+Cornelius
+Cornelle
+Cornie
+Corny
+Correna
+Correy
+Corri
+Corrianne
+Corrie
+Corrina
+Corrine
+Corrinne
+Corry
+Cortland
+Cortney
+Cory
+Cosetta
+Cosette
+Cosimo
+Cosola
+Costanza
+Costas
+Costas-Dinos
+Count
+Coursdev
+Coursey
+Court
+Courtenay
+Courtnay
+Courtney
+Craig
+Crawford
+Crin
+Cris
+Crissie
+Crissy
+Crista
+Cristabel
+Cristal
+Cristen
+Cristi
+Cristian
+Cristiane
+Cristie
+Cristin
+Cristina
+Cristine
+Cristionna
+Cristofaro
+Cristy
+Croix
+Crysta
+Crystal
+CrystalBay
+Crystie
+Cthrine
+Cubical
+Cubicle
+Cuong
+Curt
+Curtis
+Cuthbert
+Cyb
+Cybil
+Cybill
+Cycelia
+Cymbre
+Cynde
+Cyndi
+Cyndia
+Cyndie
+Cyndy
+Cynethia
+Cynthea
+Cynthia
+Cynthie
+Cynthy
+Cynthya
+Cyril
+Cyrine
+Cyrus
+Czes
+D'Anne
+Dacey
+Dacia
+Dacie
+Dacy
+Dae
+Dael
+Daffi
+Daffie
+Daffy
+Dagmar
+Dahlia
+Daile
+Daisey
+Daisi
+Daisie
+Daisy
+Dale
+Dalenna
+Dalia
+Dalila
+Dalip
+Dallas
+Daloris
+Dalton
+Damara
+Damaris
+Damian
+Damien
+Damil
+Damita
+Damon
+Dan
+Dana
+Danell
+Danella
+Danette
+Dani
+Dania
+Danial
+Danica
+Danice
+Daniel
+Daniela
+Daniele
+Daniella
+Danielle
+Danika
+Danila
+Danilo
+Danit
+Danita
+Danna
+Danni
+Dannie
+Danny
+Dannye
+Dante
+Dany
+Danya
+Danyelle
+Danyette
+Daphene
+Daphine
+Daphna
+Daphne
+Dara
+Darb
+Darbie
+Darby
+Darcee
+Darcey
+Darci
+Darcie
+Darcy
+Darda
+Dareen
+Darell
+Darelle
+Dari
+Daria
+Darice
+Darina
+Darko
+Darla
+Darleen
+Darlene
+Darline
+Darlleen
+Darnell
+Daron
+Darrel
+Darrell
+Darrelle
+Darren
+Darrin
+Darrol
+Darry
+Darryl
+Darsey
+Darsie
+Darwin
+Darya
+Daryl
+Daryn
+Dasha
+Dasi
+Dasie
+Dasya
+Dat
+Data
+Datas
+Datha
+Dau
+Daune
+Dave
+Daveen
+Daveta
+David
+Davida
+Davina
+Davinder
+Davine
+Davis
+Davita
+Dawn
+Dawna
+Daya
+Dayle
+Dayna
+Dayton
+Ddene
+De
+De-Anna
+DeAnne
+DeWayne
+Dean
+Deana
+Deane
+Deann
+Deanna
+Dear
+Deb
+Debadeep
+Debbi
+Debbie
+Debby
+Debee
+Debera
+Debi
+Debor
+Debora
+Deborah
+Debra
+Declan
+Dede
+Dedie
+Dedra
+Dee
+Dee dee
+DeeAnn
+Deeanne
+Deedee
+Deena
+Deepak
+Deerdre
+Deeyn
+Dehlia
+Deidre
+Deina
+Deirdre
+Del
+Dela
+Delancey
+Delbert
+Delcina
+Delcine
+Delfin
+Delia
+Delila
+Delilah
+Delinda
+Delisle
+Dell
+Della
+Delle
+Delly
+Delmar
+Delora
+Delores
+Deloria
+Deloris
+Delphine
+Delphinia
+Demet
+Demeter
+Demetra
+Demetre
+Demetri
+Demetria
+Demetris
+Demi
+Den
+Dena
+Deni
+Denice
+Deniece
+Denis
+Denise
+Denna
+Denni
+Dennie
+Dennis
+Denny
+Denver
+Deny
+Denys
+Denyse
+Denzil
+Deonne
+Dept
+Der
+Der-Chang
+Derek
+Deri
+Derick
+Derin
+Dermot
+Derrick
+Derrik
+Deryck
+Des
+Desdemona
+Design
+Desirae
+Desire
+Desiree
+Desiri
+Desmond
+Detlef
+Detlev
+Dev
+Deva
+Devan
+Devi
+Devin
+Devina
+Devinne
+Devon
+Devondra
+Devonna
+Devonne
+Devora
+Dewey
+Dewi
+Dexter
+Dhansukh
+Dhanvinder
+Dhawal
+Dhiraj
+Dhiren
+Di
+Dia-Edin
+Diahann
+Diamond
+Dian
+Diana
+Diandra
+Diane
+Diane-marie
+Dianemarie
+Diann
+Dianna
+Dianne
+Diannne
+Dick
+Dickens
+Dicky
+Didani
+Didar
+Didi
+Didier
+Dido
+Diego
+Dien
+Diena
+Dierdre
+Dieter
+Dieuwertje
+Digby
+Diju
+Dilip
+Dilpreet
+Dimitra
+Dimitri
+Dimitrios
+Dina
+Dinah
+Dineke
+Dinesh
+Dinh
+Dinker
+Dinnie
+Dinny
+Dino
+Dion
+Dione
+Dionis
+Dionne
+Dirk
+Dis
+Discover
+Dita
+Divina
+Divine
+Dix
+Dixie
+Djenana
+Djordje
+Dnadoc
+Dniren
+Dnsproj
+Do
+Doc
+Dode
+Dodi
+Dodie
+Dody
+Doe
+Doll
+Dolley
+Dolli
+Dollie
+Dolly
+Dolores
+Dolorita
+Doloritas
+Domenic
+Domenick
+Domenico
+Domeniga
+Dominga
+Domini
+Dominic
+Dominica
+Dominique
+Don
+Dona
+Donal
+Donald
+Donall
+Donella
+Donelle
+Donetta
+Donia
+Donica
+Donielle
+Donita
+Donn
+Donna
+Donnajean
+Donnamarie
+Donnette
+Donni
+Donnice
+Donnie
+Donny
+Donovan
+Door
+Doortje
+Dora
+Doralia
+Doralin
+Doralyn
+Doralynn
+Doralynne
+Dore
+Doreen
+Dorelia
+Dorella
+Dorelle
+Dorena
+Dorene
+Doretta
+Dorette
+Dorey
+Dori
+Doria
+Dorian
+Dorice
+Dorie
+Dorin
+Dorine
+Doris
+Dorisa
+Dorise
+Dorita
+Doro
+Dorolice
+Dorolisa
+Dorotea
+Doroteya
+Dorothea
+Dorothee
+Dorothy
+Dorree
+Dorreen
+Dorri
+Dorrie
+Dorris
+Dorry
+Dorthea
+Dorthy
+Dory
+Dosi
+Dot
+Doti
+Dotti
+Dottie
+Dotty
+Doug
+Douglas
+Douglass
+Dowell
+Doyle
+Dpn
+Dpnis
+Dpnlab
+Drago
+Dre
+Dreddy
+Dredi
+Drew
+Drieka
+Drona
+Dru
+Druci
+Drucie
+Drucill
+Drucy
+Drudy
+Drusi
+Drusie
+Drusilla
+Drusy
+Du-Tuan
+Duane
+Duc
+Duke
+Dulce
+Dulcea
+Dulci
+Dulcia
+Dulciana
+Dulcie
+Dulcine
+Dulcinea
+Dulcy
+Duljit
+Dulsea
+Duncan
+Dung
+Duong
+Dupuy
+Duquette
+Durali
+Durantaye
+Duryonna
+Dusan
+Dusty
+Dutch
+Duy
+Dvm
+Dvs
+Dwain
+Dwaine
+Dwayne
+Dwight
+Dyan
+Dyana
+Dyane
+Dyann
+Dyanna
+Dyanne
+Dyke
+Dyna
+Dynah
+Dzung
+Eachelle
+Eada
+Eadie
+Eadith
+Ealasaid
+Eamon
+Eamonn
+Earl
+Earle
+Earnest
+Eartha
+Easter
+Eastreg
+Eba
+Ebba
+Eben
+Ebonee
+Ebony
+Ebrahim
+Ecocafe
+Ed
+Eda
+Eddi
+Eddie
+Eddy
+Ede
+Edee
+Edel
+Edeline
+Eden
+Edgar
+Edi
+Edie
+Edin
+Edita
+Edith
+Editha
+Edithe
+Ediva
+Edlene
+Edmond
+Edmund
+Edmundo
+Edmx
+Edna
+Edouard
+Edric
+Eduardo
+Edward
+Edwin
+Edwina
+Edy
+Edyta
+Edyth
+Edythe
+Efdal
+Effie
+Ehab
+Ehi
+Eiji
+Eileen
+Eilis
+Eimile
+Eirena
+Eirik
+Ekaterina
+Eladio
+Elaina
+Elaine
+Elana
+Elane
+Elayne
+Elbert
+Elberta
+Elbertina
+Elbertine
+Elda
+Eldon
+Eleanor
+Eleanora
+Eleanore
+Electra
+Eleen
+Elena
+Elene
+Eleni
+Elenore
+Eleonora
+Eleonore
+Elex
+Elfie
+Elfreda
+Elfrida
+Elfrieda
+Elga
+Elhamy
+Elianora
+Elianore
+Elias
+Elicia
+Elie
+Eliezer
+Eline
+Elinor
+Elinore
+Elio
+Eliot
+Elisa
+Elisabet
+Elisabeth
+Elisabetta
+Elise
+Elisha
+Elissa
+Elita
+Eliza
+Elizabet
+Elizabeth
+Elizalde
+Elka
+Elke
+Ella
+Elladine
+Elle
+Elleke
+Ellen
+Ellene
+Ellette
+Elli
+Ellie
+Elliot
+Elliott
+Ellis
+Ellissa
+Ellwood
+Elly
+Ellyn
+Ellynn
+Elmar
+Elmer
+Elmira
+Elna
+Elnora
+Elnore
+Eloisa
+Eloise
+Elonore
+Elora
+Elpida
+Els
+Elsa
+Elsbeth
+Else
+Elset
+Elsey
+Elsi
+Elsie
+Elsinore
+Elspeth
+Elsy
+Elton
+Eluned
+Elva
+Elvera
+Elvert
+Elvina
+Elvira
+Elwira
+Elwood
+Elwyn
+Elyn
+Elyse
+Elysee
+Elysha
+Elysia
+Elyssa
+Elza
+Elzbieta
+Em
+Ema
+Emad
+Emalee
+Emalia
+Emanuel
+Emelda
+Emelia
+Emelina
+Emeline
+Emelita
+Emelyne
+Emer
+Emera
+Emerson
+Emery
+Emil
+Emilda
+Emile
+Emilee
+Emili
+Emilia
+Emilie
+Emiline
+Emilio
+Emily
+Emlyn
+Emlynn
+Emlynne
+Emma
+Emmalee
+Emmaline
+Emmalyn
+Emmalynn
+Emmalynne
+Emmanuel
+Emmeline
+Emmey
+Emmi
+Emmie
+Emmy
+Emmye
+Emogene
+Emory
+Emp
+Empdb
+Emr
+Emran
+Emyle
+Emylee
+Ende
+Eng
+Engbert
+Engin
+Engracia
+Enid
+Enis
+Enrica
+Enrichetta
+Enrico
+Enrika
+Enriqueta
+Enver
+Envoy
+Enzo
+Eoin
+Eolanda
+Eolande
+Ephraim
+Eran
+Erda
+Erdem
+Erena
+Erhard
+Eric
+Erica
+Erich
+Ericha
+Erick
+Ericka
+Erik
+Erika
+Erin
+Erina
+Erinn
+Erinna
+Erkan
+Erle
+Erlene
+Erma
+Ermengarde
+Ermentrude
+Ermina
+Erminia
+Erminie
+Ermo
+Erna
+Ernaline
+Ernest
+Ernesta
+Ernestine
+Ernesto
+Ernie
+Erning
+Ernst
+Errol
+Ertan
+Ertha
+Erv
+Ervin
+Erwin
+Eryn
+Erzsebet
+Es
+Esam
+Esko
+Esma
+Esmail
+Esmaria
+Esme
+Esmeralda
+Esmond
+Essa
+Essam
+Essie
+Essy
+Esta
+Estel
+Estele
+Estell
+Estella
+Estelle
+Ester
+Esther
+Estrella
+Estrellita
+Etas
+Ethan
+Ethel
+Ethelda
+Ethelin
+Ethelind
+Etheline
+Ethelyn
+Ethyl
+Etienne
+Etta
+Etti
+Ettie
+Etty
+Eudora
+Eugene
+Eugenia
+Eugenie
+Eugine
+Eula
+Eulalie
+Eunice
+Euphemia
+Eustacia
+Eva
+Evaleen
+Evan
+Evangelia
+Evangelin
+Evangelina
+Evangeline
+Evangelo
+Evania
+Evanne
+Evans
+Eve
+Eveleen
+Evelien
+Evelina
+Eveline
+Evelyn
+Everett
+Everette
+Evert
+Evette
+Evey
+Evie
+Evita
+Evona
+Evonne
+Evvie
+Evvy
+Evy
+Ewen
+Ext
+Eyde
+Eydie
+Eyk
+Ezella
+Ezmeralda
+Fabien
+Fabienne
+Fadi
+Fady
+Fae
+Fahim
+Fai
+Faina
+Fairy
+Faith
+Faiz
+Faizal
+Fallon
+Famke
+Fan
+Fanchette
+Fanchon
+Fancie
+Fancy
+Fanechka
+Fania
+Fanni
+Fannie
+Fanny
+Fanya
+Far
+Fara
+Farag
+Farah
+Farand
+Fares
+Farhad
+Farhan
+Fariba
+Fariborz
+Farica
+Farid
+Farooq
+Farouk
+Farra
+Farrah
+Farrand
+Farrukh
+Farshid
+Faruk
+Farzad
+Farzin
+Fast
+Fastmer
+Fastowl
+Fatima
+Faun
+Faunie
+Faustina
+Faustine
+Fausto
+Fawn
+Fawne
+Fawnia
+Fay
+Faydra
+Faye
+Fayette
+Fayina
+Fayma
+Fayre
+Fayth
+Faythe
+Faz
+Fearless
+Federica
+Fedora
+Fei
+Fei-Yin
+Fekri
+Felecia
+Felicdad
+Felice
+Felicia
+Felicity
+Felicle
+Felipa
+Felipe
+Felisha
+Felita
+Felix
+Feliza
+Felton
+Femke
+Fenelia
+Feng
+Feodora
+Ferdinand
+Ferdinanda
+Ferdinande
+Fereidoon
+Feridoun
+Fern
+Fernand
+Fernanda
+Fernande
+Fernandina
+Fernando
+Ferne
+Fey
+Feynman
+Fiann
+Fianna
+Fidela
+Fidelia
+Fidelity
+Field
+Fifi
+Fifine
+Fikre
+Fil
+Filia
+Filibert
+Filide
+Filion
+Filippa
+Fima
+Fina
+Finance
+Fintan
+Fiona
+Fionan
+Fionna
+Fionnula
+Fiore
+Fiorenze
+Firat
+Fitness
+Fitz
+Fitzgerald
+Fitzroy
+Fleet
+Fletcher
+Fleur
+Fleurette
+Flo
+Flor
+Flora
+Florance
+Flore
+Florella
+Florence
+Florencia
+Florentia
+Florenza
+Florette
+Flori
+Floria
+Florida
+Florie
+Florina
+Florinda
+Florine
+Floris
+Florri
+Florrie
+Florry
+Flory
+Flossi
+Flossie
+Flossy
+Floyd
+Flss
+Flying
+Foad
+Focus
+Follick
+Fonnie
+Fons
+Forrest
+Foster
+Fotini
+Fouad
+Four
+Fox
+Fqa
+Fran
+Franc
+France
+Francene
+Frances
+Francesca
+Francine
+Francis
+Francisca
+Francisco
+Franciska
+Franco
+Francois
+Francoise
+Francyne
+Frank
+Franka
+Franki
+Frankie
+Franklin
+Franklyn
+Franky
+Franni
+Frannie
+Franny
+Frantisek
+Franz
+Franza
+Fraser
+Frayda
+Fred
+Freda
+Freddi
+Freddie
+Freddy
+Fredelia
+Frederic
+Frederica
+Frederick
+Fredericka
+Frederika
+Frederique
+Fredi
+Fredia
+Fredra
+Fredrika
+Freek
+Freeman
+Freida
+Freya
+Frieda
+Friederike
+Frinel
+Fritz
+Froukje
+Fscocos
+Fu-Shin
+Fulvia
+Fung
+Furrukh
+Fuzal
+Fwp
+Fwpas
+Fwpreg
+Gaal
+Gabbey
+Gabbi
+Gabbie
+Gabe
+Gabey
+Gabi
+Gabie
+Gabriel
+Gabriela
+Gabriell
+Gabriella
+Gabrielle
+Gabriellia
+Gabrila
+Gaby
+Gae
+Gael
+Gaetan
+Gaffney
+Gahn
+Gail
+Gailya
+Gajendra
+Gale
+Galen
+Galina
+Gama
+Ganesh
+Gant
+Garan
+Gareth
+Garland
+Garnet
+Garnette
+Garney
+Garo
+Garry
+Garth
+Gary
+Gaston
+Gates
+Gateway
+Gavin
+Gavra
+Gavrielle
+Gay
+Gaye
+Gayel
+Gayl
+Gayla
+Gayle
+Gayleen
+Gaylene
+Gaynor
+Gayronza
+Ge
+Gedas
+Gee
+Gee-Meng
+Geer
+Geetha
+Geety
+Geir
+Gelais
+Gelya
+Gen
+Gena
+Gene
+General
+Geneva
+Genevieve
+Genevra
+Genga
+Genia
+Genna
+Genni
+Gennie
+Gennifer
+Genny
+Genovera
+Genowefa
+Genvieve
+Geoff
+Geoffrey
+Georganne
+George
+GeorgeAnn
+Georgeanna
+Georgeanne
+Georgena
+Georges
+Georgeta
+Georgetta
+Georgette
+Georgia
+Georgiana
+Georgianna
+Georgianne
+Georgie
+Georgina
+Georgine
+Ger
+Gerald
+Geralda
+Geraldine
+Geralene
+Gerard
+Gerardjan
+Gerardo
+Gerben
+Gerber
+Gerda
+Gerhard
+Gerhardine
+Geri
+Gerianna
+Gerianne
+Gerladina
+Germ
+Germain
+Germaine
+Germana
+Gernot
+Gerrard
+Gerri
+Gerrie
+Gerrilee
+Gerrit
+Gerry
+Gert
+Gerta
+Gerti
+Gertie
+Gertrud
+Gertruda
+Gertrude
+Gertrudis
+Gerty
+Geza
+Ghassan
+Ghassem
+Gheorghe
+Ghislain
+Ghislaine
+Gia
+Giacinta
+Giambattista
+Giampaolo
+Giana
+Giang
+Gianina
+Gianna
+Gib
+Gigi
+Gihan
+Gil
+Gilbert
+Gilberta
+Gilberte
+Gilbertina
+Gilbertine
+Gilda
+Gilemette
+Giles
+Gill
+Gillan
+Gilles
+Gilli
+Gillian
+Gillie
+Gilligan
+Gilly
+Gin
+Gina
+Ginelle
+Ginette
+Ginevra
+Ginger
+Gini
+Ginn
+Ginni
+Ginnie
+Ginnifer
+Ginny
+Gino
+Gint
+Gio
+Giorgia
+Giovanna
+Giovanni
+Gipsy
+Giralda
+Giri
+Girish
+Gisela
+Gisele
+Gisella
+Giselle
+Gita
+Giuditta
+Giulia
+Giulietta
+Giuseppe
+Giustina
+Gizela
+Glad
+Gladi
+Gladys
+Glass
+Gleda
+Glen
+Glenda
+Glendon
+Glenine
+Glenn
+Glenna
+Glennie
+Glennis
+Glori
+Gloria
+Gloriana
+Gloriane
+Glornia
+Glory
+Glyn
+Glynda
+Glynis
+Glynn
+Glynnis
+Gnni
+Go
+Godfrey
+Godiva
+Goel
+Gokal
+Gokul
+Gokul-Chandra
+Golda
+Goldarina
+Goldi
+Goldia
+Goldie
+Goldina
+Goldwyn
+Goldy
+Gopal
+Goran
+Gord
+Gorde
+Gordie
+Gordon
+Gordy
+Goska
+Goutam
+Grace
+Gracia
+Gracie
+Graciela
+Gracinda
+Gracomda
+Grady
+Graeme
+Graham
+Grame
+Grant
+Grantley
+Grason
+Grata
+Gratia
+Gratiana
+Gray
+Grayce
+Grazia
+Greer
+Greet
+Greg
+Gregg
+Gregory
+Greta
+Gretal
+Gretchen
+Grete
+Gretel
+Grethel
+Gretna
+Gretta
+Grey
+Grier
+Griet
+Grietje
+Griselda
+Grissel
+Grover
+Grzegorz
+Guanyun
+Gudrun
+Guendolen
+Guenevere
+Guenna
+Guenther
+Guglielma
+Gui
+Guido
+Guilford
+Guillema
+Guillemette
+Guillermo
+Guinevere
+Guinna
+Gunars
+Guner
+Gunfer
+Gunilla
+Gunnar
+Gunter
+Guo-Qiang
+Gupta
+Gurcharan
+Gurdip
+Gurjinder
+Gurjit
+Gurmeet
+Gursharan
+Gurvinder
+Gus
+Gusella
+Gussi
+Gussie
+Gussy
+Gusta
+Gusti
+Gustie
+Gusty
+Guy
+Guylain
+Guylaine
+Gwen
+Gwenda
+Gwendolen
+Gwendolin
+Gwendolyn
+Gweneth
+Gwenette
+Gwenneth
+Gwenni
+Gwennie
+Gwenny
+Gwennyth
+Gwenora
+Gwenore
+Gwyn
+Gwyneth
+Gwynith
+Gwynne
+Gypsy
+Gyula
+Gzl
+Ha
+Habeeb
+Habib
+Hack-Hoo
+Hadi
+Hadria
+Hady
+Hafeezah
+Haggar
+Hai
+Haig
+Hailee
+Haily
+Hakan
+Hal
+Hala
+Haleigh
+Halette
+Haley
+Hali
+Halie
+Halimeda
+Halina
+Hall
+Halley
+Halli
+Hallie
+Hally
+Hamid
+Hamilton
+Hamzeh
+Han
+Han-Co
+Han-Van
+Hana
+Hanco
+Handoko
+Hang-Tong
+Hanh
+Hanhb
+Hanja
+Hank
+Hanna
+Hannah
+Hanneke
+Hanni
+Hannie
+Hannis
+Hanns
+Hanny
+Hans
+Happy
+Hardyal
+Hareton
+Hari
+Harinder
+Harish
+Harlene
+Harley
+Harli
+Harlie
+Harm
+Harmi
+Harmonia
+Harmonie
+Harmony
+Harold
+Haroon
+Harpal
+Harper
+Harpreet
+Harri
+Harrie
+Harriet
+Harriett
+Harrietta
+Harriette
+Harriot
+Harriott
+Harrison
+Harry
+Hartley
+Haruko
+Harvey
+Hasler
+Hassan
+Haste
+Hatti
+Hattie
+Hatty
+Hayden
+Hayley
+Hazel
+Hazem
+He
+Heath
+Heather
+Hector
+Heda
+Hedda
+Heddi
+Heddie
+Heddy
+Hedi
+Hedvig
+Hedvige
+Hedwig
+Hedwiga
+Hedy
+Heida
+Heidi
+Heidie
+Heike
+Heino
+Heinz
+Helaina
+Helaine
+Heleen
+Helen
+Helen-elizabeth
+Helena
+Helene
+Helenelizabeth
+Helenka
+Helga
+Helge
+Hellen
+Helli
+Hellmut
+Helma
+Helmut
+Helmuth
+Heloise
+Helsa
+Helyn
+Hemant
+Hendra
+Hendrik
+Hendrika
+Hengameh
+Henk
+Henka
+Hennie
+Hennrietta
+Henny
+Henri
+Henrie
+Henrieta
+Henrietta
+Henriette
+Henrika
+Henry
+Henryetta
+Hensley
+Hephzibah
+Heping
+Hera
+Herb
+Herbert
+Herbie
+Herman
+Hermann
+Hermia
+Hermien
+Hermina
+Hermine
+Herminia
+Hermione
+Hermon
+Hernan
+Hernandez
+Herre
+Herronald
+Herschel
+Herta
+Hertha
+Herve
+Hesham
+Hester
+Hesther
+Hestia
+Hetti
+Hettie
+Hetty
+Hewlet
+Hideki
+Hideo
+Hien
+Hilary
+Hilda
+Hildagard
+Hildagarde
+Hilde
+Hildegaard
+Hildegarde
+Hildy
+Hillary
+Hilliard
+Hilliary
+Hilmi
+Himanshu
+Hin-Wai
+Hinda
+Hing
+Hing-Fai
+Hiren
+Hiroki
+Hiroko
+Hirooki
+Hiroshi
+Hitoshi
+Ho
+Hoa
+Hoa-Van
+Hoang
+Hock
+Hodge
+Hoekstra
+Hoi-Kin
+Hojjat
+Holli
+Hollie
+Holly
+Holly-anne
+Hollyanne
+Holst
+Homa
+Homayoon
+Homer
+Hon-Kong
+Honey
+Hongzhi
+Honor
+Honoria
+Hoog
+Hooi-Lee
+Hope
+Hor-Lam
+Horacio
+Horatia
+Horatio
+Horst
+Hortense
+Hortensia
+Hossein
+Hot
+Hotline
+Houman
+Housseini
+How
+How-Kee
+Howard
+Howden
+Howie
+Hoy
+Hpone
+Hq
+Hqs
+Hr
+Hrdata
+Hrinfo
+Hsieh
+Hsin-shi
+Hsing-Ju
+Htd
+Huan
+Huan-yu
+Hubert
+Hudai
+Huelsman
+Hugh
+Hugo
+Huguette
+Hui
+Huib
+Hukam
+Hulda
+Hulst
+Humberto
+Humphrey
+Hung
+HungQuoc
+Hunter
+Huong
+Huppert
+HuuLiem
+Huub
+Huy
+Huyen
+Hwei-Ling
+Hyacinth
+Hyacintha
+Hyacinthe
+Hyacinthia
+Hyacinthie
+Hynda
+Hynek
+Hyung
+Iain
+Ian
+Ianthe
+Ibbie
+Ibby
+Ibrahim
+Ichiro
+Icy
+Icylyn
+Ida
+Idalia
+Idalina
+Idaline
+Idell
+Idelle
+Idette
+Idris
+Idt
+Idus
+Ifti
+Ignace
+Ignatius
+Igor
+Ihor
+Ijff
+Ike
+Ikram
+Ilan
+Ilda
+Ileana
+Ileane
+Ilene
+Ilise
+Ilka
+Illa
+Illinois
+Ilona
+Ilsa
+Ilse
+Ilya
+Ilysa
+Ilyse
+Ilyssa
+Imelda
+Imogen
+Imogene
+Imojean
+Imre
+Imtaz
+Imtiaz
+Ina
+Inam
+Inanc
+Ind
+Inderjit
+Indiana
+Indira
+Indy
+Ineke
+Ines
+Inesita
+Inessa
+Inez
+Inga
+Ingaberg
+Ingaborg
+Inge
+Ingeberg
+Ingeborg
+Ingemar
+Inger
+Ingres
+Ingrid
+Ingunna
+Inm
+Inna
+Inquire
+Ioan
+Ioana
+Iolande
+Iolanthe
+Iona
+Iormina
+Ira
+Iraj
+Irc
+Ireland
+Irena
+Irene
+Irice
+Irina
+Iris
+Irish
+Irita
+Irma
+Irv
+Irvin
+Irving
+Isa
+Isaac
+Isabeau
+Isabel
+Isabelita
+Isabell
+Isabella
+Isabelle
+Isadora
+Isahella
+Iseabal
+Ishan
+Isidora
+Isin
+Isis
+Isl
+Ismail
+Isobel
+Isoft
+Israel
+Issam
+Issi
+Issie
+Issy
+Italo
+Iteam
+Iteke
+Its-Eng
+Iva
+Ivan
+Ivett
+Ivette
+Ivie
+Ivo
+Ivona
+Ivonne
+Ivor
+Ivory
+Ivy
+Iwan
+Iwona
+Iws
+Iyun
+Izabel
+Izak
+Izumi
+Izuru
+Izzy
+J-Francois
+JR
+Jaan
+Jabir
+Jacalyn
+Jacek
+Jacenta
+Jacinda
+Jacinta
+Jacintha
+Jacinthe
+Jack
+Jackelyn
+Jacki
+Jackie
+Jacklin
+Jacklyn
+Jackquelin
+Jackqueline
+Jackson
+Jacky
+Jaclin
+Jaclyn
+Jacob
+Jacque
+Jacquelin
+Jacqueline
+Jacquelyn
+Jacquelynn
+Jacquenetta
+Jacquenette
+Jacques
+Jacquetta
+Jacquette
+Jacqui
+Jacquie
+Jacynth
+Jacynthe
+Jada
+Jade
+Jae
+Jaffer
+Jag
+Jagat
+Jagdev
+Jagdish
+Jagjeet
+Jagjit
+Jagriti
+Jai
+Jaime
+Jaimie
+Jaine
+Jak
+Jake
+Jamal
+Jaman
+James
+JamesMichael
+Jami
+Jamie
+Jamima
+Jamin
+Jamison
+Jammie
+Jan
+Jana
+Janaya
+Janaye
+Jandy
+Jane
+Janean
+Janeczka
+Janeen
+Janel
+Janela
+Janell
+Janella
+Janelle
+Janene
+Janenna
+Janessa
+Janet
+Janeta
+Janetta
+Janette
+Janeva
+Janey
+Jania
+Janice
+Janick
+Janie
+Janifer
+Janina
+Janine
+Janio
+Janis
+Janith
+Janka
+Jann
+Janna
+Jannel
+Jannelle
+Janos
+Janot
+Janson
+Janusz
+Jany
+Jap
+Japan
+Jaquelin
+Jaquelyn
+Jaquenetta
+Jaquenette
+Jaquith
+Jasbinder
+Jashvant
+Jasmin
+Jasmina
+Jasmine
+Jason
+Jaspreet
+Jastinder
+Jasver
+Jatinder
+Javad
+Javed
+Javier
+Jawad
+Jawaid
+Jay
+Jaya
+Jayant
+Jayendra
+Jayesh
+Jayme
+Jaymee
+Jayne
+Jaynell
+Jaynie
+Jazmin
+Jderek
+Jean
+Jean-Bernard
+Jean-Claude
+Jean-Denis
+Jean-Francois
+Jean-Guy
+Jean-Jacques
+Jean-Louis
+Jean-Luc
+Jean-Marc
+Jean-Marie
+Jean-Michel
+Jean-Normand
+Jean-Paul
+Jean-Pierre
+Jean-Robert
+Jean-Roch
+Jean-Yves
+Jeana
+Jeane
+Jeanelle
+Jeanette
+Jeanice
+Jeanie
+Jeanine
+Jeanna
+Jeanne
+Jeannette
+Jeannie
+Jeannine
+Jeannot
+Jed
+Jeff
+Jeffery
+Jeffrey
+Jehanna
+Jelene
+Jemie
+Jemima
+Jemimah
+Jemmie
+Jemmy
+Jen
+Jena
+Jenda
+Jenelle
+Jeni
+Jenica
+Jeniece
+Jenifer
+Jeniffer
+Jenilee
+Jenine
+Jenn
+Jenna
+Jennee
+Jennette
+Jenni
+Jennica
+Jennie
+Jennifer
+Jennilee
+Jennine
+Jenny
+Jenson
+Jerald
+Jeralee
+Jere
+Jeremy
+Jeri
+Jermaine
+Jeroen
+Jerome
+Jerrie
+Jerrilee
+Jerrilyn
+Jerrine
+Jerry
+Jerrylee
+Jerzy
+Jess
+Jessa
+Jessalin
+Jessalyn
+Jessamine
+Jessamyn
+Jesse
+Jesselyn
+Jessi
+Jessica
+Jessie
+Jessika
+Jessy
+Jester
+Jesus
+Jet
+Jewel
+Jewell
+Jewelle
+Jey
+Jian
+Jianli
+Jill
+Jillana
+Jillane
+Jillayne
+Jilleen
+Jillene
+Jilli
+Jillian
+Jillie
+Jilly
+Jim
+Jimmie
+Jimmy
+Jimson
+Jin
+Jin-Yun
+Jinann
+Jing
+Jinny
+Jiri
+Jirina
+Jo
+Jo ann
+Jo-Ann
+Jo-Marie
+Jo-anne
+JoAnne
+JoDee
+JoLee
+Joachim
+Joan
+Joana
+Joane
+Joanie
+Joann
+Joanna
+Joannah
+Joannes
+Joannie
+Joao
+Joaquin
+Jobey
+Jobi
+Jobie
+Jobina
+Joby
+Jobye
+Jobyna
+Jocelin
+Joceline
+Jocelyn
+Jocelyne
+Jochem
+Jock
+Jodi
+Jodie
+Jodine
+Jody
+Joe
+Joeann
+Joel
+Joela
+Joelie
+Joell
+Joella
+Joelle
+Joellen
+Joelly
+Joellyn
+Joelynn
+Joeri
+Joete
+Joey
+Johan
+Johann
+Johanna
+Johannah
+Johanne
+John
+John-Jr
+John-Paul
+John-Sr
+Johna
+Johnath
+Johnathan
+Johnette
+Johnna
+Johnnie
+Johnny
+Joice
+Joji
+Jojo
+Joke
+Jolanda
+Joleen
+Jolene
+Joletta
+Joli
+Jolie
+Joline
+Joly
+Jolyn
+Jolynn
+Jon
+Jonathan
+Jonell
+Jonelle
+Joni
+Jonie
+Jonis
+Jonthan
+Joo-Euin
+Joo-Geok
+Joon
+Jooran
+Jordain
+Jordan
+Jordana
+Jordanna
+Jorey
+Jorge
+Jori
+Jorie
+Jorrie
+Jorry
+Jos
+Josanne
+Joscelin
+Jose
+Josee
+Josef
+Josefa
+Josefina
+Joseph
+Josepha
+Josephina
+Josephine
+Josey
+Joshi
+Joshua
+Josi
+Josie
+Josine
+Josselyn
+Jossine
+Josy
+Jourdan
+Joy
+Joya
+Joyan
+Joyann
+Joyce
+Joycelin
+Joydeep
+Joye
+Joyous
+Jozef
+Jozsef
+Jsandye
+Juan
+Juana
+Juanita
+Jud
+Jude
+Judi
+Judie
+Judith
+Juditha
+Judy
+Judye
+Juergen
+Juieta
+Juile
+Julee
+Jules
+Juli
+Julia
+Julian
+Juliana
+Juliane
+Juliann
+Julianna
+Julianne
+Julie
+JulieAnne
+Julien
+Julienne
+Juliet
+Julieta
+Julietta
+Juliette
+Julina
+Juline
+Julio
+Julissa
+Julita
+Julius
+Jun
+June
+Junette
+Jung
+Junia
+Junie
+Junina
+Junk
+Juozas
+Jurek
+Jurg
+Jurgen
+Justin
+Justina
+Justine
+Justinn
+Justino
+Jutta
+Jyoti
+Kac
+Kacey
+Kacie
+Kacy
+Kaela
+Kah-Ming
+Kai
+Kai-Ming
+Kai-Wai
+Kaia
+Kaiching
+Kaila
+Kaile
+Kailey
+Kaitlin
+Kaitlyn
+Kaitlynn
+Kaja
+Kakalina
+Kaki
+Kala
+Kalai
+Kaleena
+Kali
+Kalie
+Kalila
+Kalina
+Kalinda
+Kalindi
+Kalle
+Kalli
+Kally
+Kalpit
+Kalvin
+Kalyan
+Kam
+Kam-Suen
+Kamal
+Kaman
+Kambhampati
+Kambiz
+Kameko
+Kamil
+Kamila
+Kamilah
+Kamillah
+Kaminsky
+Kamlesh
+Kamran
+Kamyar
+Kana
+Kanata
+Kandace
+Kandy
+Kang-Yuan
+Kania
+Kannan
+Kanu
+Kanya
+Kapsch
+Kara
+Kara-lynn
+Karalee
+Karalynn
+Karam
+Karan
+Kare
+Karee
+Karel
+Karen
+Karena
+Kari
+Karia
+Karie
+Karil
+Karilynn
+Karim
+Karin
+Karina
+Karine
+Kariotta
+Karisa
+Karissa
+Karita
+Karl
+Karla
+Karlee
+Karleen
+Karlen
+Karlene
+Karlie
+Karlon
+Karlotta
+Karlotte
+Karly
+Karlyn
+Karmen
+Karna
+Karol
+Karola
+Karole
+Karolien
+Karolina
+Karoline
+Karoly
+Karon
+Karrah
+Karrie
+Karry
+Kartik
+Kary
+Karyl
+Karylin
+Karyn
+Kas
+Kasey
+Kasifa
+Kasper
+Kass
+Kassandra
+Kassem
+Kassey
+Kassi
+Kassia
+Kassie
+Kast
+Kat
+Kata
+Katalin
+Katarina
+Kataryna
+Kate
+Katee
+Katerina
+Katerine
+Katey
+Kath
+Katha
+Katharina
+Katharine
+Katharyn
+Kathe
+Katherin
+Katherina
+Katherine
+Katheryn
+Kathi
+Kathie
+Kathleen
+Kathlin
+Kathrerine
+Kathrine
+Kathryn
+Kathryne
+Kathy
+Kathye
+Kati
+Katie
+Katina
+Katine
+Katinka
+Katja
+Katleen
+Katlin
+Katrina
+Katrine
+Katrinka
+Katsumi
+Katsunori
+Katti
+Kattie
+Katuscha
+Katusha
+Katy
+Katya
+Kaushik
+Kay
+Kaycee
+Kaye
+Kayla
+Kayle
+Kaylee
+Kayley
+Kaylil
+Kaylyn
+Kaz
+Kazem
+Kazuhiko
+Kazuhito
+Kazuko
+Kazuo
+Kazuyuki
+Kedah
+Kee
+Keeley
+Keelia
+Keely
+Keep
+Kees
+Keith
+Kelcey
+Kelci
+Kelcie
+Kelcy
+Kelila
+Kellen
+Kelley
+Kelli
+Kellia
+Kellie
+Kellina
+Kellsie
+Kelly
+Kellyann
+Kelsey
+Kelsi
+Kelsy
+Keltouma
+Kelvin
+Kelwin
+Kem
+Kemal
+Kemp
+Ken
+Kendall
+Kendra
+Kendre
+Kenji
+Kenna
+Kenneth
+Kennon
+Kenny
+Kent
+Kentaro
+Kenyon
+Keri
+Keriann
+Kerianne
+Kerri
+Kerri-Ann
+Kerrie
+Kerrill
+Kerrin
+Kerry
+Kerstin
+Kesley
+Keslie
+Kessel
+Kessia
+Kessiah
+Kessley
+Ketan
+Ketti
+Kettie
+Ketty
+Keven
+Kevin
+Kevina
+Kevyn
+Keys
+Khai
+Khalid
+Khalil
+Khamdy
+Khanh
+Khosro
+Khue
+Khurshid
+Ki
+Kiah
+Kial
+Kiam
+Kiele
+Kiem
+Kien
+Kien-Nghiep
+Kiennghiep
+Kieran
+Kieron
+Kiersten
+Kiet
+Kikelia
+Kiki
+Kiley
+Kim
+Kim-Minh
+Kim-Tram
+Kimberlee
+Kimberley
+Kimberli
+Kimberly
+Kimberlyn
+Kimbra
+Kimihiko
+Kimiko
+Kimio
+Kimmi
+Kimmie
+Kimmy
+Kin
+Kin-Wai
+Kin-Yee
+King-Haut
+Kingsley
+Kinman
+Kinna
+Kip
+Kipp
+Kippie
+Kippy
+Kira
+Kirbee
+Kirbie
+Kirby
+Kiri
+Kirit
+Kirk
+Kirsten
+Kirsteni
+Kirsti
+Kirstie
+Kirstin
+Kirstyn
+Kirtikumar
+Kishor
+Kishore
+Kissee
+Kissiah
+Kissie
+Kit
+Kitson
+Kitt
+Kitti
+Kittie
+Kitty
+Kiyoon
+Kizzee
+Kizzie
+Kjell
+Klaas
+Klara
+Klarika
+Klarrisa
+Klaus
+Klazien
+Klazina
+Klink
+Knut
+Ko
+Koen
+Koji
+Kok-khiang
+Koko
+Kollen
+Konrad
+Konstance
+Konstanze
+Koo
+Kora
+Koral
+Koralle
+Koray
+Kordula
+Kore
+Korella
+Koren
+Koressa
+Kori
+Korie
+Korney
+Korrie
+Korry
+Kostas
+Kouji
+Krier
+Krinda
+Kris
+Krishan
+Krishna
+Krishnamurthy
+Krissie
+Krissy
+Krista
+Kristal
+Kristan
+Kriste
+Kristel
+Kristen
+Kristi
+Kristie
+Kristien
+Kristin
+Kristina
+Kristine
+Kristopher
+Kristy
+Kristyn
+Krysta
+Krystal
+Krystalle
+Krystle
+Krystn
+Krystyna
+Krzysztof
+Ktusn
+Kuang-Tsan
+Kue
+Kui
+Kui-Soon
+Kuldip
+Kum-Meng
+Kumar
+Kung
+Kunie
+Kunitaka
+Kurt
+Kusum
+Kuswara
+Kwan
+Kwei-San
+Kwing
+Kwok
+Kwok-Lan
+Kwok-Wa
+Kwong
+Ky
+Kyla
+Kyle
+Kylen
+Kylie
+Kylila
+Kylynn
+Kym
+Kynthia
+Kyoko
+Kyong
+Kyrstin
+Lurette
+LLoyd
+La
+La verne
+Lab
+Labfive
+Lac
+Lacee
+Lacey
+Lachu
+Lacie
+Lacy
+Ladan
+Ladell
+Ladonna
+Laetitia
+Lai
+Laina
+Laine
+Lainey
+Lalit
+Lalitha
+Lamar
+Lan
+Lana
+Lanae
+Lance
+Lane
+Lanette
+Laney
+Lang
+Lani
+Lanie
+Lanita
+Lanna
+Lanni
+Lanny
+Lapkin
+Laquinta
+Lara
+Laraine
+Lari
+Larina
+Larine
+Larisa
+Larissa
+Lark
+Larkin
+Larry
+Lars
+Larue
+Lary
+Larysa
+Laryssa
+Las
+Laser
+Lashonda
+Laslo
+Latashia
+Laten
+Latia
+Latisha
+Latonya
+Latrena
+Latrina
+Laura
+Lauraine
+Laural
+Lauralee
+Laure
+Lauree
+Laureen
+Laurel
+Laurella
+Lauren
+Laurena
+Laurence
+Laurene
+Laurent
+Lauretta
+Laurette
+Lauri
+Laurianne
+Laurice
+Laurie
+Laurna
+Laury
+Lauryn
+Lavena
+Laverna
+Laverne
+Lavina
+Lavinia
+Lavinie
+Lavonda
+Lawrence
+Layananda
+Layla
+Layne
+Layney
+Laz
+Lazlo
+Le
+LeRoy
+Lea
+Leah
+Leandra
+Leann
+Leanna
+Leanne
+Leanor
+Leanora
+Leaton
+Lebbie
+Lecien
+Leda
+Leddy
+Lee
+Lee-Anne
+Leeann
+Leeanne
+Leecia
+Leela
+Leelah
+Leena
+Leendert
+Leesa
+Leese
+Leeuwen
+Legra
+Lei-See
+Leia
+Leif
+Leigh
+Leigha
+Leighann
+Leil
+Leila
+Leilah
+Leisa
+Leisha
+Leita
+Lela
+Lelah
+Leland
+Lelia
+Len
+Lena
+Lendon
+Lenee
+Lenette
+Leni
+Lenka
+Lenna
+Lennart
+Lenny
+Leno
+Lenora
+Lenore
+Leny
+Leo
+Leodora
+Leoine
+Leola
+Leoline
+Leon
+Leona
+Leonanie
+Leonard
+Leonardo
+Leonas
+Leone
+Leonelle
+Leonida
+Leonie
+Leonor
+Leonora
+Leonore
+Leontine
+Leontyne
+Leora
+Les
+Leshia
+Lesia
+Lesley
+Lesli
+Leslie
+Lesly
+Lester
+Lesya
+Leta
+Lethia
+Leticia
+Letisha
+Letitia
+Letizia
+Letta
+Letti
+Lettie
+Letty
+Leung
+Levent
+Levy
+Lew
+Lewis
+Lex
+Lexi
+Lexie
+Lexine
+Lexis
+Lexy
+Leyla
+Leyton
+Lezlee
+Lezlie
+Li
+Li-Ming
+Lia
+Liam
+Lian
+Lian-Hong
+Liana
+Liane
+Lianna
+Lianne
+Lib
+Libbey
+Libbi
+Libbie
+Libby
+Libor
+Licha
+Lida
+Lidia
+Lidio
+Liduine
+Liem
+Liesa
+Liesbeth
+Liese
+Lil
+Lila
+Lilah
+Lilas
+Lili
+Lilia
+Lilian
+Liliana
+Liliane
+Lilias
+Lilin
+Lilith
+Lilla
+Lilli
+Lillian
+Lillie
+Lillien
+Lillis
+Lilllie
+Lilly
+Lily
+Lilyan
+Lin
+Lina
+Lincoln
+Lind
+Linda
+Linda-Joy
+Lindi
+Lindie
+Lindsay
+Lindsey
+Lindsy
+Lindy
+Line
+Linea
+Linell
+Linet
+Lineth
+Linette
+Ling-Yue
+Ling-Zhong
+Lingyan
+Linh
+Linn
+Linnea
+Linnell
+Linnet
+Linnie
+Lino
+Linzie
+Linzy
+Lionel
+Liping
+Lira
+Lisa
+Lisabeth
+Lisbeth
+Lise
+Lisetta
+Lisette
+Lisha
+Lishe
+Lissa
+Lissi
+Lissie
+Lissy
+Lita
+Liuka
+Liv
+Liva
+Livia
+Liviu
+Livvie
+Livvy
+Livvyy
+Livy
+Liz
+Liza
+Lizabeth
+Lizbeth
+Lizette
+Lizz
+Lizzie
+Lizzy
+Ljiljana
+Ljilyana
+Loan
+Loay
+Loc
+Lodovico
+Loella
+Loes
+Loesje
+Logan
+Logntp
+Lois
+Loise
+Lola
+Loleta
+Lolita
+Lolly
+Lon
+Lona
+Lonee
+Long
+Longdist
+Loni
+Lonna
+Lonneke
+Lonni
+Lonnie
+Loon
+Lope
+Lora
+Lora-Lee
+Lorain
+Loraine
+Loralee
+Loralie
+Loralyn
+Lorcan
+Loree
+Loreen
+Lorelei
+Lorelle
+Loren
+Lorena
+Lorene
+Lorenza
+Lorenzo
+Loreta
+Loretta
+Lorettalorna
+Lorette
+Lori
+Loria
+Lorianna
+Lorianne
+Lorie
+Lorilee
+Lorilyn
+Lorinda
+Lorine
+Loris
+Lorita
+Lorletha
+Lorna
+Lorne
+Lorraine
+Lorrayne
+Lorrel
+Lorri
+Lorrie
+Lorrin
+Lorry
+Lory
+Los
+Lothar
+Lotta
+Lotte
+Lotti
+Lottie
+Lotty
+Lou
+LouAnn
+Louella
+Louie
+Louis
+Louis-Philippe
+Louis-Rene
+Louisa
+Louise
+Louisette
+Lourdes
+Loutitia
+Lovina
+Lowell
+Lowietje
+Lowry
+Lpo
+Lrc
+Lsi
+Lsiunix
+Lu
+Luan
+Luann
+Lubomir
+Lubomyr
+Luc
+Lucas
+Luce
+Luci
+Lucia
+Luciana
+Luciano
+Lucie
+Lucien
+Lucienne
+Lucila
+Lucilia
+Lucille
+Lucina
+Lucinda
+Lucine
+Lucita
+Lucky
+Lucretia
+Lucy
+Ludovico
+Ludovika
+Luella
+Luelle
+Luigi
+Luis
+Luisa
+Luise
+Lujanka
+Luke
+Lula
+Lulita
+Lulu
+Luong
+Luping
+Lura
+Lurleen
+Lurlene
+Lurline
+Lusa
+Luther
+Luuk
+Luz
+Ly-Khanh
+Lyda
+Lydda-June
+Lydia
+Lydie
+Lyle
+Lyman
+Lyn
+Lynda
+Lynde
+Lyndel
+Lyndell
+Lyndia
+Lyndon
+Lyndsay
+Lyndsey
+Lyndsie
+Lyndy
+Lyne
+Lynea
+Lynelle
+Lynett
+Lynette
+Lynn
+Lynna
+Lynne
+Lynnea
+Lynnell
+Lynnelle
+Lynnet
+Lynnett
+Lynnette
+Lynsey
+Lynwood
+Lyse
+Lyssa
+Lysy
+Maaike
+Maala
+Maarten
+Mab
+Mabel
+Mabelle
+Mable
+Mac
+Mace
+Maciej
+Mack
+Mada
+Madalena
+Madalene
+Madalyn
+Madan
+Maddalena
+Maddi
+Maddie
+Maddy
+Madel
+Madelaine
+Madeleine
+Madelena
+Madelene
+Madelin
+Madelina
+Madeline
+Madella
+Madelle
+Madelon
+Madelyn
+Madge
+Madlen
+Madlin
+Madonna
+Mady
+Mae
+Maegan
+Mag
+Magda
+Magdaia
+Magdalen
+Magdalena
+Magdalene
+Magdi
+Magdy
+Maged
+Maggee
+Maggi
+Maggie
+Maggy
+Magnolia
+Mahala
+Mahalia
+Mahboob
+Mahendra
+Mahesh
+Mahlon
+Mahmood
+Mahmoud
+Mahmut
+Mahshad
+Mai
+Maia
+Maible
+Maid
+Maidisn
+Maidlab
+Maidsir
+Maidxpm
+Maier
+Maiga
+Maighdiln
+Mail
+Mainoo
+Maint
+Mair
+Maire
+Maisey
+Maisie
+Maitilde
+Maitreya
+Majid
+Makam
+Makary
+Makiko
+Mal
+Mala
+Malanie
+Malaysia
+Malcolm
+Malena
+Malethia
+Malgosia
+Malia
+Malik
+Malina
+Malinda
+Malinde
+Malissa
+Malissia
+Mallik
+Mallissa
+Mallorie
+Mallory
+Malorie
+Malory
+Malva
+Malvina
+Malynda
+Mame
+Mami
+Mamie
+Mamoru
+Man
+Man-Fai
+Manami
+Manas
+Manda
+Mandana
+Mandi
+Mandie
+Mandy
+Manfred
+Manh
+Manhatten
+Mani
+Manijeh
+Manimozhi
+Manish
+Manjinder
+Manjit
+Manmohan
+Manny
+Manoj
+Manon
+Manou
+Manouch
+Mansukha
+Mansum
+Manuel
+Manuela
+Manya
+Mara
+Marabel
+Marc
+Marc-Andre
+Marc-Antoine
+Marce
+Marcel
+Marcela
+Marcelia
+Marcella
+Marcelle
+Marcellina
+Marcelline
+Marcelo
+March
+Marchelle
+Marci
+Marcia
+Marcie
+Marcile
+Marcille
+Marco
+Marcos
+Marcus
+Marcy
+Mardi
+Mareah
+Marek
+Marella
+Maren
+Marena
+Maressa
+Marg
+Marga
+Margalit
+Margalo
+Margaret
+Margareta
+Margarete
+Margaretha
+Margarethe
+Margaretta
+Margarette
+Margariet
+Margarita
+Margaux
+Marge
+Margeaux
+Margery
+Marget
+Margette
+Margi
+Margie
+Margit
+Margo
+Margot
+Margret
+Margriet
+Marguerita
+Marguerite
+Margy
+Mari
+Maria
+Mariaelena
+Mariam
+Marian
+Mariana
+Mariann
+Marianna
+Marianne
+Maribel
+Maribelle
+Maribeth
+Marice
+Maridel
+Marie
+Marie-Andree
+Marie-Josee
+Marie-Luce
+Marie-Nadine
+Marie-ann
+Marie-jeanne
+Marieann
+Mariejeanne
+Marieka
+Marieke
+Mariel
+Mariele
+Marielle
+Mariellen
+Mariesara
+Mariet
+Marietta
+Mariette
+Marigold
+Marijke
+Marijo
+Marika
+Marilee
+Marilin
+Marillin
+Marilyn
+Marilynn
+Marilynne
+Marin
+Marina
+Marinette
+Marinna
+Mario
+Marion
+Mariquilla
+Maris
+Marisa
+Marisca
+Mariska
+Marissa
+Marit
+Marita
+Maritsa
+Mariya
+Marj
+Marja
+Marjan
+Marje
+Marjet
+Marji
+Marjie
+Marjo
+Marjoke
+Marjolein
+Marjorie
+Marjory
+Marjy
+Mark
+Marketa
+Marko
+Markus
+Marla
+Marlaine
+Marlane
+Marleah
+Marlee
+Marleen
+Marlena
+Marlene
+Marley
+Marlie
+Marlies
+Marlin
+Marline
+Marlo
+Marloes
+Marlon
+Marlyn
+Marlyne
+Marna
+Marne
+Marney
+Marni
+Marnia
+Marnie
+Maroun
+Marquita
+Marriet
+Marrilee
+Marris
+Marrissa
+Marscha
+Marsh
+Marsha
+Marshal
+Marshall
+Marsie
+Marsiella
+Marta
+Martelle
+Martguerita
+Martha
+Marthe
+Marthena
+Marti
+Martica
+Martie
+Martijn
+Martin
+Martina
+Martine
+Martino
+Martita
+Marty
+Martynne
+Marv
+Marvell
+Marvette
+Marvin
+Marwan
+Mary
+Mary-Ann
+Mary-Ellen
+Mary-Jane
+Mary-Jo
+Mary-Michelle
+Mary-Pat
+MaryKay
+MaryLou
+MaryLynn
+Marya
+Maryam
+Maryann
+Maryanna
+Maryanne
+Marybelle
+Marybeth
+Maryellen
+Maryjane
+Maryjo
+Maryl
+Marylee
+Marylin
+Marylinda
+Marylynne
+Maryrose
+Marys
+Marysa
+Maryse
+Maryvonne
+Masa
+Masahiro
+Masamichi
+Masha
+Maskell
+Maso
+Mason
+Masood
+Massoud
+Mat
+Matelda
+Materkowski
+Mathew
+Mathilda
+Mathilde
+Matilda
+Matilde
+Mats
+Matt
+Matthew
+Matti
+Mattie
+Matty
+Maud
+Maude
+Maudie
+Maura
+Maure
+Maureen
+Maureene
+Maurene
+Maurice
+Mauricio
+Maurijn
+Maurine
+Maurise
+Maurita
+Maurizia
+Mauro
+Maury
+Mavis
+Mavra
+Max
+Maxey
+Maxi
+Maxie
+Maxine
+Maxy
+May
+Mayasandra
+Maybelle
+Maycel
+Maye
+Mayeul
+Maylynn
+Maynard
+Maynie
+Mayumi
+McGee
+Mccauley
+Me
+Mead
+Meade
+Meagan
+Meaghan
+Meara
+Mechelle
+Medria
+Meena
+Meer
+Meeting
+Meg
+Megan
+Megen
+Meggi
+Meggie
+Meggy
+Meghan
+Meghann
+Megumi
+Mehboob
+Mehdi
+Mehetabel
+Mehmet
+Mehmud
+Mehrzad
+Mei
+Mel
+Mela
+Melamie
+Melania
+Melanie
+Melantha
+Melany
+Melba
+Melbourne
+Melek
+Melesa
+Melessa
+Melford
+Melhem
+Melicent
+Melina
+Melinda
+Melinde
+Melinie
+Melisa
+Melisande
+Melisandra
+Melisenda
+Melisent
+Melissa
+Melisse
+Melita
+Melitta
+Mella
+Melli
+Mellicent
+Mellie
+Mellisa
+Mellisent
+Melloney
+Melly
+Melodee
+Melodie
+Melody
+Melonie
+Melony
+Melosa
+Melva
+Melvin
+Melynda
+Mendel
+Mentor
+Mer
+Merb
+Mercedes
+Mercer
+Merci
+Mercie
+Mercy
+Merdia
+Meredith
+Meredithe
+Meriann
+Meridel
+Meridian
+Meridith
+Meriel
+Merilee
+Meriline
+Merilyn
+Meris
+Merissa
+Merl
+Merla
+Merle
+Merlin
+Merlina
+Merline
+Merna
+Merola
+Merralee
+Merridie
+Merrie
+Merrielle
+Merrile
+Merrilee
+Merrili
+Merrill
+Merrily
+Merry
+Mersey
+Merunix
+Merv
+Mervin
+Mervyn
+Meryl
+Message
+Mesut
+Meta
+Meter
+Methi
+Metrics
+Metyn
+Mewa
+Mfgeng
+Mia
+Micaela
+Micah
+Michael
+Michael-Morgan
+Michaela
+Michaelina
+Michaeline
+Michaella
+Michal
+Micheal
+Michel
+Michele
+Michelina
+Micheline
+Michell
+Michelle
+Michie
+Michiel
+Michigan
+Michiko
+Mick
+Mickey
+Micki
+Mickie
+Micky
+Mico
+Micro
+Mid
+Midge
+Miep
+Mietek
+Migdalia
+Mignon
+Mignonne
+Miguel
+Miguela
+Miguelita
+Mihaela
+Mihai
+Mika
+Mikaela
+Mike
+Mikelis
+Mikhail
+Mikihito
+Miklos
+Mil
+Mila
+Milan
+Mildred
+Mildrid
+Milena
+Miles
+Milicent
+Milissent
+Milka
+Millard
+Milli
+Millicent
+Millie
+Millisent
+Millo
+Milly
+Milo
+Milou
+Milt
+Milton
+Milzie
+Mimi
+Min
+Mina
+Minda
+Mindy
+Minerva
+Minetta
+Minette
+Ming
+Ming-Chang
+Ming-Ming
+Minh-Phuc
+Minhwi
+Minna
+Minnaminnie
+Minne
+Minnesota
+Minni
+Minnie
+Minnnie
+Minny
+Minoru
+Minta
+Miof mela
+Miquela
+Mira
+Mirabel
+Mirabella
+Mirabelle
+Miran
+Miranda
+Mireielle
+Mireille
+Mirella
+Mirelle
+Miriam
+Mirilla
+Miriya
+Mirjam
+Mirna
+Miro
+Miroslav
+Misbah
+Misha
+Miss
+Missagh
+Missie
+Missy
+Mister
+Misti
+Misty
+Mitch
+Mitchell
+Mitesh
+Mitsuko
+Mitzi
+Miwa
+Miwako
+Miyuki
+Mkt
+Mo
+Modesta
+Modestia
+Modestine
+Modesty
+Moe
+Moel
+Mohamad
+Mohamed
+Mohammad
+Mohammed
+Mohan
+Mohd
+Moina
+Moira
+Moises
+Moll
+Mollee
+Molli
+Mollie
+Molly
+Mommy
+Mona
+Monah
+Monica
+Moniek
+Monika
+Monique
+Monling
+Monroe
+Monte
+Monteene
+Montreal
+Monty
+Moon
+Mora
+Moray
+Moreen
+Morena
+Morgan
+Morgana
+Morganica
+Morganne
+Morgen
+Moria
+Moris
+Morissa
+Morley
+Morna
+Morrie
+Morris
+Mort
+Moselle
+Moshe
+Mot
+Motaz
+Mougy
+Mouna
+Mounir
+Moveline
+Moyna
+Moyra
+Mozelle
+Mrugesh
+Muffin
+Mufi
+Mufinella
+Muhammad
+Muinck
+Muire
+Mukul
+Mukund
+Mun-Hang
+Munaz
+Muni
+Munir
+Murat
+Mureil
+Murial
+Muriel
+Murielle
+Murray
+Murry
+Mustafa
+Mustapha
+My
+Myla
+Myra
+Myrah
+Myranda
+Myriam
+Myrilla
+Myrle
+Myrlene
+Myrna
+Myron
+Myrta
+Myrthille
+Myrtia
+Myrtice
+Myrtie
+Myrtille
+Myrtle
+Mysore
+Nabil
+Nachum
+Nad
+Nada
+Nadean
+Nadeem
+Nadeen
+Nader
+Nadia
+Nadim
+Nadine
+Nadir
+Nadiya
+Nady
+Nadya
+Nagaraj
+Nahum
+Naile
+Naim
+Naima
+Naji
+Najib
+Nakina
+Nalani
+Nalin
+Nam
+Nam-Kiet
+Nam-Soo
+Namrata
+Nan
+Nana
+Nananne
+Nance
+Nancee
+Nancey
+Nanci
+Nancie
+Nancy
+Nandita
+Nando
+Nanete
+Nanette
+Nang
+Nani
+Nanice
+Nanine
+Nannette
+Nanni
+Nannie
+Nanny
+Nanon
+Naohiko
+Naoma
+Naomi
+Nara
+Naren
+Narendra
+Naresh
+Nari
+Narida
+Nariko
+Narinder
+Narrima
+Naser
+Nash
+Nashib
+Nashir
+Nashville
+Nasser
+Nat
+Nata
+Natala
+Natalee
+Natalie
+Natalina
+Nataline
+Nataly
+Natalya
+Natascha
+Natasha
+Natasja
+Natassia
+Natassja
+Nath
+Nathalia
+Nathalie
+Nathan
+Nathaniel
+National
+Natividad
+Natka
+Natty
+Natver
+Naval
+Naveen
+Nawa
+Nayan
+Nayneshkumar
+Nazi
+Nazib
+Neal
+Neala
+Ned
+Neda
+Nedda
+Nedi
+Neely
+Neena
+Neetu
+Neil
+Neila
+Neile
+Neill
+Neilla
+Neille
+Nel
+Nelda
+Nelia
+Nelie
+Nell
+Nelle
+Nelleke
+Nelli
+Nellie
+Nelly
+Nelson
+Nenad
+Nerissa
+Nerita
+Nermana
+Nert
+Nerta
+Nerte
+Nerti
+Nertie
+Nerty
+Ness
+Nessa
+Nessi
+Nessie
+Nessy
+Nesta
+Neste
+Netas
+Netta
+Netti
+Nettie
+Nettle
+Netty
+Nevein
+Nevil
+Neville
+Nevsa
+New
+Newell
+Newton
+Neysa
+Nga
+Ngai
+Ngan
+Nguyen
+Nguyet
+Nha
+Nhien
+Nhut
+Nial
+Niall
+Nic
+Nichol
+Nicholas
+Nichole
+Nicholle
+Nick
+Nicki
+Nickie
+Nicky
+Nico
+Nicol
+Nicola
+Nicolas
+Nicole
+Nicolea
+Nicolette
+Nicoli
+Nicolina
+Nicoline
+Nicolle
+Niek
+Niel
+Nigel
+Nijen
+Nik
+Nikaniki
+Nike
+Niki
+Nikki
+Nikkie
+Nikky
+Nikolaos
+Nikoletta
+Nikolia
+Nikos
+Nill
+Nils
+Nina
+Ninetta
+Ninette
+Ning
+Ninnetta
+Ninnette
+Ninno
+Ninon
+Nir
+Nirmal
+Nishith
+Nissa
+Nisse
+Nissie
+Nissy
+Nita
+Nitin
+Nixie
+Niz
+Nj
+Noami
+Nobuko
+Nobutaka
+Node
+Noel
+Noelani
+Noell
+Noella
+Noelle
+Noellyn
+Noelyn
+Noemi
+Noeschka
+Nola
+Nolana
+Nolie
+Nollie
+Nomi
+Nona
+Nonah
+Nong
+Noni
+Nonie
+Nonna
+Nonnah
+Nooshin
+Nopi
+Nora
+Norah
+Noraly
+Norbert
+Norcal
+Norean
+Noreen
+Norel
+Norene
+Norikatsu
+Norikazu
+Noriko
+Norina
+Norine
+Norio
+Norm
+Norma
+Norman
+Normand
+Norri
+Norrie
+Norry
+Norstar
+Norton
+Norvie
+Noslab
+Notley
+Noubar
+Nova
+Novelia
+Novene
+Noyes
+Nuno
+Nuntel
+Nurettin
+Nurhan
+Nuri
+Nuvit
+Nydia
+Nyssa
+Octavia
+Octavio
+Odele
+Odelia
+Odelinda
+Odella
+Odelle
+Odessa
+Odetta
+Odette
+Odile
+Odilia
+Odille
+Ofelia
+Ofella
+Ofilia
+Oguz
+Ohio
+OJ
+Okan
+Okey
+Oksana
+Ola
+Olav
+Ole
+Oleesa
+Olenka
+Olga
+Olia
+Olimpia
+Olive
+Oliver
+Olivette
+Olivia
+Olivie
+Oliy
+Ollie
+Olly
+Olusola
+Olva
+Olwen
+Olympe
+Olympia
+Olympie
+Omar
+Omayma
+Omer
+Ondrea
+Oneida
+Onette
+Onge
+Onida
+Oona
+Oorschot
+Opal
+Opalina
+Opaline
+Open
+Oper
+Ophelia
+Ophelie
+Opto
+Ora
+Oral
+Oralee
+Oralia
+Oralie
+Oralla
+Oralle
+Orden
+Orel
+Orelee
+Orelia
+Orelie
+Orella
+Orelle
+Oren
+Orenzo
+Oriana
+Orie
+Orlando
+Orly
+Orlyn
+Orsa
+Orsola
+Ortensia
+Oryal
+Osama
+Oscar
+Osiris
+Osmond
+Ossama
+Otakar
+Otfried
+Otha
+Othelia
+Othella
+Othilia
+Othilie
+Ott
+Ottawa
+Ottcsr
+Otter
+Ottilie
+Oue
+Ovila
+Owen
+Ozay
+Ozlem
+Pac
+Pacific
+Padma
+Padraig
+Padriac
+Page
+Paige
+Painterson
+Pak
+Pak-Jong
+Pal
+Palme
+Palmer
+Paloma
+Pam
+Pamela
+Pamelina
+Pamella
+Pammi
+Pammie
+Pammy
+Panch
+Pandora
+Pankaj
+Pankesh
+Panos
+Pansie
+Pansy
+Paola
+Paolina
+Papagena
+Paper
+Papers
+Paqs
+Par
+Pardeep
+Pardip
+Pardo
+Parham
+Parker
+Parkinson
+Parks
+Parminder
+Parnell
+Pars
+Partap
+Partha
+Partick
+Parveen
+Parvin
+Parviz
+Pas
+Pascal
+Pascale
+Pasiedb
+Pat
+Patadm
+Patch
+Patches
+Patching
+Patchit
+Patience
+Patra
+Patrica
+Patrice
+Patricia
+Patrick
+Patrizia
+Patsy
+Patt
+Patti
+Pattie
+Patty
+Paul
+Paula
+Paule
+Pauletta
+Paulette
+Pauli
+Paulie
+Paulien
+Paulina
+Pauline
+Paulinus
+Paulita
+Paulo
+Paulus
+Pauly
+Pavia
+Pavla
+Pawel
+Payroll
+Pcta
+Pde
+Peach
+Pearl
+Pearla
+Pearle
+Pearline
+Peder
+Pedro
+Peg
+Pegeen
+Peggi
+Peggie
+Peggy
+Pei-Chien
+Pelly
+Pen
+Penang
+Penelopa
+Penelope
+Peng
+Peng-David
+Penni
+Pennie
+Penny
+Pension
+Pepi
+Pepita
+Per
+Percy
+Peri
+Peria
+Perl
+Perla
+Perle
+Perri
+Perrin
+Perrine
+Perry
+Persis
+Pet
+Peta
+Petar
+Pete
+Peter
+Petr
+Petra
+Petre
+Petri
+Petrina
+Petronella
+Petronia
+Petronilla
+Petronille
+Petter
+Petunia
+Pey-Kee
+Phaedra
+Phaidra
+Phan
+Phat
+Phebe
+Phedra
+Phelia
+Phil
+Philip
+Philipa
+Philippa
+Philippe
+Philippine
+Philis
+Phillida
+Phillie
+Phillip
+Phillis
+Philly
+Philomena
+Phoebe
+Phoenix
+Phu
+Phuoc
+Phuong
+Phyl
+Phylis
+Phyllida
+Phyllis
+Phyllys
+Phylys
+Pia
+Pic
+Pick
+Pier
+Pierette
+Piero
+Pierre
+Pierre-Alain
+Pierre-Andre
+Pierre-Henri
+Pierre-Marc
+Pierre-Yves
+Pierrette
+Pierrick
+Pieter
+Pietra
+Pinakin
+Pinder
+Pinecrest
+Ping
+Ping-Kong
+Piotr
+Piper
+Pippa
+Pippy
+Pirooz
+Piroska
+Pit
+Pittsburgh
+Pivert
+Piyush
+Po
+Poh-Soon
+Pojanart
+Poldi
+Polly
+Pollyanna
+Pooh
+Poppy
+Porfirio
+Portia
+Poulos
+Powell
+Power
+Prab
+Prabir
+Pradeep
+Pradip
+Pradyumn
+Prafula
+Prakash
+Pramod
+Prams
+Prashant
+Pratibha
+Praveen
+Prayson
+Prem
+Preston
+Previn
+Pricing
+Print
+Priore
+Pris
+Prisca
+Priscella
+Priscilla
+Prissie
+Pritchard
+Priti
+Prity
+Priya
+Problems
+Pru
+Prudence
+Prudi
+Prudy
+Prue
+Pryor
+Pui-Wah
+Pulak
+Puneet
+Puran
+Purnam
+Purvee
+Qainfo
+Qainsp
+Quality
+Quan
+Quang
+Quang-Trung
+Queenie
+Quentin
+Querida
+Quinn
+Quinta
+Quintana
+Quintilla
+Quintina
+Quoc
+Quoc-Vu
+Quon
+Quyen
+Quynh
+Rachael
+Rachel
+Rachele
+Rachelle
+Radames
+Radford
+Radha
+Radio
+Radomir
+Radoslav
+Rae
+Raeann
+Raf
+Rafa
+Rafael
+Rafaela
+Rafaelia
+Rafaelita
+Raffi
+Rafi
+Rafiq
+Raghuvir
+Ragu
+Ragui
+Rahal
+Rahel
+Raina
+Raine
+Rainer
+Raj
+Rajan
+Rajani
+Rajeev
+Rajesh
+Rajinderpal
+Rajiv
+Raju
+Rakel
+Rakesh
+Rakhuma
+Raleigh
+Ralina
+Ralph
+Ram
+Rama
+Ramakant
+Raman
+Ramana
+Ramanamurthy
+Ramanand
+Ramaprakash
+Ramesh
+Ramez
+Ramin
+Ramiz
+Ramniklal
+Ramon
+Ramona
+Ramonda
+Ramses
+Ran-Joo
+Rana
+Rand
+Randa
+Randal
+Randall
+Randee
+Randene
+Randhir
+Randi
+Randie
+Randolph
+Randy
+Ranea
+Ranee
+Ranga
+Rani
+Rania
+Ranice
+Ranique
+Ranjit
+Rank
+Ranna
+Ransom
+Ranson
+Ranvir
+Rao
+Raouf
+Raoul
+Raphaela
+Raquel
+Raquela
+Rashid
+Rashmi
+Rasia
+Rasla
+Raudres
+Raul
+Raven
+Ravi
+Ravinder
+Ray
+Raychel
+Raye
+Raymond
+Rayna
+Raynald
+Raynell
+Rayshell
+Raz
+Rch
+Rchisn
+Rchlab
+Rea
+Reagan
+Real
+Reba
+Rebbecca
+Rebe
+Rebeca
+Rebecca
+Rebecka
+Rebeka
+Rebekah
+Rebekkah
+Rec
+Redgie
+Ree
+Reeba
+Reed
+Reena
+Reese
+Reeta
+Reeva
+Reg
+Regan
+Reggi
+Reggie
+Regina
+Reginald
+Regine
+Regis
+Reid
+Reiko
+Reina
+Reind
+Reine
+Reinhard
+Reinhold
+Rejean
+Rejeanne
+Remi
+Remington
+Remo
+Remy
+Ren
+Rena
+Renae
+Renata
+Renate
+Renato
+Rene
+Rene-Alain
+Renee
+Renell
+Renelle
+Renie
+Rennie
+Renny
+Reno
+Renu
+Reta
+Retha
+Reuben
+Reva
+Revkah
+Rex
+Rey
+Reyaud
+Reyna
+Reynold
+Reza
+Reznechek
+Rhea
+Rheal
+Rheba
+Rheta
+Rhett
+Rhetta
+Rhiamon
+Rhianna
+Rhianon
+Rhoda
+Rhodia
+Rhodie
+Rhody
+Rhona
+Rhonda
+Ri
+Ria
+Riane
+Riannon
+Rianon
+Riaz
+Ric
+Rica
+Ricardo
+Ricca
+Rich
+Richard
+Richardo
+Richardson
+Richelle
+Richie
+Rici
+Rick
+Rickey
+Ricki
+Rickie
+Rickrd
+Ricky
+Rico
+Riekie
+Rieni
+Rigby
+Rigel
+Rigoberto
+Rijn
+Rijos
+Rijswijk
+Riki
+Rikki
+Rilla
+Rima
+Rina
+Ringo
+Rini
+Rio
+Risa
+Rita
+Riva
+Rivalee
+Rivi
+Rivkah
+Rivy
+Riyad
+Riyaz
+Rizwan
+Rizzo
+Roana
+Roanna
+Roanne
+Rob
+Robb
+Robbi
+Robbie
+Robbin
+Robby
+Robbyn
+Robena
+Robenia
+Robert
+Roberta
+Roberto
+Robertson
+Robin
+Robina
+Robinet
+Robinett
+Robinetta
+Robinette
+Robinia
+Roby
+Robyn
+Rocco
+Roch
+Rochell
+Rochella
+Rochelle
+Rochette
+Rocio
+Rocke
+Rocky
+Rod
+Roda
+Roddy
+Roderick
+Rodger
+Rodi
+Rodie
+Rodina
+Rodney
+Rodrigo
+Rodrigus
+Roe
+Roel
+Roelof
+Rogelio
+Roger
+Rohit
+Rois
+Rojer
+Roland
+Rolande
+Rolando
+Rolf
+Rollie
+Rollo
+Rolly
+Roly
+Roman
+Romano
+Romina
+Rommel
+Romola
+Romona
+Romonda
+Romulus
+Romy
+Ron
+Rona
+Ronald
+Ronalda
+Ronan
+Ronda
+Ronen
+Rong-Chin
+Roni-Jean
+Ronica
+Ronn
+Ronna
+Ronneke
+Ronni
+Ronnica
+Ronnie
+Ronny
+Roobbie
+Roque
+Rora
+Rori
+Rorie
+Rory
+Ros
+Rosa
+Rosabel
+Rosabella
+Rosabelle
+Rosaleen
+Rosalia
+Rosalie
+Rosalind
+Rosalinda
+Rosalinde
+Rosaline
+Rosalyn
+Rosalynd
+Rosamond
+Rosamund
+Rosana
+Rosanna
+Rosanne
+Rosario
+Roscoe
+Rose
+RoseAnne
+Roseann
+Roseanna
+Roselia
+Roselin
+Roseline
+Rosella
+Roselle
+Rosemaria
+Rosemarie
+Rosemary
+Rosemonde
+Rosene
+Rosetta
+Rosette
+Roshelle
+Rosie
+Rosina
+Rosita
+Roslyn
+Rosmunda
+Ross
+Rosy
+Roupen
+Row
+Rowan
+Rowe
+Rowena
+Roxana
+Roxane
+Roxanna
+Roxanne
+Roxi
+Roxie
+Roxine
+Roxy
+Roy
+Roya
+Royal
+Royce
+Roz
+Rozalia
+Rozalie
+Rozalin
+Rozamond
+Rozanna
+Rozanne
+Roze
+Rozele
+Rozella
+Rozelle
+Rozett
+Rozina
+Ru
+Ruben
+Rubetta
+Rubi
+Rubia
+Rubie
+Rubin
+Rubina
+Ruby
+Ruchel
+Ruchi
+Rudie
+Rudolf
+Rudolph
+Rudy
+Rueben
+Rui
+Rui-Yuan
+Rungroj
+Ruperta
+Rurick
+Russ
+Russel
+Russell
+Rustu
+Rusty
+Ruth
+Ruthann
+Ruthanne
+Ruthe
+Ruthi
+Ruthie
+Ruthy
+Ruud
+Ryann
+Rycca
+Ryman
+Ryoung
+Ryszard
+Saba
+Sabah
+Sabina
+Sabine
+Sabra
+Sabrina
+Sabuson
+Sacha
+Sachiko
+Sacto
+Sada
+Sadan
+Sadella
+Sadie
+Sadru
+Sadye
+Saeed
+Saeid
+Sage
+Saibal
+Said
+Saidee
+Saied
+Sait
+Sal
+Salah
+Salaidh
+Saleem
+Saleh
+Sales
+Salim
+Salina
+Salis
+Sallee
+Salli
+Sallie
+Sally
+Sallyann
+Sallyanne
+Saloma
+Salome
+Salomi
+Salvador
+Salvatore
+Sam
+Saman
+Samantha
+Samara
+Samaria
+Sameh
+Sami
+Samia
+Samir
+Sammie
+Sammy
+Samual
+Samuel
+Sanae
+Sanchez
+Sande
+Sandeep
+Sandhya
+Sandi
+Sandie
+Sandra
+Sandrine
+Sandro
+Sandy
+Sandye
+Sang-Maun
+Sangman
+Sanja
+Sanjay
+Sanjeet
+Sanjeev
+Sanjoy
+Santiago
+Sapphira
+Sapphire
+Sara
+Sara-ann
+Saraann
+Sarah
+Sarajane
+Sarangarajan
+Sarath
+Saree
+Sarena
+Sarene
+Sarette
+Sari
+Sarina
+Sarine
+Sarita
+Saroj
+Sascha
+Sasha
+Sashenka
+Sask
+Saskia
+Sastry
+Saswata
+Sati
+Satoshi
+Sattar
+Satyajit
+Saudra
+Saul
+Saumitra
+Saundra
+Savina
+Savita
+Sayed
+Sayeeda
+Sayla
+Sayre
+Scarlet
+Scarlett
+Schaffer
+Schell
+Schouwen
+Schyndel
+Scot
+Scott
+Scottie
+Scotty
+Scovill
+Scpbuild
+Scpiivo
+Scptest
+Seamus
+Sean
+Seana
+Seang
+Seanna
+Sebastian
+Sedat
+Sedigheh
+Seelan
+Seema
+Seiji
+Seiko
+Sejal
+Seka
+Sela
+Selcuk
+Selena
+Selene
+Selestina
+Selia
+Selie
+Selim
+Selime
+Selina
+Selinda
+Seline
+Sella
+Selle
+Selma
+Selva
+Selvaraj
+Selwyn
+Semmler
+Sena
+Sephira
+Seraphine
+Serban
+Serdar
+Serena
+Serene
+Serge
+Sergei
+Sergio
+Sergiu
+Seth
+Setsuko
+Seungchul
+Seven
+Severin
+Sey-Ping
+Seyar
+Seyfollah
+Seyma
+Shabbir
+Shae
+Shafiq
+Shafique
+Shahab
+Shahid
+Shahram
+Shahriar
+Shahrokh
+Shaib
+Shaibal
+Shailendra
+Shailesh
+Shailin
+Shaina
+Shaine
+Shaji
+Shaker
+Shakoor
+Shalna
+Shalne
+Shama
+Shamim
+Shamshad
+Shamsia
+Shan
+Shana
+Shanda
+Shandee
+Shandeigh
+Shandie
+Shandra
+Shandy
+Shane
+Shani
+Shanie
+Shankar
+Shanna
+Shannah
+Shannen
+Shannon
+Shanon
+Shanta
+Shantee
+Shanti
+Shara
+Sharad
+Sharai
+Sharee
+Shari
+Sharia
+Sharity
+Sharl
+Sharla
+Sharleen
+Sharlene
+Sharline
+Sharon
+Sharona
+Sharone
+Sharri
+Sharron
+Sharyl
+Sharyn
+Shashank
+Shashi
+Shaughan
+Shaukat
+Shaun
+Shauna
+Shaw
+Shawn
+Shawna
+Shawnee
+Shay
+Shayla
+Shaylah
+Shaylyn
+Shaylynn
+Shayna
+Shayne
+Shea
+Sheba
+Shedman
+Sheela
+Sheelagh
+Sheelah
+Sheena
+Sheeree
+Sheila
+Sheila-kathryn
+Sheilah
+Sheilakathryn
+Sheileagh
+Shekar
+Shekhar
+Shel
+Shela
+Shelagh
+Shelba
+Shelbi
+Shelby
+Sheldon
+Shelia
+Shell
+Shelley
+Shelli
+Shellie
+Shelly
+Shelton
+Shen-Zhi
+Shena
+Shep
+Sher
+Sheree
+Sheri
+Sheri-Lynn
+Sheridan
+Sherie
+Sherill
+Sherilyn
+Sherline
+Sherman
+Sherrel
+Sherri
+Sherrie
+Sherrill
+Sherry
+Sherrye
+Sherryl
+Sherwood
+Sherwyn
+Sherye
+Sheryl
+Shiela
+Shigeki
+Shigeru
+Shih-Dar
+Shila
+Shilla
+Shina
+Shing-Cheong
+Shing-Chi
+Shingcheon
+Shinichi
+Shinichiro
+Shir
+Shirene
+Shirin
+Shirish
+Shirl
+Shirlee
+Shirleen
+Shirlene
+Shirley
+Shirley-Ann
+Shirline
+Shiroshi
+Shiu
+Shiv
+Shiva
+Shivdarsan
+Shlomo
+Shobana
+Shoeb
+Shoji
+Shona
+Shorwan
+Shoshana
+Shoshanna
+Shou
+Shou-Mei
+Shouli
+Shuang
+Shuichi
+Shuji
+Shunhui
+Shunro
+Shuo
+Shuqing
+Shutterbug
+Shya-Yun
+Shyam
+Shyoko
+Siamack
+Siamak
+Siana
+Sianna
+Sib
+Sibbie
+Sibby
+Sibeal
+Sibel
+Sibella
+Sibelle
+Sibilla
+Sibley
+Sibyl
+Sibylla
+Sibylle
+Sichao
+Sickle
+Sid
+Sidney
+Sidone
+Sidoney
+Sidonia
+Sidonnie
+Sieber
+Siew
+Siew-Kiat
+Sig
+Siggy
+Sigrid
+Siham
+Sik-Yin
+Sika
+Sil
+Sile
+Sileas
+Silva
+Silvana
+Silvester
+Silvestro
+Silvia
+Silvie
+Simen
+Simeon
+Simhan
+Simon
+Simon-Cheuk
+Simon-Pui-Lok
+Simona
+Simone
+Simonette
+Simonne
+Simulation
+Sindee
+Sing-Pin
+Sinh
+Siobhan
+Sioux
+Siouxie
+Sir
+Sisely
+Sisile
+Sissela
+Sissie
+Sissy
+Siu-Ling
+Siu-Man
+Siusan
+Siva
+Skiclub
+Skip
+Skipper
+Skippy
+Sky
+Skyler
+Sluis
+Smita
+Smith
+Snair
+Snehal
+Sofeya
+Sofia
+Sofie
+Sohail
+Sohale
+Sohayla
+Sol
+Solita
+Solomon
+Somsak
+Son
+Sonbol
+Sondra
+Sonia
+Sonja
+Sonni
+Sonnie
+Sonnnie
+Sonny
+Sono
+Sonoe
+Sonya
+Sophey
+Sophi
+Sophia
+Sophie
+Sophronia
+Sorcha
+Sorin
+Sosanna
+Sotos
+Souheil
+Souphalack
+Souza
+Soyeh
+Soyong
+Spence
+Spencer
+Spenser
+Spicer
+Spiros
+Srinivas
+Sriranjani
+Sriv
+StClair
+Stace
+Stacee
+Stacey
+Staci
+Stacia
+Stacie
+Stacy
+Stafani
+Stan
+Stanislas
+Stanislaw
+Stanley
+Star
+Starla
+Starlene
+Starlet
+Starlin
+Starr
+Stars
+Starsdps
+Stateson
+Steen
+Stefa
+Stefan
+Stefania
+Stefanie
+Stefano
+Steffane
+Steffen
+Steffi
+Steffie
+Steinar
+Stella
+Stepha
+Stephan
+Stephana
+Stephane
+Stephani
+Stephanie
+Stephannie
+Stephany
+Stephen
+Stephenie
+Stephi
+Stephie
+Stephine
+Stergios
+Sterling
+Stesha
+Stevana
+Steve
+Steven
+Stevena
+Stew
+Stewart
+Stirling
+Stock
+Stoddard
+Stone
+Storm
+Stormi
+Stormie
+Stormy
+Stu
+Stuart
+Student
+Su
+Suat
+Subhash
+Subhashini
+Subhra
+Subi
+Subra
+Subramaniam
+Subu
+Sucha
+Sudesh
+Sue
+Sue-May
+Sueanne
+Suellen
+Suha
+Suhas
+Suk-Yin
+Sukey
+Sukhendu
+Sukhwant
+Suki
+Sula
+Sule
+Sultan
+Sundaram
+Sunil
+Sunning
+Sunny
+Sunshine
+Supriya
+Surendra
+Suria
+Surinder
+Survey
+Surya
+Susan
+Susana
+Susanetta
+Susann
+Susanna
+Susannah
+Susanne
+Susette
+Susi
+Susie
+Susil
+Susy
+Suvanee
+Suzan
+Suzane
+Suzann
+Suzanna
+Suzanne
+Suzette
+Suzi
+Suzie
+Suzy
+Svend
+Svenn-Erik
+Svr
+Swact
+Swandi
+Swd
+Swee-Joo
+Sybil
+Sybila
+Sybilla
+Sybille
+Sybyl
+Syd
+Sydel
+Sydelle
+Sydney
+Syed
+Syl
+Sylva
+Sylvain
+Sylvia
+Sylvie
+Sylvio
+Symen
+Synful
+Sys
+Syyed
+Tab
+Tabatha
+Tabbatha
+Tabbi
+Tabbie
+Tabbitha
+Tabby
+Tabina
+Tabitha
+Tac
+Tad
+Tadayuki
+Tadeusz
+Tae
+Taffy
+Tahir
+Tai
+Tai-Jen
+Taiwana
+Tak
+Tak-Wai
+Takako
+Takashi
+Takehiko
+Takis
+Talia
+Tallia
+Tallie
+Tallou
+Tallulah
+Tally
+Talya
+Talyah
+Tam
+Tamar
+Tamara
+Tamarah
+Tamarra
+Tamera
+Tami
+Tamiko
+Tamma
+Tammara
+Tammi
+Tammie
+Tammy
+Tamqrah
+Tamra
+Tan
+Tana
+Tandi
+Tandie
+Tandy
+Tanhya
+Tani
+Tania
+Tanitansy
+Tansy
+Tanya
+Tao
+Tap
+Tape
+Tara
+Tarah
+Tarik
+Tariq
+Taro
+Tarra
+Tarrah
+Tarte
+Tarus
+Taryn
+Taryna
+Tas
+Tash
+Tasha
+Tasia
+Tat
+Tata
+Tate
+Tatiana
+Tatiania
+Tats
+Tatsman
+Tatsuya
+Tatum
+Tatyana
+Tavis
+Tawauna
+Tawei
+Tawnya
+Tawsha
+Tayeb
+Tc
+Tchangid
+Tdr
+Te-Wei
+Team
+Tec
+Tech
+Technical
+Ted
+Tedda
+Teddi
+Teddie
+Teddy
+Tedi
+Tedra
+Teena
+Teetwo
+Tehchi
+Teiichi
+Teirtza
+Tej
+Tele
+Tenney
+Teodora
+Tera
+Terence
+Teresa
+Terese
+Teresina
+Teresita
+Teressa
+Terez
+Teri
+Teriann
+Terra
+Terrell
+Terrence
+Terri
+Terri-jo
+Terrie
+Terrijo
+Terrill
+Terry
+Terrye
+Tersina
+Teruko
+Terza
+Tesa
+Tesfagaber
+Tess
+Tessa
+Tessi
+Tessie
+Tessty
+Tessy
+Tetsumo
+Tetsuo
+Tetsuya
+Tetsuyuki
+Tex
+Teymour
+Thad
+Thaddeus
+Thakor
+Thalia
+Thane
+Thang
+Thanh
+Thanh-Ha
+Thanh-Hoa
+Thanh-Hung
+Thanh-Quoc
+Thanh-Son
+Thanh-Tinh
+Thanos
+Thayne
+The
+Thea
+Theadora
+Theda
+Thedora
+Thekla
+Thelma
+Theo
+Theodor
+Theodora
+Theodore
+Theodosia
+Theresa
+Therese
+Theresina
+Theresita
+Theressa
+Therine
+Thi
+Thi-cuc
+Thia
+Thierry
+Thieu
+Thinh
+Thoai
+Thom
+Thomas
+Thomasa
+Thomasin
+Thomasina
+Thomasine
+Thompson
+Thomson
+Thor
+Thornton
+Thrift
+Thuan
+Thuong
+Thuthuy
+Thuy
+Tian
+Tianbao
+Tibor
+Tidwell
+Tien
+Tiena
+Tierney
+Tiertza
+Tiff
+Tiffani
+Tiffanie
+Tiffany
+Tiffi
+Tiffie
+Tiffy
+Tiina
+Tilak
+Tilda
+Tildi
+Tildie
+Tildy
+Tillie
+Tilly
+Tilmon
+Tim
+Timi
+Timm
+Timmi
+Timmie
+Timmy
+Timothea
+Timothy
+Tin
+Tina
+Tine
+Tineke
+Ting
+Tini
+Tino
+Tiny
+Tiong-Hoe
+Tiphani
+Tiphanie
+Tiphany
+Tish
+Tisha
+Tobe
+Tobey
+Tobi
+Toby
+Tobye
+Tod
+Todd
+Toinette
+Tom
+Toma
+Tomas
+Tomasina
+Tomasine
+Tomasz
+Tomi
+Tommi
+Tommie
+Tommy
+Tomoyoshi
+Tomy
+Toney
+Toni
+Tonia
+Tonie
+Tonu
+Tony
+Tonya
+Tonye
+Tootsie
+Torcac
+Torey
+Tori
+Torie
+Torre
+Torrie
+Tory
+Tosca
+Toshi
+Toshihiro
+Toshinari
+Toss
+Tova
+Tove
+Toyanne
+Toyoji
+Tracee
+Tracey
+Traci
+Tracie
+Tracy
+Tran
+Trang
+Travis
+Trees
+Trenna
+Trent
+Tres
+Tresa
+Trescha
+Tresrch
+Tressa
+Trev
+Trever
+Trevor
+Trey
+Tri
+Tricci
+Tricia
+Tricord
+Trina
+Trinh
+Trish
+Trisha
+Trista
+Tristano
+Trix
+Trixi
+Trixie
+Trixy
+Troy
+Tru-Fu
+Truda
+Trude
+Trudey
+Trudi
+Trudie
+Trudy
+Trula
+Truman
+Truus
+Tsing
+Tsugio
+Tsuyoshi
+Tu
+Tuan
+Tuesday
+Tuhina
+Tulip
+Tun-Lin
+Tung
+Tuoi
+Turgay
+Turkey
+Turus
+Tushar
+Twana
+Twiggy
+Twila
+Twyla
+Txp
+Ty
+Tybi
+Tybie
+Tyke
+Tyler
+Tyne
+Tyronda
+Tzung
+Uday
+Udaya
+Ula
+Ulf
+Ulla
+Ulrica
+Ulrika
+Ulrikaumeko
+Ulrike
+Umakanth
+Umeko
+Umesh
+Una
+Una-Mae
+Unreg
+Upen
+Uri
+Ursa
+Ursala
+Ursola
+Ursula
+Ursulina
+Ursuline
+Usa
+Usman
+Usrouter
+Uswrsd
+Uta
+Utah
+Utilla
+Utpala
+Uunko
+Vac-man
+Vadi
+Vahe
+Vahid
+Val
+Valaree
+Valaria
+Vale
+Valeda
+Valencia
+Valene
+Valenka
+Valentia
+Valentina
+Valentine
+Valera
+Valeria
+Valerie
+Valery
+Valerye
+Valida
+Valina
+Valinda
+Valli
+Vallie
+Vallier
+Vallipuram
+Vally
+Valma
+Valry
+Van
+Van-King
+Vance
+Vanda
+Vanessa
+Vania
+Vanity
+Vanna
+Vanni
+Vannie
+Vanny
+Vanya
+Varennes
+Vasan
+Vassilis
+Vasu
+Vaughn
+Vax
+Ved
+Veda
+Veen
+Veena
+Veleta
+Velma
+Velvet
+Ven
+Veneice
+Venita
+Venkat
+Venkatakrishna
+Venkataraman
+Venus
+Vera
+Veradis
+Vere
+Verena
+Verene
+Verghese
+Veriee
+Verile
+Verina
+Verinder
+Verine
+Verla
+Verlyn
+Vern
+Verna
+Vernice
+Vernon
+Veronica
+Veronika
+Veronike
+Veronique
+Vesna
+Vevay
+Vi
+Vic
+Vicente
+Vicheara
+Vick
+Vicki
+Vickie
+Vicky
+Victor
+Victoria
+Vicuong
+Vida
+Vidya
+Viera
+Vijai
+Vijay
+Vijayalaks
+Vijya
+Vikas
+Viki
+Vikki
+Vikky
+Viktor
+Viktoria
+Vilas
+Vilhelm
+Vilhelmina
+Vilis
+Vilma
+Vilok
+Vimal
+Vimi
+Vin
+Vina
+Vinay
+Vince
+Vincent
+Vincente
+Vincenzo
+Vinh
+Vinita
+Vinni
+Vinnie
+Vinny
+Vino
+Vinod
+Viola
+Violante
+Viole
+Violet
+Violetta
+Violette
+Vipi
+Viqar
+Virgie
+Virgil
+Virgina
+Virginia
+Virginie
+Vishwa
+Vispy
+Vita
+Vital
+Vithit
+Vitia
+Vito
+Vitoria
+Vittoria
+Vittorio
+Viv
+Viva
+Vivek
+Vivi
+Vivia
+Vivian
+Viviana
+Viviane
+Vivianna
+Vivianne
+Vivie
+Vivien
+Viviene
+Vivienne
+Viviyan
+Vivyan
+Vivyanne
+Vlad
+Vladimir
+Vlado
+Vm
+Vmbackup
+Vmchange
+Vmcord
+Vo
+Vonni
+Vonnie
+Vonny
+Voort
+Vradmin
+Vries
+Vrinda
+Vrouwerff
+Vu
+VuHoan
+VuQuoc
+Vyky
+Vyza
+Wade
+Wai
+Wai-Bun
+Wai-Chau
+Wai-Hung
+Wai-Leung
+Wai-Man
+Wai-ching
+Waichi
+Waja
+Wakako
+Wallace
+Walley
+Wallie
+Wallis
+Walliw
+Wally
+Walt
+Walter
+Walton
+Waly
+Wan
+Wanda
+Wandie
+Wandis
+Waneta
+Wanids
+Wannell
+Warden
+Wargnier
+Warren
+Warwick
+Wassim
+Waverly
+Wayne
+Weber
+Wee-Lin
+Wee-Seng
+Wee-Thong
+Weilin
+Weiping
+Weitzel
+Weldon
+Wen
+Wen-Kai
+Wenda
+Wendel
+Wendeline
+Wendell
+Wendi
+Wendie
+Wendy
+Wendye
+Wenona
+Wenonah
+Wenxi
+Weringh
+Werner
+Wes
+Wesley
+Whitfield
+Whitney
+Wiebe
+Wiebren
+Wiele
+Wiesje
+Wieslaw
+Wieslawa
+Wil
+Wilbur
+Wileen
+Wilf
+Wilford
+Wilfred
+Wilhelmina
+Wilhelmine
+Wilhelmus
+Wilie
+Wilkin
+Will
+Willa
+Willabella
+Willamina
+Willard
+Willeke
+Willetta
+Willette
+Willi
+William
+Willie
+Willis
+Willow
+Willy
+Willyt
+Wilma
+Wilmer
+Wilmette
+Wilmont
+Wilona
+Wilone
+Wilow
+Wilson
+Wilton
+Win
+Windowing
+Windy
+Wing
+Wing-Ki
+Wing-Man
+Wini
+Winifred
+Winna
+Winnah
+Winne
+Winni
+Winnie
+Winnifred
+Winny
+Winona
+Winonah
+Winston
+Witold
+Wits
+Witte
+Wladyslaw
+Woei-Peng
+Wojciech
+Wolfgang
+Wonda
+Wong
+Woodline
+Woodson
+Woody
+Woon
+Wray
+Wren
+Wrennie
+Wylma
+Wylo
+Wynn
+Wynne
+Wynnie
+Wynny
+Xantippe
+Xavier
+Xaviera
+Xena
+Xenia
+Xi-Nam
+Xiao-Ming
+Xiaofeng
+Xiaojing
+Xiaomei
+Xu
+Xuan-Lien
+Xuong
+Xylia
+Xylina
+Yalcin
+Yalonda
+Yan-Zhen
+Yannick
+Yannis
+Yao
+Yarlanda
+Yasar
+Yaser
+Yasmeen
+Yasmin
+Yate
+Yatish
+Yau-Fun
+Yavar
+Yavuz
+Yawar
+Yc
+Yee-Ning
+Yehuda
+Yeirnie
+Yelena
+Yen
+Yetta
+Yettie
+Yetty
+Yeung
+Yevette
+Yih
+Yihban
+YikHon
+Ying
+Ylaine
+Ynes
+Ynez
+Yoda
+Yodha
+Yogesh
+Yogi
+Yokan
+Yoke
+Yoke-Kee
+Yoko
+Yolanda
+Yolande
+Yolane
+Yolanthe
+Yong
+Yongli
+Yonik
+Yoram
+Yoshi
+Yoshiaki
+Yoshiko
+Yoshimitsu
+Yosuf
+Youji
+Young-June
+Yousef
+Youssef
+Youwen
+Yovonnda
+Ysabel
+Yu
+Yu-Chung
+Yu-Hung
+Yu-Kai
+Yuan
+Yudy
+Yue-Min
+Yueh
+Yueli
+Yuen
+Yuen-Pui
+Yueping
+Yuji
+Yuk-Wha
+Yukihiko
+Yukinaga
+Yukinobu
+Yuko
+Yuksel
+Yukuo
+Yumi
+Yung
+Yuri
+Yussuf
+Yutaka
+Yvan
+Yves
+Yvet
+Yvette
+Yvon
+Yvonne
+Zabrina
+Zack
+Zafar
+Zafer
+Zahara
+Zahid
+Zahir
+Zahirul
+Zahra
+Zaihua
+Zainab
+Zalee
+Zan
+Zandra
+Zaneta
+Zanni
+Zara
+Zarah
+Zarella
+Zaria
+Zarla
+Zarrin
+Zaven
+Zbignew
+Zbigniew
+Zdenek
+Zdenka
+Zdenko
+Zea
+Zeb
+Zehir-Charlie
+Zehra
+Zein
+Zeina
+Zelda
+Zeljko
+Zelma
+Zena
+Zenia
+Zere
+Zero
+Zhanna
+Zhengyu
+Zia
+Ziad
+Zilvia
+Zino
+Zita
+Zitella
+Zoe
+Zoel
+Zoenka
+Zofia
+Zohar
+Zola
+Zoltan
+Zonda
+Zondra
+Zongyi
+Zonnya
+Zora
+Zorah
+Zorana
+Zorina
+Zorine
+Zouheir
+Zsa zsa
+Zsazsa
+Zuben
+Zulema
+Zulfikar
+Zuzana
+Zyg
+Zygmunt
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/last.names b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/last.names
new file mode 100644
index 0000000..a7f9d60
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/last.names
@@ -0,0 +1,13419 @@
+Amar
+Atp
+Atpco
+Atrc
+Aalders
+Aasen
+Abadines
+Abazari
+Abbatantuono
+Abbate
+Abbie
+Abbott
+Abdalla
+Abdo
+Abdollahi
+Abdou
+Abdul-Nour
+Abdulla
+Abdullah
+Abe
+Abedi
+Abel
+Abell
+Abella
+Abello
+Abelow
+Abernathy
+Abernethy
+Abi-Aad
+Abou-Arrage
+Abou-Ezze
+Aboul-Magd
+Aboussouan
+Abovyan
+Abraham
+Abrahim
+Abrams
+Absi
+Acelvari
+Acharyya
+Achcar
+Achille
+Achkar
+Ackaouy
+Acker
+Acklin
+Ackwood
+Acree
+Acres
+Acs
+Actionteam
+Acton
+Aczel
+Adair
+Adam
+Adamczyk
+Adamkowski
+Adamo
+Adamowicz
+Adams
+Adamski
+Adamson
+Adamyk
+Adcock
+Adcox
+Addetia
+Addison
+Addona
+Adeney
+Aderhold
+Adey
+Adhem
+Adimari
+Adjangba
+Adkinson
+Adler
+Admin
+Admin-mtv
+Administration
+Administrator
+Adolfie
+Adolph
+Adornato
+Adorno
+Adriaansen
+Aery
+Afkham
+Afkham-ebrahimi
+Agarwal
+Aggarwal
+Aghi
+Aghili
+Agily
+Agnew
+Agnihotri
+Agostino
+Aguiar
+Aguilar
+Aguinsky
+Aguirre
+Ahad
+Ahdieh
+Ahlberg
+Ahlers
+Ahluwalia
+Ahmad
+Ahmadi
+Ahmed
+Aidarous
+Aiken
+Aimone
+Ainsworth
+Aitken
+Ajersch
+Akai
+Akbas
+Akens
+Akers
+Akhavan
+Akhtar
+Akita
+Akkermans
+Akrawi
+Aksel
+Akyurekli
+Al
+Al Bud
+Al-Basi
+Al-Tarabichi
+Aladangady
+Alanis
+Alanoly
+Alary
+Alavi
+Albea
+Albers
+Alberse
+Albert
+Alberts
+Alberty
+Albery
+Albrecht
+Albright
+Albritton
+Albtentac
+Alburger
+Alcock
+Alcott
+Alderdice
+Aldhizer
+Aldridge
+Alegre
+Aleksic
+Aleong
+Alexan
+Alexander
+Alexson
+Alfaro
+Alford
+Alfred
+Algie
+Algood
+Alguire
+Ali
+Alidina
+Alie
+Alikhan
+Alink
+Alkire
+Allahdin
+Allahyari
+Allam
+Allaman
+Allan
+Allard
+Allaway
+Allaye-Chan
+Allen
+Alles
+Alleva
+Alleyne
+Allgood
+Allison
+Allman
+Allwork
+Almeddahim
+Almon
+Alms
+Aloi
+Alomari
+Alperovich
+Alred
+Alsaleh
+Alshabout
+Alsop
+Alspaugh
+Alston
+Alswiti
+Altadonna
+Altay
+Alteen
+Altekar
+Altherr
+Alting-Mees
+Altman
+Altmann
+Alvarez
+Alvaro
+Alvi
+Aly
+Alzofon
+Amalu
+Amarsi
+Amato
+Amavisca
+Ambach
+Ambler
+Ambroise
+Ameen
+Amelkar
+America
+Amick
+Amin
+Amini
+Aminzadeh
+Amiot
+Amir
+Amlani
+Amorim
+Amos
+Amott
+Amouzgar
+Amstutz
+Amu
+Amundsen
+Ananmalay
+Ananth
+Anastasiadis
+Anastasio
+Anaya
+Ancel
+Anchia
+Anconetani
+Anctil
+Ander
+Andersen
+Anderson
+Anderton
+Andrade
+Andrassy
+Andre
+Andreasen
+Andreessen
+Andreatos
+Andress
+Andrew
+Andrews
+Andric
+Andros
+Andrukat
+Andrusiak
+Andruzzi
+Ange
+Angeli
+Angell
+Angerer
+Angermeyr
+Anglin
+Angobaldo
+Angus
+Angustia
+Angvall
+Anhorn
+Anker
+Annab
+Annabelle
+Annable
+Annas
+Annibale
+Annunziata
+Anolik
+Ansley
+Ansorger
+Anstead
+Anstett
+Anthonissen
+Anthony
+Antinucci
+Antkowiak
+Antle
+Antoft
+Antonarelli
+Antonelli
+Anwar
+Anzarouth
+Anzures
+Aoki
+Aparicio
+Apostolopoulos
+Appell
+Appenzeller
+Applebaum
+Applegarth
+Appleyard
+Applications
+Appugliese
+Apter
+Arabadjis
+Arai
+Aramideh
+Aravamudhan
+Arbuckle
+Archambault
+Archer
+Archibald
+Arcouet
+Ard
+Ardiel
+Ardizone
+Ardoin
+Arellano
+Arend
+Ares
+Areu
+Argento
+Arias
+Aribindi
+Arkesteijn
+Arko
+Armbruster
+Armenakis
+Armenta
+Armentrout
+Armes
+Armitage
+Armolavicius
+Armour
+Armstead
+Armstrong
+Arnauld
+Arnold
+Arnon
+Arnone
+Arnott
+Aronovich
+Aronson
+Aronstam
+Arora
+Arpin
+Arro
+Arsena
+Arsenault
+Arseneau
+Arthur
+Artola
+Artspssa
+Artuso
+Artzer
+Arunachalam
+Arvin
+Arwakhi
+Aryavong
+Asawa
+Asbill
+Ascott
+Asdel
+Asfazadour
+Asghar
+Asgharzadeh
+Ashar
+Ashbee
+Ashberry
+Ashby
+Ashdown
+Ashford
+Ashley
+Ashmore
+Ashraf
+Ashton
+Ashurkoff
+Ashwood-Smith
+Ashworth
+Asing
+Asistores
+Askins
+Asprer
+Asquin
+Assaad
+Asselin
+Assenza
+Assistance
+Associates
+Astalos
+Astle
+Astley
+Aston
+Astor
+Astorino
+Atalla
+Atcheson
+Atchison
+Athanassiadis
+Athwal
+Atkins
+Atkinson
+Atl
+Atlantic
+Atoui
+Atprs
+Attanasio
+Attarchi
+Attard
+Attaway
+Attenborough
+Atteridge
+Attfield
+Atwater
+Atwell-Byrne
+Au
+Au-Yang
+Au-Yeung
+AuYeung
+Aubin
+Aubrey
+Aubuchon
+Aucoin
+Aud
+Audet
+Audette
+Auerbach
+Augeri
+Augustus
+Auker
+Aula
+Aulakh
+Auld
+Ault
+Aumoine
+Aurelius
+Auriol
+Ausley
+Austin
+Australia
+Auth
+Auton
+Autoquote
+Avard
+Avellaneda
+Averett
+Averette
+Averill
+Aversa
+Avery
+Avirett
+Awadalla
+Awadia
+Awan
+Awano
+Axberg
+Ayandeh
+Ayaz
+Aydin
+Ayers
+Ayles
+Aylwin
+Ayotte
+Ayoubzadeh
+Ayoup
+Ayrault
+Ayre
+Ayres
+Ayscue
+Ayukawa
+Ayyuce
+Azad
+Azar
+Azari
+Azarshahi
+Azer
+Azevedo
+Aziz
+Azizuddin
+Azmak
+Azzuolo
+Baab
+Baader
+Babalola
+Babasaki
+Babb
+Babcock
+Baber
+Babin
+Babineau
+Babione
+Babyak
+Baccari
+Bacchiochi
+Bacciaglia
+Bach
+Bachecongi
+Bachelu
+Bachewich
+Bachmann
+Bachner
+Bachynski
+Backshall
+Baddeley
+Badelt
+Badenoch
+Badger
+Badjari
+Badmington
+Badowski
+Badza
+Bae
+Baenziger
+Baer
+Baerg
+Bagetakos
+Bagg
+Baggerman-Webster
+Baghdadi
+Bagi
+Bagnato
+Bagshaw
+Bagwell
+Bahgat
+Bahia
+Bahl
+Bahoric
+Bailetti
+Bailey
+Bailie
+Baillargeon
+Bailloux
+Bain
+Bainer
+Baines
+Bains
+Bainton
+Baird
+Bajada
+Bajpeyi
+Bakay
+Baker
+Baker-Gregory
+Bakhach
+Bakkum
+Balaban
+Balabanian
+Balakrishnan
+Balanger
+Balascak
+Balcom
+Balderston
+Baldock
+Baldridge
+Baldwin
+Bales
+Balgalvis
+Balkenhol
+Balkissoon
+Ballantine
+Ballard
+Ballarte
+Ballinger
+Ballios
+Ballou
+Ballyk
+Balmer
+Balog
+Balogh
+Balsas
+Balser
+Balter
+Baltodano
+Balutis
+Bambach
+Bame
+Bamfo
+Banaei
+Bandel
+Banens
+Banerd
+Banerjee
+Banez
+Banfalvi
+Bangert
+Bangia
+Banigan
+Banik
+Bankhead
+Banks
+Bannai
+Bannan
+Bannard
+Bannister
+Bansal
+Banu
+Banville
+Baquero
+Barabash
+Baragar
+Barakat
+Baran
+Barbary
+Barbe
+Barbeau
+Barberena
+Barbour
+Barclay
+Barcza
+Bardsley
+Bareham
+Barel
+Barenie
+Barentsen
+Barfield
+Barham
+Baribeau
+Baril
+Barker
+Barkhouse
+Barkley
+Barksdale
+Barkwill
+Barlow
+Barnaby
+Barnes
+Barnett
+Barnhardt
+Barnhart
+Barnhill
+Barnhouse
+Barnickel
+Barnwell
+Barolet
+Barr
+Barraclough
+Barrass
+Barrell
+Barrett
+Barreyre
+Barrientos
+Barriere
+Barrington
+Barritt
+Barron
+Barrows
+Barry
+Barsch
+Barsky
+Barsony
+Barstow
+Barszczewski
+Bartholomew
+Bartkowska
+Bartlett
+Bartley
+Bartolucci
+Barton
+Bartoszewicz
+Bartra
+Bartram
+Bartush
+Barty
+Bartz
+Barwikowski
+Basa
+Basco
+Bascombe
+Bashton
+Bashyam
+Basile
+Basinger
+Baskaran
+Baskerville
+Baskin
+Basladynski
+Basmadjian
+Basnett
+Bason
+Basser
+Bassett
+Bassignana
+Bassil
+Basta
+Bastarache
+Bastien
+Basu
+Batchelder
+Batchelor
+Batchoun
+Bateman
+Bates
+Bathrick
+Batson
+Battersby
+Battershill
+Battiston
+Batura
+Baudais
+Bauer
+Baughan
+Baugnon
+Baulch
+Baum
+Baumann
+Baumberg
+Baumert
+Bautista
+Bawek
+Baxter
+Bayer
+Bayerkohler
+Bayless
+Bayley
+Bayly
+Bayne
+Baynes
+Bayno
+Bayola
+Bayraktar
+Bays
+Bazarjani
+Bazemore
+Bazerghi
+Bazerman
+Bazik
+Baziuk
+Bcs
+Beagley
+Beal
+Beall
+Beals
+Beardmore
+Bears
+Beasley
+Beato
+Beaton
+Beattie
+Beattie-Hillier
+Beatty
+Beaty
+Beaubien
+Beaucaire
+Beauchaine
+Beauchamp
+Beauchemin
+Beaudet
+Beaudette
+Beaudin
+Beaudoin
+Beaudry
+Beaule
+Beaulieu
+Beaumier
+Beaumont
+Beaupre
+Beausejour
+Beauvais
+Beavington
+Beavis
+Bebber
+Bebee
+Becan
+Bechtel
+Becke
+Becker
+Beckett
+Beckham
+Beckie
+Beckman
+Beckstead
+Beckwith
+Beconovich
+Becquart
+Bedard
+Bedford
+Bedi
+Bedient
+Bedlington
+Bednar
+Bedoya
+Bedrosian
+Beebe
+Beeby
+Beecker
+Beehler
+Beekman
+Beeman
+Beerkens
+Beers
+Bees
+Beeston
+Beeton
+Befanis
+Beffert
+Beggs
+Begley
+Behlen
+Behler
+Behm
+Behnam
+Behrens
+Behroozi
+Beil
+Beilin
+Beine
+Beique
+Beisel
+Beitinjaneh
+Bejar
+Bekkedam
+Bektas
+Belair
+Belaire
+Beland
+Belanger
+Belboul
+Belcher
+Belcourt
+Belford
+Belich
+Belir
+Belisle
+Belk
+Bellefeuille
+Bellehumeur
+Bellew
+Belley
+Bellington
+Bellis
+Bello
+Bellosa
+Belmont
+Belohoubek
+Belrango
+Belson
+Belton
+Belzile
+Bemiller
+Bemis
+Ben-Ishai
+Benavides
+Benavidez
+Benchimol
+Benda
+Bender
+Beneda
+Benedek
+Benedetti
+Benefield
+Benefits
+Beneteau
+Benfield
+Benge
+Bengtson
+Benham
+Beninger
+Benjamin
+Benjes
+Bennatt
+Benne
+Bennefeld
+Benner
+Bennett
+Benning
+Bennison
+Benoit
+Benschop
+Benski
+Benson
+Benthin
+Bentley
+Bento
+Benton
+Benwell
+Benyamin-Seeyar
+Benyon
+Benzick
+Benzie
+Berek
+Berenbach
+Beresnikow
+Bereza
+Bergado
+Berger
+Bergeron
+Bergeson
+Bergland
+Bergman
+Bergmann
+Bergquist
+Bergsma
+Bergstrom
+Bergwerff
+Berhane
+Beriault
+Berkley
+Berman
+Bermel
+Berna
+Bernard
+Bernardi
+Bernardo
+Berndt
+Berneche
+Bernhardt
+Bernier
+Berning
+Bernstein
+Berrisford
+Berro
+Berryhill
+Berteau
+Berthelet
+Bertignoll
+Bertini
+Bertolini
+Bertram
+Bertrand
+Berube
+Beshai
+Besharah
+Beshir
+Besnier
+Besse
+Bessell
+Bessette
+Bessey
+Besson
+Bessuille
+Bestavros
+Beswick
+Betcher
+Bethune
+Beton
+Betsill
+Bettadapur
+Betterley
+Betters
+Betts
+Beun
+Beuren
+Beveridge
+Beverly
+Bevington
+Bezanson
+Bezdel
+Beznowski
+Bhandari
+Bharadwa
+Bhardwaj
+Bhasin
+Bhaskar
+Bhatia
+Bhatt
+Bhattacharya
+Bhatti
+Bhoday
+Bhullar
+Biage
+Bialkenius
+Biamonte
+Bianchi
+Biard
+Bible
+Bibr
+Bice
+Bickford
+Bidetti
+Biedermann
+Biel
+Bielan
+Bielby
+Bielecki
+Bielejeski
+Bienek
+Bienia
+Bierbrier
+Bierman
+Biermann
+Biersach
+Bieszczad
+Bigelow
+Biggers
+Biggerstaff
+Biggs
+Bigley
+Bigras
+Bihl
+Bijjani
+Bijman
+Bijons
+Bilanski
+Billard
+Billing
+Billingham
+Billoteau
+Bilodeau
+Bilovus
+Bilsborough
+Bilton
+Binda
+Binder
+Binggeli
+Bingham
+Binner
+Binnington
+Bins
+Bir
+Biray
+Birk
+Birkett
+Birks
+Birkwood
+Birtch
+Bisch
+Biss
+Bissegger
+Bissette
+Bisson
+Bissonnette
+Bittenbender
+Bittman
+Bitton
+Bivens
+Bizga
+Bjorklund
+Bjornson
+Blaauw
+Blackard
+Blackburn
+Blacker
+Blackley
+Blacklock
+Blackman
+Blackshaw
+Blackshire
+Blackwelder
+Blackwell
+Blackwood
+Bladon
+Blaiklock
+Blaine
+Blair
+Blais
+Blake
+Blake-Knox
+Blakemore
+Blakeslee
+Blakey
+Blakkolb
+Blalock
+Blanchard
+Blanche
+Blanchet
+Blanchette
+Blanco-Alonso
+Blander
+Blankenship
+Blann
+Blaschuk
+Blasing
+Blasko
+Blatherwick
+Blatt
+Blauer
+Blaufus
+Blaylock
+Blazejewski
+Blazek
+Blazer
+Bleile
+Blenk
+Blenkarn
+Blesi
+Blethen
+Bleuer
+Blevins
+Blezard
+Blidy
+Blimkie
+Blissett
+Blodgett
+Bloedon
+Bloemker
+Blois
+Blomquist
+Bloodworth
+Blostein
+Blouin
+Blount
+Bluethner
+Blum
+Blumenfeld
+Blumer
+Bluschke
+Bly
+Blyskal
+Blyszczak
+Blythe
+Bmethods
+Bnrecad
+Bnrinfo
+Bnrlsi
+Bnrsport
+Boal
+Boarder
+Boase
+Boatwright
+Bobar
+Bobbitt
+Boccali
+Bockaj
+Bocklage
+Bocservice
+Boddeveld
+Boden
+Bodford
+Bodin
+Bodkin
+Bodnar
+Boeck
+Boecke
+Boehlke
+Boehms
+Boersma
+Boeyen
+Bogal
+Bogart
+Bogert
+Boggan
+Boggia
+Boggild
+Boggs
+Bogumill
+Bohanan
+Bohannon
+Bohn
+Bohner
+Boileau
+Boily
+Boinnard
+Bois
+Boisseau
+Boisset
+Boisvert
+Boivin
+Bojeck
+Bokanovich
+Bokij
+Bokish
+Boland
+Bolding
+Bolduc
+Boleda
+Boles
+Bolgos
+Bolio
+Bolli
+Bolly
+Bolon
+Bolouri
+Bolsinger
+Bolton
+Bolzon
+Bomba
+Bombardier
+Bommakanti
+Bommer
+Bomstein
+Bonahoom
+Bondurant
+Bonfanti
+Bongers
+Bonnar
+Bonneau
+Bonnefoy
+Bonner
+Bonneville
+Bonney
+Bonnin
+Bono
+Boocock
+Booker
+Booking
+Bookings
+Boone
+Boorne
+Boorse
+Boos
+Boose
+Boothroyd
+Bopp
+Boppana
+Boraie
+Boray
+Borcic
+Bordage
+Boreham
+Borek
+Borel
+Borg
+Borgia
+Borha
+Borkowicz
+Borman
+Borodajluk
+Borojevic
+Borosch
+Borosh
+Boroski
+Boroughs
+Borowiecki
+Borozny
+Borrelli
+Borsa
+Borsato
+Borson
+Bortenstein
+Borthwick
+Bortolussi
+Borum
+Boruslawski
+Borza
+Borzic
+Bosch
+Boschin
+Boscio
+Bosco
+Bose
+Bosiljevac
+Bosnyak
+Bossa
+Bossert
+Bossett
+Bossler
+Bostelmann
+Bostock
+Boswell
+Boswick
+Bosworth
+Bosy
+Bot
+Bothwell
+Bott
+Botting
+Bottis
+Botto
+Bottomley
+Bottoms
+Botyrius
+Bouchard
+Boucher
+Boucouris
+Boudin
+Boudreau
+Bouffard
+Bouick
+Boulais
+Boulay
+Boulerice
+Boulos
+Boult
+Bounds
+Bour
+Bourahla
+Bourbonnais
+Bourcier
+Bourdeau
+Bourdignon
+Bourdin
+Bouret
+Bourgaize
+Bourgault
+Bourget
+Bourgon
+Bourguignon
+Bourk
+Bourland
+Bourlet
+Bourne
+Bouroncle
+Bourque
+Bourret
+Bousfield
+Boutilier
+Boutin
+Boutnikoff
+Boutot
+Bovat
+Bovee
+Bovenizer
+Bovey
+Bowab
+Bowcock
+Bowden
+Bowen
+Bowens
+Bower
+Bowers
+Bowes
+Bowick
+Bowler
+Bowles
+Bowling
+Bowser
+Bowyer
+Boyachek
+Boyajian
+Boyce
+Boyd
+Boye
+Boyea
+Boyer
+Boyes
+Boylan
+Boyle
+Boynton
+Bozeman
+Bozicevich
+Braaksma
+Brabant
+Brabec
+Bracewell
+Brackin
+Bracy
+Bradbury
+Bradd
+Braddock
+Braddy
+Bradee
+Bradford
+Brading
+Bradley
+Bradlow
+Bradshaw
+Brady
+Bradyhouse
+Bragado
+Braganza
+Bragg
+Braginetz
+Braham
+Brailey
+Brait
+Brambley
+Bramlett
+Branchaud
+Brandel
+Brander
+Brandon
+Brandsen
+Brandstadt
+Brandt
+Brandvold
+Branham
+Brann
+Brannan
+Brannen
+Brannick
+Brannon
+Brans
+Branscombe
+Brantley
+Brar
+Brashear
+Brasington
+Brassard
+Brasselle
+Brassem
+Brasset
+Brasunas
+Brathwaite
+Bratten
+Brauer
+Brault
+Braum
+Braun
+Braunstien
+Braverman
+Brawley
+Brazeau
+Brearley
+Breault
+Bredeck
+Bredfeldt
+Breedlove
+Breglec
+Brehm
+Breisch
+Breiten
+Bremner
+Brennan
+Brennand
+Brennen
+Brent
+Breon
+Brese
+Bresee
+Breslin
+Bresnahan
+Bresnan
+Bress
+Breton
+Brett
+Breuer
+Brevard
+Brewer
+Brewster
+Brewton
+Briante
+Briard
+Brichetto
+Brickey
+Brickman
+Briden
+Bridenstine
+Bridgeford
+Bridges
+Bridgman
+Brieda
+Briel
+Brien
+Briere
+Brierley
+Briggs
+Brightwell
+Brind'Amour
+Brindley
+Briner
+Bringhurst
+Brinklow
+Brinkman
+Brintnell
+Brisby
+Brisebois
+Brissette
+Brisson
+Britman
+Britt
+Brittain
+Britto
+Britton
+Brivet
+Brivins
+Brkich
+Broadfoot
+Broadhead
+Broadwell
+Broberg
+Broca
+Brocato
+Brock
+Brockhouse
+Brocklebank
+Brockmann
+Brockmeyer
+Brockschmidt
+Brodersen
+Brodfuehrer
+Brodgen
+Brodie
+Brodman
+Brodowski
+Brody
+Brogden
+Brogdon
+Brogley
+Brokaw
+Brombal
+Bromley
+Bronec
+Bronk
+Brooke
+Brooker
+Brookes
+Brookhart
+Brookhouse
+Brooks
+Brooksbank
+Broome
+Brophy
+Broschuk
+Brossard
+Brosselard
+Brosso
+Brost
+Brostrom
+Broten
+Brothers
+Brotherton
+Brough
+Broughton
+Brouillette
+Broulik
+Broussard
+Brousseau
+Brouthillier
+Brouwer
+Brovont
+Brower
+Brown-Gillard
+Browne
+Brownfield
+Browning
+Brownlee
+Brownlie
+Brownridge
+Brox
+Broyles
+Brubaker
+Bruce
+Bruder
+Bruhl
+Bruin
+Bruketa
+Brule
+Brum
+Brummitt
+Brummund
+Brunato
+Bruncati
+Bruneau
+Brunelle
+Bruner
+Bruner-Uebelhoer
+Brunet
+Brungardt
+Brunke
+Brunner
+Brunner-Walker
+Bruno
+Brunoni
+Brunsting
+Brunton
+Brushey
+Bruxvoort
+Bryan
+Bryant
+Brydges
+Brydon
+Bryenton
+Brys
+Bryttan
+Bubak
+Bubel
+Bucci
+Buchan
+Buchanan
+Buchko
+Buckalew
+Buckhoff
+Buckingham
+Buckley
+Bucklin
+Buckman
+Buckner
+Buczek
+Budd
+Buder
+Budhram
+Budihardjo
+Budimirovic
+Buechner
+Buehler
+Buettgen
+Buffam
+Buffett
+Buford
+Bugajska
+Bugajski
+Buggie
+Buhler
+Buhr
+Buhrkuhl
+Bui
+Bujold
+Buker
+Bukowski
+Bukta
+Bulan
+Bulanda
+Bulbrook
+Bulengo
+Buley
+Bulger
+Bulifant
+Bulitka
+Bulka
+Bulkovshteyn
+Bullard
+Bullas
+Bullen
+Bullett
+Bullinger
+Bullion
+Bulman
+Bulmanis
+Bulmer
+Bulz
+Bumgarner
+Bumstead
+Bunce
+Bundschuh
+Bundy
+Bunker
+Bunn
+Bunner
+Bunting
+Buntrock
+Bunzey
+Buratynski
+Burbage
+Burbidge
+Burcew
+Burchat
+Burchby
+Burdett
+Burdette
+Burdick
+Burega
+Burek
+Burge
+Burger
+Burgi
+Burgin
+Burkard
+Burke
+Burkepile
+Burkert
+Burkett
+Burkey
+Burkhardt
+Burleigh
+Burleson
+Burnage
+Burness
+Burnet
+Burnett
+Burnette
+Burney
+Burns
+Burnside
+Burr
+Burrell
+Burrowes
+Burrows
+Burrus
+Burruss
+Burton
+Burwell
+Busby
+Buscaglia
+Buscarino
+Busch
+Busche
+Buschelman
+Bush
+Bushell
+Bushnell
+Bushnik
+Buskard
+Buske
+Buskens
+Busko
+Bussewitz
+Bussey
+Bustillos
+Busuttil
+Butcher
+Butner
+Butta
+Butterfield
+Butters
+Buttrey
+Butts
+Butvich
+Buxton
+Buzzell
+Bvworks
+Bycenko
+Byczko
+Bydeley
+Byer
+Byers
+Byk
+Bykowy
+Bylina
+Byrd
+Byrgesen
+Byrne
+Byrnes
+CHOCS
+COKOL
+CPM
+CSR
+Cabaniss
+Cabral
+Caceres
+Cacha
+Cachero
+Cadd
+Cadeau
+Cadieux
+Cadshare
+Cadtools
+Cadzow
+Cae
+Caffrey
+Caffry
+Caglayan
+Cahill
+Cai
+Caie
+Cain
+Caine
+Caines
+Cairns
+Caison
+Cakarevic
+Calcote
+Calder
+Caldwell
+Caleta
+Calhoun
+Calis
+Calistoga
+Calistro
+Calkins
+Callaghan
+Callahan
+Callanan
+Calleja
+Callender
+Callery
+Callos
+Calloway
+Calmejane
+Calmenson
+Calow
+Caltrider
+Calva
+Calvin
+Camblin
+Cambre
+Camel-Toueg
+Cameron
+Camet
+Camillucci
+Camirand
+Campagna
+Campanella
+Campara
+Campbell
+Campbell-Tapp
+Campeau
+Camplone
+Campos
+Canada
+Canavan
+Cancela
+Candelario
+Canfield
+Cannataro
+Cano
+Cantlie
+Cantrell
+Cantwell
+Canuel
+Capelle
+Capes
+Capindale
+Caple
+Caplinger
+Capobianco
+Capostagno
+Capozzi
+Capps
+Capretta
+Captives
+Caputo
+Carbajal
+Carbonara
+Carbone
+Carboni
+Carbonneau
+Cardella
+Carden
+Cardozo
+Cards
+Careers
+Carella
+Caresani
+Carevic
+Carew
+Carey
+Cargill
+Cargnelli
+Carignan
+Carkner
+Carlberg
+Carldata
+Carle
+Carlebach
+Carli
+Carlin
+Carling
+Carlisle
+Carlock
+Carlos
+Carlsen
+Carlson
+Carlton
+Carlyle
+Carmichael
+Carmody
+Carmona
+Carnegie
+Carnogursky
+Caron
+Carpentier
+Carpool
+Carr
+Carranza
+Carriere
+Carrillo
+Carrmtce
+Carroll
+Carron
+Carruthers
+Carson
+Carsten
+Carstensen
+Carswell
+Carter
+Carty
+Carufel
+Caruk
+Caruso
+Caruth
+Carver
+Casadonte
+Casalou
+Casanova
+Casas
+Cascarini
+Casey
+Cashin
+Caskey
+Casler
+Casnji
+Casper
+Casperson
+Cassady
+Cassar
+Cassat
+Cassese
+Cassidy
+Casson
+Castaban
+Castell
+Castelloe
+Casten
+Castillo
+Casto
+Castonguay
+Castro
+Castro-Herrera
+Castronova
+Catanach
+Caterina
+Catering
+Catherwood
+Catlett
+Cato
+Caton
+Cau
+Caudill
+Caudle
+Cauthen
+Cauthers
+Cavan
+Cavanagh
+Cavasin
+Cavasso
+Caves
+Cavill
+Caviness
+Cavnar
+Cawley
+Cayer-Fleck
+Cayless
+Cayouette
+Caza
+Cech
+Cecil
+Cegelski
+Celestin
+Cellucci
+Cemensky
+Cencier
+Centeno
+Center
+Centers
+Centis
+Centre
+Ceponis
+Ceranic
+Cerny
+Cervantes
+Cesaratto
+Cesario
+Cescon
+Cetraro
+Chaaban
+Chaar
+Chabrat
+Chacko
+Chacon
+Chaddha
+Chaddock
+Chadha
+Chadwick
+Chafin
+Chafy
+Chagnon
+Chahal
+Chai
+Chai-Seong
+Chaikowsky
+Chaintreuil
+Chaisupakosol
+Chakrabarty
+Chakraborty
+Chakravarti
+Chalifour
+Chalker
+Challice
+Chalmers
+Chamard
+Chamayou
+Chambers
+Chambliss
+Champion-Demers
+Champsi
+Chamsi
+Chan
+Chanchlani
+Chanco
+Chandan
+Chandra
+Chandran
+Chandrashekar
+Chane
+Chang
+Changes
+Channa
+Channell
+Channen
+Chanonat
+Chanpong
+Chantal
+Chao
+Chapa
+Chapdelaine
+Chapen
+Chapin
+Chapleau
+Chaplin
+Chapman
+Chapmond
+Chappell
+Chappuis
+Chaput
+Charbonneau
+Charchanko
+Chardon
+Charest
+Charette
+Chari
+Charko
+Charlebois
+Charlino
+Charlinski
+Charlton
+Charney
+Charron
+Charter
+Chartier
+Chartrand
+Chasse
+Chatel
+Chatfield
+Chatha
+Chatterley
+Chatterton
+Chattoe
+Chattos
+Chau
+Chaudhary
+Chaudhry
+Chaudry
+Chauhan
+Chaurasia
+Chaurette
+Chautems
+Chauvin
+Chavers
+Chaves
+Chavez
+Chavis
+Chawla
+Chea
+Cheal
+Cheatham
+Cheba
+Checinski
+Checkland
+Chee
+Cheeseman
+Cheesman
+Cheetham
+Cheevers
+Chen
+Chenard
+Chenault
+Chene
+Cheney
+Cheng
+Chenier
+Chennette
+Chenoweth
+Chepregi
+Cher
+Cherkas
+Chern
+Chernetsky
+Cherng
+Cherrier
+Cheshire
+Chesser
+Chester
+Chesterfield
+Cheuk
+Cheung
+Cheval
+Chevarie
+Chhabria
+Chia
+Chiabaut
+Chiamvimonvat
+Chiang
+Chiarelli
+Chiavaroli
+Chickorie
+Chiem
+Chien
+Chik
+Chilausky
+Childerhose
+Childers
+Childree
+Childress
+Childs
+Chilibeck
+Chilton
+Chima
+Ching
+Chiniwala
+Chinnery
+Chiou
+Chirachanchai
+Chisholm
+Chisolm
+Chitkara
+Chitnis
+Chityal
+Chiu
+Chiverton
+Chiykowski
+Chmara
+Cho
+Choe
+Chohan
+Choi
+Cholet
+Cholette
+Cholewinski
+Chomik
+Chona
+Chong
+Choo
+Choo-Kang
+Chopowick
+Choptovy
+Choquette
+Chorley
+Chotkowski
+Chou
+Choudhury
+Chouhan
+Chouinard
+Chowhan
+Choy
+Choynowska
+Chrisman
+Christensen
+Christian
+Christiansen
+Christianson
+Christie
+Christl
+Christopher
+Christy
+Chronowic
+Chu
+Chuah
+Chubb
+Chugha
+Chui
+Chummun
+Chun
+Chung
+Chungphaisan
+Chunn
+Churas
+Churchill
+Chychrun
+Chytil
+Ciampini
+Cianci
+Ciancibello
+Ciaralli
+Ciaschi
+Ciccarelli
+Cicchino
+Cicci
+Cicek
+Cicero
+Ciesielski
+Cifelli
+Cifersky
+Cigay
+Cimino
+Cimolai
+Cinar
+Cinicolo
+Cinq-Mars
+Ciocca
+Ciochon
+Cioffi
+Ciolfi
+Cipolla
+Circe
+Cirulli
+Cisco
+Ciskowski
+Citarelli
+Cividino
+Cizmar
+Clacher
+Claggett
+Clairmont
+Claise
+Clampitte
+Clancy
+Clapham
+Clapp
+Clark
+Clark-Stewart
+Clarka
+Clarke
+Clarkson
+Clary
+Clason
+Claveau
+Claxton
+Claybrook
+Clayton
+Cleary
+Cleere
+Clegg
+Clemens
+Clements
+Clemmons
+Clendening
+Clenney
+Clerke
+Cleroux
+Clifford
+Clifton
+Clinckett
+Cline
+Clinger
+Clinkard
+Clinton
+Clipperton
+Clipsham
+Clites
+Clocklab
+Cloherty
+Clooney
+Closson
+Clost
+Clough
+Clouthier
+Cloutier
+Clow
+Cluett
+Clusiau
+Clysdale
+Co
+Co-op
+Co-ordinator
+Coady
+Coallier
+Coathup
+Coats
+Cobaugh
+Cobb
+Cobban
+Cobbold
+Coble
+Cobley
+Cobo
+Cobran
+Cocco
+Cochran
+Cochrane
+Cockburn
+Cocke
+Cockins
+Cocos-Archive
+Codack
+Codata
+Coddington
+Codoc
+Codrington
+Cody
+Coe
+Coffey
+Cogan
+Cogdell
+Coggins
+Coghlan
+Cogwell
+Cohea
+Cohen
+Cohn-Sfetcu
+Cohoe
+Cohrs
+Colagrosso
+Colangelo
+Colantonio
+Colbert
+Colbourne
+Colby
+Colclasure
+Coldwell
+Cole
+Coleman
+Coles
+Coley
+Colford
+Colgan
+Collazo
+Collecutt
+Colledge
+Collette
+Collevecchio
+Colley
+Collier
+Collins
+Collis
+Collyer
+Coloads
+Colontonio
+Colpitts
+Colquette
+Colquhoun
+Colquitt
+Colston
+Colter
+Colterman
+Colton
+Colucci
+Colvin
+Colwell
+Combaz
+Combee
+Combellack
+Combos
+Combs
+Comeau
+Comley
+Comm
+Commazzi
+Comments
+Committe
+Commons
+Communication
+Communications
+Comp
+Compton
+Comstock
+Comtois
+Conboy
+Conde
+Condurelis
+Conerly
+Coneybeare
+Congdon
+Conistis
+Conklin
+Conley
+Conlin
+Conlon
+Connell
+Connelly
+Conner
+Conners
+Connolly
+Connor
+Connors
+Connors-Cronin
+Conoly
+Conrad
+Conrath
+Conroy
+Constable
+Constantin
+Constantine
+Constantinescu
+Constantinides
+Constantino
+Constantinof
+Consultancy
+Contardo
+Conte
+Contine
+Contomichalos
+Conway
+Coochey
+Coody
+Coogan
+Cooke
+Cooksey
+Cooley
+Coombs
+Cooney
+Cooper
+Coord
+Coordinator
+Coors
+Copeland
+Copello
+Copeman
+Copes
+Copleston
+Copley
+Copp
+Coppedge
+Coppins
+Coqueugniot
+Corace
+Corbeil
+Corbett
+Corbin
+Corbitt
+Corcoran
+Cordell
+Cordy
+Corey
+Coriaty
+Corker
+Corkey
+Corkum
+Corless
+Corlett
+Corley
+Cormier
+Cornaro
+Cornell
+Corner
+Corpening
+Corr
+Correa
+Correia
+Corrigan
+Corritore
+Corriveau
+Corsale
+Corse
+Corson
+Corvo
+Cosburn
+Cosentino
+Cosgrove
+Cosner
+Cossota
+Costa
+Costache
+Costadimas
+Costandi
+Costantino
+Costanzino
+Costas
+Coste
+Costello
+Costelloe
+Costen
+Cote
+Cothran
+Cotnam
+Cotner
+Cotten
+Cottengim
+Cotter
+Cottingham
+Cottrell
+Cotugno
+Cotuna
+Coucopoulos
+Couey
+Coughran
+Coules
+Coulman
+Coulombe
+Coulson
+Coulter
+Coulterman
+Coupal
+Coupland
+Courchesne
+Couron
+Coursey
+Coursol
+Courson
+Courtadm
+Courtney
+Courville
+Couse
+Couser
+Cousineau
+Cousins
+Coutellier
+Couto
+Coutu
+Couture
+Covach
+Coverdale
+Covey
+Coviensky
+Coville
+Covington
+Cowan
+Cowart
+Cowell
+Cowen
+Cowick
+Cowley
+Cowling
+Cownie
+Cowper
+Coxall
+Coxe
+Coyle
+Coyne
+Cozart
+Cozyn
+Cozzi
+Crabb
+Crabe
+Crabtree
+Cracknell
+Craddock
+Crafton
+Craggs
+Craghead
+Craib
+Craig
+Craig-Dupuis
+Crain
+Cramer
+Cramm
+Crampton
+Cranston
+Crapco
+Crase
+Craver
+Crawford
+Crawhall
+Crawley
+Crawshaw
+Craycraft
+Cre
+Creamer
+Crean
+Creane
+Creasey
+Creasman
+Credico
+Credille
+Creech
+Creecy
+Cregan
+Creighton
+Cremer
+Crepeau
+Crerar
+Creswell
+Crews
+Cribbs
+Crick
+Crickard
+Cricker
+Crigger
+Crippen
+Cripps
+Crisler
+Cristescu
+Criswell
+Critchley
+Crocker
+Crockett
+Crogie
+Croisetiere
+Crolla
+Crompton
+Cromwell
+Cronan
+Cronin
+Cronk
+Cronkright
+Cronkwright
+Crooks
+Croom
+Cropper
+Crosby
+Crossley
+Crosson
+Crosswell
+Croteau
+Crothers
+Crotty
+Crowder
+Crowe
+Crowell
+Crowle
+Crowley
+Croxall
+Croxford
+Crozier
+Crucefix
+Cruey
+Cruickshank
+Crumpton
+Crutchfield
+Cruz
+Cruzado
+Csaszar
+Csenar
+Csop
+Csreport
+Ctas
+Cuany
+Cucchiaro
+Cucci
+Cuccioletta
+Cucuzzella
+Cuddihey
+Cuddihy
+Cuddy
+Cuellar
+Cuervo
+Cuffle
+Cuffling
+Cuggy
+Culberson
+Culbertson
+Culbreth
+Culham
+Culkin
+Cullen
+Culley
+Cullipher
+Cullum
+Culmer
+Culp
+Culver
+Culverhouse
+Cummine
+Cumming
+Cummings
+Cummins
+Cumpston
+Cung
+Cunha-Gomes
+Cunningham
+Cuper
+Cupido
+Curley
+Curmon
+Curnow
+Curran
+Currer
+Currie
+Currier
+Currin
+Curtin
+Curtis
+Cusato
+Cushing
+Cusick
+Cusson
+Custsupport
+Cusumano
+Cuthbert
+Cuthill
+Cutrufello
+Cuu
+Cwirzen
+Cyr
+Cytrynbaum
+Czappa
+Czarnecki
+Czeban
+Czychun
+D'Ambrosio
+D'Amico
+D'Amour
+D'Andrea
+D'Angelo
+D'Anjou
+D'Antonio
+D'Aoust
+D'Cruz
+D'Ingianni
+D'Ippolito
+D'Onofrio
+D'Orazio
+D'Silva
+D'Soto
+D'Souza
+D'lima
+DALE
+DDOCDB
+DMS
+DMSDB
+DPNBUILD
+DPP
+Da
+Da Gama
+Da Silva
+DaGama
+Daaboul
+Dacal
+Dace
+Dach
+Dachelet
+Dack
+Dacre
+Dada
+Dadalt
+Dadgar
+Dadkhah
+Dafoe
+Dagenais
+Dagert
+Dages
+Dagg
+Dagley
+Dagnall
+Dagoulis
+Dahan
+Dahl
+Dai
+Daigle
+Daigneault
+Dailey
+Daimee
+Dajerling
+Dal
+Daley
+Dall'oste
+Dallago
+Dallaire
+Dallal
+Dallas
+Dalmard
+Dalrymple
+Dalsiel
+Dalton
+Daly
+Dambenieks
+Damena
+Damerji
+Damiano
+Dana
+Danagher
+Danai
+Danbrook
+Dancy
+Dando
+Dandurand
+Dane
+Daneshzadeh
+Danforth
+Dangubic
+Daniel
+Danielak
+Daniells
+Daniels
+Danilowicz
+Daniluk
+Danjean
+Danker
+Danko
+Dann
+Dans
+Dansereau
+Dantu
+Dantzler
+Danzeisen
+Dao
+Daoud
+Daoust
+Daquano
+Darby
+Darcel
+Darcie
+Darcy
+Darden
+Dares
+Darlington
+Darnel
+Darnell
+Darou
+Darr
+Darrimon
+Darroch
+Darshi
+Darveau
+Das
+Dasch
+Dasd
+Dasilva
+Dasrath
+Dassani
+Dassie
+DataSupport
+Datacenter
+Datema
+Datta
+Dattalo
+Daudin
+Daugavietis
+Daugherty
+Daughtrey
+Daunais
+Dauphinais
+Dautenhahn
+Dauterive
+Davalo
+Davey
+David
+David-Yerumo
+Davidovich
+Davids
+Davidson
+Davies
+Davis
+Davison
+Davy
+Dawe
+Dawkins
+Dawson
+Daya
+Dayal
+Dayberry
+Daymond
+Db
+Dba
+Dbase
+Dbs
+De
+De Anda
+De Baets
+De Beaumont
+De Boer
+De Cecco
+De Coursey
+De Cristofaro
+De La
+De Leon
+De Los
+De Marco
+De Martino
+De Muinck
+De Souza
+De Toni
+De Varennes
+De Vito
+De Vries
+De Wiele
+De-Boer
+DeAcetis
+DeAlmeida
+DeBernardo
+DeBlois
+DeBrun
+DeBrusk
+DeCacqueray
+DeCiccio
+DeCoursin
+DeFacendis
+DeFalco
+DeFazio
+DeFord
+DeFrancesco
+DeGrandis
+DeHaan
+DeLargy
+DeLat
+DeLorenzo
+DeMarco
+DeNest
+DeNoon
+DePalma
+DeRaaf
+DeSalis
+DeSimone
+DeStefani
+DeWitte
+DeYoung
+DeZoete
+DeKoning
+Deagle
+Deak
+Deakin
+Dealto
+Deanda
+Deane
+Deans
+Dearaujo
+Deardurff
+Deason
+Deatherage
+Deatrick
+Debassige
+Debnam
+Deboer
+Debord
+Debortoli
+Decaire
+Decapua
+Decarie
+Decasper
+Decelles
+Decker
+Decleir
+Decourcy
+Deczky
+Dedas
+Deduk
+Dee
+Deek
+Deere
+Deery
+Deevey
+Defilippis
+Deforeit
+Defranchi
+Degan
+Degen
+Degenova
+Degraauw
+Deguines
+Deguire
+Dehghan
+Dehner
+Dehoff
+Deibert
+Deicher
+Deininger
+Deitiker
+Dejongh
+Dekeyser
+Del
+DelVecchio
+Delaat
+Delage
+Delahay
+Delaney
+Delangis
+Delbridge
+Delbrouck
+Deleon
+Delf
+Delgrosse
+Deligdisch
+Delisle
+Deliva
+Della
+Delle
+Delli
+Dellinger
+Delmar
+Delolmodiez
+Delong
+Delorenzi
+Delorme
+Delroy
+Deluca
+Deluce
+Deluco
+Delzer
+Demarest
+Demchuk
+Dement
+Demeo
+Demers
+Demetrick
+Demeulemeester
+Demidenko
+Demir
+Demjen
+Demone
+Demorest
+Demps
+Dempsey
+Dempster
+Demren
+Dendi
+Denebeim
+Deneen
+Denemark
+Denette
+Deng
+Denike
+Denis
+Denison
+Denley
+Denman
+Denmark
+Dennen
+Denney
+Denning
+Dennis
+Denno
+Denomme
+Denter
+Denton
+Denver
+Deol
+Depelteau
+Depew
+Dephoure
+Deployment
+Depooter
+Dept
+Derbyshire
+Derganc
+Dermardiros
+Derome
+Derosa
+Derrett
+Derry
+Dery
+DesMarais
+Desai
+Desantis
+Desautels
+Desch
+Deschamps
+Deschiffart
+Descoteaux
+Descotes
+Desgroseilliers
+Desharnais
+Desilets
+Desjardins
+Desjarlais
+Deslandes
+Deslaurier
+Deslauriers
+Desmond
+Desorbay
+Desourdy
+Despault
+Despinic
+Desplanque
+Despres
+Desrochers
+Desroches
+Desrosiers
+Dessain
+Destech
+Deugau
+Deugo
+Deutschmann
+Devarennes
+Devault
+Deveau
+Development
+Devenny
+Devenyi
+Devenyns
+Devera
+Devgon
+Devincenzi
+Devine
+Devlin
+Devore
+Devouges
+Devreeze
+Dewart
+Dewit
+Dewitt
+Deyirmendjian
+Dhaliwal
+Dhar
+Dhaussy
+Dhillon
+Dhir
+Dhuga
+Dhupar
+Di
+Di Cosola
+Di Giambattista
+Di Maso
+Di Millo
+Di Ninno
+DiFalco
+DiLoreto
+DiPasquale
+DiPerna
+DiPietro
+DiRienzo
+DiSisto
+DiTecco
+Diaconu
+Dias
+Diaz
+Dibenedetto
+Dibler
+Dicaprio
+Dickard
+Dickerson
+Dickeson
+Dickford
+Dickie
+Dickinson
+Dicks
+Dickson
+Didio-Duggan
+Dido
+Diduch
+Didylowski
+Diec
+Diederichs
+Diedrich
+Diee
+Diekman
+Diemel
+Diener
+Diep
+Dieplinger
+Diersch
+Diesing
+Dieter
+Dietrich
+Dieu
+Diffee
+Diffie
+Difilippo
+Difrancesco
+Digenova
+Digiacomo
+Digilio
+Dignam
+Dikaitis
+Dikens
+Dilallo
+Dilen
+Dilkie
+Dillabough
+Dillard
+Dilley
+Dillingham
+Dillon
+Dillow
+Dimarzo
+Dimillo
+Dimitry
+Dinalic
+Dinges
+Dingle
+Dingley
+Dingman
+Dinkel
+Dinnerville
+Dinnin
+Dinsmore
+Diogo
+Dion
+Dionne
+Dipace
+Dipierro
+Dipper
+Diradmin
+Dirbm
+Diretto
+Dirilten
+Disalvo
+Discenza
+Discover
+Disher
+Dishong
+Dispatch
+Dissinger
+Ditko
+Dittburner
+Dix
+Dixon
+Dmsrtime
+Dmuchalsky
+Doak
+Doan
+Dobbing
+Dobbins
+Dobbs
+Dobby
+Doble
+Dobransky
+Docherty
+Dockendorff
+Documentation-Grp
+Doczy
+Doda
+Dodd
+Dodds
+Dodgson
+Dodier
+Dodman
+Dodson
+Doerfel
+Doerksen
+Doerr
+Doggett
+Dohan
+Doherty
+Doi
+Doig
+Dokken
+Dokuzoguz
+Dolan
+Dolezal
+Dolginoff
+Dolgov
+Doliska
+Dolson
+Domanico
+Domas
+Domine
+Dominguez
+Dommety
+Donaghue
+Donahee
+Donahue
+Donak
+Donald
+Donaldson
+Donator
+Doncaster
+Doncell
+Donegan
+Donelan
+Dong
+Donkers
+Donleycott
+Donlon
+Donnelly
+Donner
+Donoghue
+Donohoe
+Donohue
+Donovan
+Doodeman
+Dooley
+Doolin
+Doolittle
+Dorais
+Doran
+Doray
+Dordari
+Dore
+Dorey
+Dorion
+Dorion-Magnan
+Doriot
+Doris-Hampton
+Dormer
+Dorn
+Dornback
+Dorotich
+Dorr
+Dorrell
+Dorronsoro
+Dorsey
+Dorval
+Dosanjh
+Dosenbach
+Doshi
+Doskas
+Doss
+Dost
+Dotsey
+Dotson
+Dottin
+Doublesin
+Doucet
+Doucette
+Doud
+Douet
+Dougall
+Dougherty
+Doughty
+Douglas
+Douglass
+Dourley
+Douville
+Dovel
+Dover
+Dovydaitis
+Dow
+Dowd
+Dowding
+Dowdy
+Dowell
+Dower
+Dowker
+Dowling
+Downer
+Downes
+Downey
+Downing
+Downs
+Dowse
+Dowser
+Doyle
+Doyon
+Dpierre
+Dpnqa
+Dpu
+Drabek
+Dracula
+Draffin
+Dragan
+Dragert
+Dragnea
+Drago
+Draier
+Drakage
+Drane
+Dransfield
+Draper
+Drappel
+Draves
+Dray
+Drayton
+Dreisbach
+Drenan
+Drennan
+Drescher
+Dresser
+Dressler
+Drewes
+Driedger
+Drinnan
+Driscoll
+Driver
+Drobnik
+Drolet
+Drop-Box
+Dropin
+Droste
+Drouin
+Drubld
+Drumheller
+Drummer
+Drummond
+Druzeta
+Drwiega
+Dryer
+Drynan
+Du Berger
+DuBois
+DuPaul
+DuPuis
+Dube
+Dubeau
+Dubee
+Dubose
+Dubreck
+Dubreuil
+Dubroff
+Dubroy
+Dubuc
+Duchaine
+Ducharme
+Duchesne
+Ducic
+Duda
+Dudas
+Dudgeon
+Dudley
+Dueck
+Duensing
+Dueppen
+Duffin
+Duffney
+Dufford
+Duffy
+Dufloth
+Dufour
+Dufresne
+Dugal
+Dugar
+Dugas
+Duggan
+Duguay
+Dukes
+Dula
+Dulaney
+Dulin
+Dulmage
+Dulude
+Dumais
+Dumas
+Dummer
+Dumont
+Dumouchel
+Dumouchelle
+Dunajski
+Dunbar
+Duncan
+Duncan-Smith
+Dundin
+Dunfield
+Dungan
+Dunham
+Dunik
+Dunkelman
+Dunlap
+Dunlay
+Dunlop
+Dunmore
+Dunn
+Dunne
+Dunnett
+Dunning
+Dunningham
+Dunnion
+Dunphy
+Dunsmore
+Dunson
+Dunstan
+Duong
+Duplacey
+Duplan
+Dupont
+Dupras
+Dupre
+Dupree
+Dupuis-Mignault
+Dupuy
+Duquette
+Duran
+Durant
+Durham
+Durie
+Durling
+Durnford
+Durose
+Durousseau
+Durovic
+Dursse
+Dusomos
+Duthie
+Dutil
+Dutt
+Duvarci
+Duxbury
+Duyck
+Dwyer
+Dyba
+Dybenko
+Dyck
+Dyess
+Dyke
+Dyment
+Dyna
+Dynie
+Dyrdahl
+Dysart
+Dyson
+Dziamba
+Dziawa
+Dziemian
+Dzioba
+ENG
+ETAS
+Eagles
+Eakes
+Eakins
+Eales
+Eansor
+Earles
+Earley
+Early
+Earnhardt
+Earps
+Easaw
+Eason
+Easson
+Easterling
+Eastick
+Eastland
+Eastman
+Easton
+Eastus
+Eaton
+Eaves
+Ebara
+Ebata
+Ebbinghaus
+Eberle
+Eberlin
+Ebert
+Ebrahim
+Eby
+Echols
+Ecker
+Eckert
+Eckhart
+Ecklund
+Eckstein
+Ecocafe
+Ecroyd
+Eddins
+Eddisford
+Edelman
+Eder
+Edey
+Edgar
+Edgette
+Edgreen
+Edkins
+Edmison
+Edmonds
+Edmondson
+Edmunds
+Edmxtest
+Edwards
+Edwige
+Edwin
+Efland
+Efstration
+Efthim
+Egan
+Egdorf
+Egerman
+Eggebraaten
+Eggersgluess
+Eggleton
+Egli
+Egner
+Ehlers
+Ehninger-Cuervo
+Ehrenfried
+Ehrenholz
+Ehrlich
+Eicher
+Eide
+Eierstock
+Eike
+Eimer
+Einarsson
+Einersen
+Eisele
+Eisen
+Eisenach
+Eisenhart
+Eisler
+Eisnor
+Eiswirth
+Eitner
+Ekiert
+El-Am
+El-Guebaly
+El-Hawary
+El-Torky
+Elam
+Elbeze
+Elbi
+Elchakieh
+Eldreth
+Eldridge
+Electronics
+Eleftheriou
+Eley
+Elgar
+Elgie
+Elgin
+Elhage
+Elhamahmy
+Elias
+Elie
+Elkaim
+Elkington
+Elkins
+Elks
+Ellacott
+Elledge
+Ellement
+Ellens
+Eller
+Ellerman
+Ellington
+Elliot
+Elliott
+Ellis
+Ellison
+Ellul
+Elms
+Elsing
+Elson
+Elting
+Elwood
+Ely
+Elzer
+Emami
+Emdin-Sproule
+Emerick
+Emerson
+Emery
+Emesh
+Emhart
+Emig
+Emili
+Emmell
+Emmerstorfer
+Emmert
+Emmons
+Emmott
+Emond
+Emory
+EmployeeClub
+Emrick
+Emro
+Encomenderos
+Endenburg
+Enderle
+Enders
+Endrys
+Endsley
+Enet
+Engel
+Engelberg
+Engelbrecht
+Engelhart
+Engineering
+England
+Engle
+Englebrick
+Engleman
+Englert
+English
+Engman
+Ennis
+Enns
+Enos
+Ensign
+Ensing
+Enstone
+Entwistle
+Environment
+Eow
+Eperjesy
+Eppenstiner
+Epperson
+Eppich
+Epplett
+Epps
+Epstein
+Epting
+Erbach
+Erbilgin
+Erfani
+Ergle
+Erguven
+Erichsen
+Erickson
+Eriksson
+Erkel
+Erler
+Erling
+Ermarkaryan
+Ermey
+Ernst
+Eroler
+Eros
+Ersil
+Erskine
+Ertan
+Ertl
+Ervi
+Erwin
+Esc
+Esch
+Escher
+Escobedo
+Escobido
+Escutin
+Esgate
+Esguerra
+Eshelman
+Eskew
+Eskicioglu
+Eskildsen
+Eslambolchi
+Esler
+Esliger
+Esmaili
+Esparza
+Espinosa
+Esposito
+Esry
+Esselbach
+Esser
+Essery
+Essig
+Esson
+Estabrooks
+Este
+Esteghamat
+Estep
+Estes
+Estey
+Estridge
+Eswara
+Etchieson
+Etemad
+Eteminan
+Ethier
+Ethington
+Etoh
+Etten
+Ettridge
+Ettson
+Etu
+Etzell
+Eu
+Eubanks
+Euler
+Eustace
+Eustis
+Eva
+Evans
+Eveleigh
+Evely
+Evenson
+Events
+Everett
+Everitt
+Evers
+Evraire
+Ewanchyna
+Ewart
+Ewasyshyn
+Ewing
+Exner
+Eyers
+Ezekiel
+Ezzat
+FASTONE
+FISOPN
+FWPtools
+Fab
+Fabijanic
+Fabris
+Fabrizio
+Fabry
+Fadel
+Fadlallah
+Fafara
+Fagan
+Fagg
+Fahey
+Fahrenthold
+Fahy
+Fainaru
+Fainecos
+Fairclough
+Fairless
+Fairlie
+Fairman
+Faison
+Fait
+Fajardo
+Falaki
+Falardeau
+Falbee
+Falcao
+Falconer
+Faletti
+Faley
+Falke
+Falkenstrom
+Falkner
+Fallah
+Fallahi
+Falletti
+Fallis
+Fallows
+Falquero
+Falt
+Familiadis
+Fanchi
+Fangio
+Fani
+Fann
+Fansher
+Fantauzzi
+Fanthome
+Fanus
+Faou
+Farag
+Farago
+Farah
+Farahvash
+Farant
+Fares
+Fargis
+Farhan
+Farias
+Farley
+Farmer
+Farnham
+Farnsworth
+Farnum
+Farquhar
+Farr
+Farranto
+Farrell
+Farren
+Farrington
+Farronato
+Farrow
+Faruque
+Fasken
+Fastfeat
+Fastpack
+Fater
+Fatica
+Fattouh
+Faubert
+Faucher
+Faulkner
+Faust
+Favell
+Favreau
+Favrot
+Fawcett
+Fazel
+Feddeman
+Feddersen
+Feder
+Federico
+Fedoruk
+Fedyk
+Feeley
+Feeney
+Fehr
+Feil
+Feild
+Feist
+Feith
+Felczak
+Feldberg
+Felder
+Feldman
+Felfli
+Felicetti
+Felix
+Felli
+Felske
+Feltman
+Felton
+Fenati
+Fender
+Fenez
+Fenn
+Fennell
+Fenner
+Fennessey
+Fenton
+Fenwick
+Fequiere
+Feregyhazy
+Ference
+Ferenz
+Fererro
+Ferguson
+Fergusson
+Ferland
+Fermoyle
+Fernald
+Fernandes
+Fernandez
+Fernando
+Ferner
+Ferrao
+Ferrara
+Ferraro
+Ferree
+Ferreira
+Ferrell
+Ferrer
+Ferrero
+Ferriera
+Ferriss
+Ferro
+Ferruzzi
+Fetterman
+Fetting
+Fetzko
+Feutlinske
+Fevre-Renault
+Feyen
+Ficco
+Ficici
+Ficken
+Ficker
+Fickes
+Fiegel
+Fielden
+Fielding
+Fields
+Fieldsup
+Fierthaler
+Fiest
+Fifield
+Figura
+Fikis
+Fildey
+Filer
+Filion
+Filippi
+Filkins
+Fillmore
+Filpus
+Filson
+Finak
+Finane
+Finckler
+Findlay
+Finkhelstein
+Finlayson
+Finley
+Finn
+Finnerty
+Finney
+Finnie
+Finnighan
+Finnon
+Finucane
+Finzel
+Fiore
+Fiorile
+Firat
+Firerobin
+Firment
+Firtos
+Fischer
+Fischetti
+Fiset
+Fisette
+Fishencord
+Fisher
+Fishman
+Fisico
+Fisprod
+Fiszman
+Fitch
+Fiteny
+Fitz
+Fitzgerald
+Fitzgibbons
+Fitzpatrick
+Fitzsimmons
+Fixsen
+Flach
+Flaherty
+Flanagan
+Flanders
+Flann
+Flansburg
+Flatley
+Fleischer
+Fleishman
+Fleming
+Fleskes
+Fletcher
+Fleuchaus
+Fleugel
+Fleurima
+Fleury
+Flewelling
+Flickinger
+Flindall
+Flintall
+Floch
+Flook
+Flookes
+Florence
+Flores
+Florescu
+Florez
+Florjancic
+Flowers
+Floyd
+Floysvik
+Fludgate
+Flueckinger
+Fluet
+Fluney
+Flury
+Fluty
+Flynn
+Fobert
+Focht
+Focsaneanu
+Fodell
+Foeppel
+Foessl
+Foest
+Fogelson
+Foght
+Fogle
+Fogleman
+Fok
+Foldes
+Foley
+Follett
+Follmer
+Folwell
+Fon
+Fondacaro
+Fong
+Fontaine
+Fontana
+Fontanilla
+Fontanini
+Foods
+Foong
+Foos
+Forbes
+Forbrich
+Forbs
+Forden
+Forecasting
+Foreman
+Forese
+Forester
+Forgeron
+Forghani
+Forgues
+Forland
+Formagie
+Forno
+Forrest
+Forrester
+Forslund
+Forster
+Forsythe
+Fortier
+Fortman
+Fortner
+Foss
+Fothergill
+Foubert
+Foucault
+Fouchard
+Fougere
+Fouillard
+Fouke
+Fouletier
+Foulkes
+Fouret
+Fournel
+Fourney
+Fournier
+Fouts
+Fowler
+Fowler-Hornbuckle
+Fowles
+Fowlkes
+Fowlston
+Foxworthy
+Fradette
+Fragnito
+Fraley
+Fralick
+Fralix
+Frampton
+Fran
+France
+Francese
+Francis
+Francispillai
+Franco
+Francoeur
+Francois
+Frangoulis
+Frankcom
+Frankenberger
+Frankos
+Franks
+Frans
+Frantz
+Franze
+Franzky
+Franzwa
+Frape
+Fraser
+Fraties
+Frazier
+Fredenburgh
+Frederick
+Frederico
+Fredette
+Fredine
+Fredrickson
+Fredriksen
+Freeburn
+Freeland
+Freeley
+Freeth
+Freimark
+Freire
+Freiwald
+Freixe
+French
+Frendo
+Frenette
+Freno
+Fretz
+Frey
+Freyermuth
+Freyler
+Fricker
+Fricks
+Fridel
+Frie
+Friedberg
+Frieder
+Friedl
+Friedrich
+Frierson
+Friesen
+Frink
+Frischknecht
+Frisk
+Friton
+Fritz
+Frizado
+Frobel
+Froberg
+Frobisher
+Frodsham
+Froehlich
+Froncek
+Frondozo
+Fronsee
+Fross
+Frosst
+Froud
+Frucci
+Frumerie
+Fryar
+Frydach
+Fryer
+Fssup
+Fu
+Fuchs
+Fucito
+Fujii
+Fujimoto
+Fujiwara
+Fukui
+Fulford
+Fulk
+Fullager
+Fuller
+Fullmer
+Fullum
+Fulmer
+Fumerton
+Funamoto
+Funderburg
+Fung
+Funston
+Fuqua
+Furdoonji
+Furgerson
+Furlin
+Furlow
+Furmaniak
+Furst
+Fusca
+Fuson
+Fussell
+Fwpco
+Fyfe
+Fysh
+Gabbai
+Gabbard
+Gabe
+Gaboury
+Gabriel
+Gach
+Gaconnier
+Gadbois
+Gadher
+Gadouchis
+Gadsby
+Gadzinowski
+Gaebel
+Gaffney
+Gaftea
+Gagan
+Gagne
+Gagnier
+Gagnon
+Gahan
+Gahir
+Gahlot
+Gahr
+Gahunia
+Gaiarsa
+Gaiger
+Gaime
+Gainer
+Gaines
+Gaiotti
+Gaiser
+Gaitan
+Gaither
+Gajewski
+Gajowiak
+Galanakis
+Galasso
+Galbraith
+Galdwin
+Galipeau
+Gallagher
+Gallais
+Gallegos
+Gallenbeck
+Galligan
+Gallinger
+Gallion
+Gallman
+Gallo
+Gallops
+Gallouzi
+Galloway
+Galluzzi
+Galt
+Galvin
+Gamarnik
+Gammage
+Gamsa
+Gan
+Ganadry
+Gandhi
+Gane
+Gangotra
+Ganguly
+Gann
+Ganness
+Gannon
+Gans
+Gantt
+Gara
+Garamvolgyi
+Garand
+Garay
+Garbis
+Garbish
+Garcia
+Garcia-Lamarca
+Garcia-Molina
+Gard
+Gardiner
+Gardner
+Garee
+Gareis
+Garey
+Garfield
+Garg
+Garguilo
+Gargul
+Gargulak
+Garinger
+Garito
+Garmon
+Garneau
+Garrett
+Garry
+Gartley
+Gartshore
+Garvey
+Garvin
+Garwood
+Gary
+Gascho
+Gascon
+Gasikowski
+Gaskins
+Gaspard
+Gasparotto
+Gass
+Gast
+Gaston
+Gasul
+Gateau
+Gateley
+Gater
+Gates
+Gatka
+Gattrell
+Gaube
+Gaudet
+Gaudet-Montsion
+Gaudon
+Gaudreau
+Gaughan
+Gaul
+Gault
+Gause
+Gauthier
+Gavens
+Gavidia
+Gavillucci
+Gavin
+Gawargy
+Gawdan
+Gawronski
+Gawtrey
+Gaylor
+Gaylord
+Gayman
+Gazier
+Gazo
+Gdowik
+Geadah
+Geary
+Gebhardt
+Gebhart
+Gebrael
+Geddes
+Gedeon
+Gedman
+Geer
+Geesman
+Gehm
+Gehr
+Gehring
+Geiger
+Geisler
+Geldrez
+Gelinas
+Gell
+Gelling
+Gelo
+Gemmill
+Gendre
+Gendron
+Geneau
+Genet
+Genge
+Genova
+Genovise
+Gentes
+Gentzler
+Geoffrion
+George
+Georges
+Georgescu
+Georgiou
+Gera
+Gerald
+Gerard
+Gerbec
+Gerber
+Gerenser
+Gergen
+Gerhart
+Gerlich
+Gerlinsky
+Gerlt
+Germain
+Germano
+Germe
+Gerritse
+Gerstmar
+Gerth
+Gertridge
+Gervais
+Gervaise
+Gerynowicz
+Gesino
+Getchell
+Geuder
+Gewell
+Geyer
+Ghaemi
+Ghaemian
+Ghanem
+Ghangurde
+Ghani
+Ghantous
+Ghartey
+Ghasemian
+Ghatta
+Gheciu
+Ghelarducci
+Gheorghe
+Ghidali
+Ghorashy
+Ghossein
+Ghulati
+Giallo
+Giamatteo
+Giamberardino
+Giang
+Giani
+Giannandrea
+Giarritta
+Gibb
+Gibbins
+Gibbons
+Gibbs
+Gibeault
+Giblin
+Gibson
+Gidaro
+Giekes
+Gierka
+Giertych
+Giesbrecht
+Gieschen
+Giese
+Giguere
+Gilbertson
+Gilchrist
+Giles
+Gillard
+Gille
+Gilleland
+Gilles
+Gillespie
+Gillespy
+Gillet
+Gillette
+Gilliam
+Gilliard
+Gillies
+Gilligan
+Gilliland
+Gillis
+Gillon
+Gillot
+Gillstrom
+Gilmore
+Gilmour
+Gilstorf
+Gimon
+Ginest
+Gingerich
+Gingras
+Gingrich
+Ginzburg
+Gioffre
+Gionet
+Giordano
+Giotis
+Giovinazzo
+Girard
+Girgis
+Giridharagopal
+Girotti
+Girouard
+Giroux
+Girvan
+Gittins
+Giuhat
+Giuliani
+Giuntini
+Glancey
+Glanfield
+Glaros
+Glasa
+Glaser
+Glasgow
+Glasser
+Glast
+Glaszczak
+Glazer
+Gleason
+Gleditsch
+Glemboski
+Glenn
+Glew
+Glidewell
+Glinka
+Glinski
+Glofcheskie
+Glover
+Glowa
+Glucksman
+Glymph
+Gnaedinger
+Goatcher
+Goba
+Gobeil
+Gobeli
+Gockel
+Godard
+Godcharles
+Goddard
+Godden
+Goddette
+Godfrey
+Godin
+Godina
+Godish
+Godley
+Godlington
+Godo
+Godowsky
+Godse
+Godsoe
+Godwin
+Goel
+Goerss
+Goertz
+Goertzen
+Goetz
+Goff
+Gofron
+Goggin
+Goh
+Goheen
+Goin
+Goins
+Golas
+Golaszewski
+Golczewski
+Goldberg
+Goldenberg
+Golder
+Goldman
+Goldmann
+Goldner
+Goldstein
+Goldthorp
+Golia
+Goliss
+Golka
+Goller
+Golshan
+Gombos
+Gomes
+Gomez
+Gomm
+Goniotakis
+Gonsalves
+Gonzales
+Gonzalez
+Gooch
+Good
+Goodbar
+Gooderham
+Goodfellow
+Goodier
+Goodinson
+Goodman
+Goodner
+Goodridge
+Goodrow
+Goodson
+Goodwin
+Goold
+Gooley
+Goos
+Gopaul
+Gopisetty
+Gorasia
+Gorberg
+Gording
+Gordon
+Gores
+Gorfine
+Gorham
+Gorhum
+Goricanec
+Goridkov
+Goring
+Gorius
+Gorlick
+Gorman
+Gorsky
+Gorton
+Gorzocoski
+Goss
+Gosselin
+Gosset
+Gostanian
+Goswick
+Goszczynski
+Gotch
+Gotchall
+Gothard
+Gottlieb
+Gottschalk
+Gottstein
+Goudreau
+Gougeon
+Gough
+Gouhara
+Goukon
+Gould
+Gouldson
+Goulet
+Goulette
+Goulfine
+Goupil
+Gourley
+Govindarajan
+Govindasamy
+Gowan
+Gowda
+Gowens
+Gower
+Gowin
+Gowl
+Gowland
+Goyal
+Goyer
+Goyette
+Goza
+Gozen
+Grabner
+Grabowski
+Gracey
+Grader
+Graessley
+Graff
+Grafton
+Graibe
+Grainger
+Graman
+Gramiak
+Granata
+Grandbois
+Grande
+Grandmason
+Grandy
+Granfield
+Granger
+Granic
+Granner
+Grasman
+Grassmann
+Gratton
+Grau
+Grauer
+Grausso
+Gravelle
+Gravely
+Graver
+Graves
+Gravitt
+Gravitte
+Grawberger
+Graybill
+Graydon
+Grayson
+Graziano
+Grazzini
+Greaver
+Greaves
+Grebil
+Grebner
+Greco
+Greeley
+Greenberg
+Greene
+Greenfield
+Greenlee
+Greenstreet
+Greenway
+Greer
+Gregarick
+Greger
+Gregg
+Grego
+Gregoire
+Gregor-Pearse
+Gregorio
+Gregorski
+Gregory
+Grelck
+Grenier
+Grenon
+Greszczuk
+Greveling
+Grevy
+Grewal
+Greytock
+Gribbon
+Gribbons
+Griffioen
+Griffith
+Griffiths
+Grigg
+Grignon
+Grigsby
+Grills
+Grimble
+Grimes
+Grimm
+Grimmell
+Grimshaw
+Grimsley
+Griner
+Grinham
+Grisoni
+Grissom
+Griswold
+Gritton
+Grixti
+Groce
+Grochau
+Groetsema
+Groff
+Grogan
+Grohovsky
+Groleau
+Grona
+Grondin
+Gronwall
+Grooms
+Grootenboer
+Gros
+Grosjean
+Grosman
+Grosse
+Grossman
+Grossutti
+Groth
+Groulx
+Grover
+Groves
+Grovestine
+Growden
+Gruau
+Grubbs
+Gruber
+Grueneich
+Gruenhagen
+Grund
+Gruska
+Gruszczynski
+Gryder
+Grzegorek
+Grzesik
+Gu
+Guajardo
+Guarino
+Guarnera
+Guatto
+Guay
+Gubbins
+Gubenco
+Gucer
+Guciz
+Gudgeon
+Guenette
+Guercioni
+Guerette
+Guerin
+Guertin
+Guevara
+Guido
+Guignon
+Guilbault
+Guilbert
+Guilford
+Guilfoyle
+Guillaume
+Guillet
+Guilmette
+Guimond
+Guin
+Guindi
+Guindon
+Guinnane
+Guirguis
+Guisler
+Guitard
+Gulbrandsen
+Gulick
+Gulis
+Gulko
+Gullekson
+Gultekin
+Gulvin
+Gumb
+Gumbley
+Gummadi
+Gunasekera
+Gunawan
+Gundecha
+Gundersen
+Gunderson
+Gundlach
+Gundry
+Gung
+Gungor
+Gunkel
+Gunn
+Gunnells
+Gunter
+Gunther
+Gupta
+Gupton
+Gur-Arie
+Gurash
+Gure
+Gurer
+Gurevitch
+Gurgenci
+Gurica
+Gurley
+Gurney
+Gursahaney
+Gursin
+Gustafson
+Gustlin
+Gutcher
+Gutermuth
+Guth
+Guthrie
+Guthro
+Gutierrez
+Guttman
+Guty
+Gutzmann
+Guy-Arbour
+Guyot
+Guzman
+Gyenes
+Gyger
+Gylys
+Gysel
+Gyurcsak
+HDI
+HSI
+Haack
+Haaksman
+Haas
+Habel
+Habelrih
+Habert
+Habib
+Hachadorian
+Hache
+Hachey
+Hacker
+Hackett
+Hadaway
+Haddad
+Hadden
+Haddow
+Hadel
+Hadi
+Hadirahardjo
+Hadley
+Hadziomerovic
+Haerle
+Haertel
+Hafermalz
+Hafiz
+Hafleigh
+Hagan
+Hagar
+Hage
+Hagen
+Hagenbuch
+Hagenbuck
+Hager
+Hagerty
+Hagewood
+Haggart
+Haggarty
+Haggerty
+Hagglund
+Hagley
+Hagstrom
+Hagwood
+Hahn
+Haig
+Haigh
+Hailes
+Hailey
+Hainer
+Haines
+Hainline
+Haire
+Hakansson
+Halbedel
+Halejak
+Haley
+Halford
+Halicki
+Hallenbeck
+Hallett
+Halley
+Halligan
+Halliwill
+Hallman
+Hally
+Halovanic
+Halpenny
+Halpern
+Halpin
+Haluk
+Halula
+Hamavand
+Hambali
+Hameed
+Hamel
+Hamelin
+Hamid
+Hamidi
+Hamilton
+Hamlett
+Hamlin
+Hammel
+Hammerli
+Hammermeister
+Hammond
+Hammonds
+Hamner
+Hamori
+Hamoui
+Hampel
+Hampshire
+Hampson
+Hampton
+Hamra
+Han
+Hancock
+Handforth
+Handley
+Handschy
+Hanel
+Haney
+Hanford
+Hanham
+Hanington
+Hankins
+Hanlan
+Hanley
+Hann
+Hanna
+Hannah
+Hanneman
+Hannula
+Hanrahan
+Hans
+Hansen
+Hanser
+Hanson
+Hansquine
+Hansson
+Hanzel
+Hanzlicek
+Hao
+Harabedian
+Harada
+Harapiak
+Harbord
+Harbottle
+Harbour
+Hardage
+Hardcastle
+Hardee
+Hardersen
+Hardin
+Harding
+Hardison
+Hardman
+Hardwick
+Hardyman
+Haren
+Hargadon
+Hargreaves
+Hargrove
+Hargrow
+Hariman
+Harker
+Harkness
+Harlan
+Harless
+Harley
+Harman
+Harmon
+Harms
+Harold
+Haroutounian
+Harpe
+Harper
+Harrawood
+Harrell
+Harriett
+Harrington
+Harriott
+Harris
+Harrison
+Harrod
+Harron
+Harsch
+Harsham
+Harshfield
+Harte
+Hartell
+Harter
+Hartford
+Hartgrove
+Hartin
+Hartkopf
+Hartland
+Hartleb
+Hartley
+Hartling
+Hartman
+Hartsell
+Harty
+Hartzel
+Harvey
+Harville
+Harvison
+Harwerth
+Harwood
+Hasan
+Hasbrouck
+Hasegawa
+Hasen
+Hasham
+Hashemi
+Hashimoto
+Haskins
+Haslach
+Hassan
+Hassenklover
+Hassey
+Hassold
+Hasted
+Hastic
+Hastie
+Hastings
+Hatcher
+Hatchett
+Hately
+Hatfield
+Hathaway
+Hatridge
+Hattar
+Hatten
+Hattingh
+Hatz
+Hatzenbichler
+Hau
+Haubert
+Hauck
+Hauerstock
+Haufe
+Hauge
+Haughey
+Haughwout
+Haugrud
+Haupt
+Haurie
+Hause
+Hautanen
+Havelock
+Haveman
+Haverkamp
+Haverty
+Havis
+Hawes
+Hawi
+Hawken
+Hawker
+Hawkes
+Hawkins
+Hawley
+Hawryluk
+Hawrysh
+Hawryszko
+Hawthorne
+Hayden
+Haydock
+Haydon
+Hayes
+Haylock
+Hayman
+Haynes
+Hayward
+Hazeldine
+Hazell
+Hazelrig
+Hazelton
+Hazen
+Hazenboom
+Hazlett
+Heald
+Health-Safety
+Healy
+Hearn
+Hearnden
+Heaton
+Hebbar
+Hebert
+Heckman
+Heddell
+Hedin
+Hedke
+Hedman
+Hedrich
+Hedrick
+Heeralall
+Heffernan
+Heffner
+Hehn-Schroeder
+Heidebrecht
+Heidepriem
+Heighton
+Heikkila
+Heile
+Heilig
+Heiliger
+Heilsnis
+Heinen
+Heinjus
+Heinke
+Heinonen
+Heinrichs
+Heinzman
+Heisler
+Heitmann
+Hekel
+Heki
+Heldenbrand
+Helem
+Helfrick
+Helgeland
+Helkaa
+Hellberg
+Heller
+Hellyer
+Helms
+Helmy
+Helpb
+Helpline
+Helseth
+Helstab
+Helton
+Helwege
+Hembrick
+Hemens-Davis
+Hemme
+Hemmerle
+Hemphill
+Hempinstall
+Hempstead
+Henao
+Hench
+Henderson
+Hendren
+Hendricks
+Hendrickse
+Hendricksen
+Hendriks
+Hendrycks
+Henein
+Heng
+Hengeveld
+Hengl
+Hengst
+Henley
+Henline
+Henneberger
+Hennebury
+Hennelly
+Hennessy
+Henninger
+Hennon
+Henriksen
+Hensen
+Henshaw
+Hensley
+Henson
+Henthorne
+Hepburn
+Heppes
+Herak
+Herbel
+Herberger
+Herbers
+Hering
+Herlihy
+Herling
+Hermack
+Herman
+Hermann-Kendall
+Hermanns
+Hermes
+Hernandez
+Herndon
+Herner
+Hernon
+Herod
+Heroux
+Herr
+Herrage
+Herren
+Herrera
+Herrick
+Herring
+Herrington
+Herriotts
+Herrmann
+Herron
+Herscovici
+Herscovitch
+Hersee
+Hershberger
+Hertler
+Hertzog
+Herve
+Herzig
+Hesche
+Hesk
+Hesketh
+Heslop
+Hess
+Hesse
+Hester
+Hetzel
+Heurich
+Hewage
+Hewer
+Hewett
+Hewitt
+Hews
+Heybroek
+Heydon
+Heyer
+Heynen
+Heys
+Heystraeten
+Heyward
+Hibberd
+Hibler
+Hickerson
+Hickey
+Hickin
+Hickman
+Hickman-Miott
+Hickox
+Hicks
+Hiebsch
+Hien
+Hiers
+Higginbotham
+Higgins
+Higham
+Highet
+Highsmith
+Hight
+Hightower
+Higuchi
+Hikita
+Hilaire
+Hilberman
+Hilbig
+Hildebrand
+Hilder
+Hildum
+Hilla
+Hillard
+Hiller
+Hilliard
+Hillidge
+Hillier
+Hillring
+Hills
+Hillson
+Hilmi
+Hils
+Hilton
+Hiltz
+Hilwa
+Hincher
+Hinchey
+Hinchley
+Hindle
+Hinds
+Hindson
+Hine
+Hiner
+Hines
+Hingtgen
+Hink
+Hinkel
+Hinkle
+Hinojosa
+Hinsdale
+Hinshaw
+Hinson
+Hinton
+Hinton-Smith
+Hinz
+Hipp
+Hippert
+Hipson
+Hirakawa
+Hiraki
+Hirayama
+Hirose
+Hirsch
+Hirshman
+Hisaki
+Hiscoe
+Hiscott
+Hishchak
+Hisko
+Hislop
+Hitchcock
+Hitchings
+Hite
+Hiusser
+Hively
+Hixon
+Hixson
+Hjartarson
+Hlady
+Hlauschek
+Hnidek
+Hoadley
+Hoag
+Hoagland
+Hoang
+Hoare
+Hobbs
+Hobesh
+Hobgood
+Hobin
+Hoch
+Hochberger
+Hockaday
+Hoctor
+Hocutt
+Hodder
+Hoddinott
+Hodgens
+Hodges
+Hodgins
+Hodgkin
+Hodgkiss
+Hodgson
+Hoeg
+Hoehling
+Hoehn
+Hoeksma
+Hoekstra
+Hoeler
+Hoelscher
+Hoequist
+Hoes
+Hoferek
+Hoffelt
+Hoffman
+Hoffmann
+Hoffmeister
+Hoffpauir
+Hoffstedder
+Hofmann
+Hofmeister
+Hofstede
+Hofstetter
+Hogeboom
+Hogg
+Hoggan
+Hogue
+Holberry
+Holbrooks
+Holcomb
+Holcombe
+Holcroft
+Holdaway
+Holder
+Holdren
+Holesinger
+Holinski
+Hollack
+Holland
+Hollander
+Hollandsworth
+Hollbach
+Hollen
+Hollenbach
+Hollenbeck
+Holleran
+Holley
+Holliday
+Hollingshead
+Hollingsworth
+Hollington
+Hollingworth
+Hollis
+Hollister
+Holloway
+Hollran
+Holman
+Holmans
+Holmes
+Holmquist
+Holness
+Holsclaw
+Holthaus
+Holton
+Holtz
+Holtze
+Holvey
+Holy
+Holz
+Homa
+Homan
+Homayoon
+Homayoun
+Homonick
+Honbarrier
+Honda
+Honeycutt
+Honkakangas
+Hook
+Hooker
+Hooks
+Hooper
+Hoorman
+Hooton
+Hoover
+Hopcroft
+Hopf
+Hopkins
+Hopkinson
+Hopley
+Hoppenworth
+Hopper
+Hopson
+Hoque
+Hor
+Horak
+Horalek
+Horban
+Hord
+Hore
+Horemans
+Horgan
+Horianopoulos
+Horkoff
+Hornacek
+Hornbeck
+Hornbeek
+Hornburg
+Horne
+Horning
+Hornung
+Horowitz
+Horsfield
+Horstman
+Horton
+Horvath
+Horwitz
+Horwood
+Hosang
+Hoscheid
+Hoshi
+Hosier
+Hoskin
+Hosking
+Hoskins
+Hosneld
+Hosseini
+Hotline
+Hotlist
+Hotson
+Houde
+Hough
+Houghton
+Houk
+Houle
+Hounsell
+Houston
+Hoverman
+Hovey
+Hovinga
+Howald
+Howard
+Howarth
+Howat
+Howe
+Howe-Patterson
+Howell
+Howerton
+Howes
+Howie
+Howlett
+Howley
+Howorth
+Howse
+Hoxie
+Hoyer
+Hoyt
+Hr
+Hrenyk
+Hrubik
+Hrushowy
+Hruska
+Hrvatin
+Hsiang
+Hsieh
+Hsu
+Hu
+Huang
+Hubal
+Hubbard
+Huber
+Huberman
+Hubers
+Hubert
+Hubley
+Huboi
+Hudak
+Huddleston
+Hudecek
+Hudepohl
+Hudgins
+Hudson
+Hudy
+Hudyma
+Huelsman
+Huenemann
+Huestis
+Huether
+Hufana
+Huffman
+Hugel
+Huggins
+Hughes
+Hughes-Cunningham
+Hughey
+Hughson
+Hugo
+Huguin
+Huhn
+Hui
+Huitt
+Hulen
+Hulett
+Huliganga
+Hulme
+Hulvershorn
+Hume
+Humenik
+Humenuk
+Humes
+Humiston
+Hummel
+Hummerston
+Humphrey
+Humphreys
+Humphries
+Hundrieser
+Huneault
+Hungle
+Hunike
+Hunnicutt
+Hunsberger
+Hunsucker
+Hunter
+Huntington
+Huntley
+Huor
+Huot
+Hupe
+Huppert
+Hurd
+Huret
+Hurman
+Hurst
+Hurtado
+Hurteau
+Hurtubise
+Hurwitz
+Husain
+Husarewych
+Hussain
+Hussein
+Hussey
+Huszar
+Huszarik
+Hutcherson
+Hutchings
+Hutchins
+Hutchinson
+Hutchison
+Hutson
+Hutt
+Hutter
+Hutton
+Huynh
+Hvezda
+Hwang
+Hyatt
+Hyde
+Hyer
+Hyers
+Hyjek
+Hylarides
+Hyman
+Hyndman
+Hyrne
+Hysler
+Hyslop
+Hyte
+IBNTAS
+IC
+IEM
+INFO-MANAGEMENT
+INSP
+IOCNTRL
+IRCMARKET
+IRCMTL
+ITAC
+Iacoviello
+Iacovo
+Ianace
+Iannotti
+Iannozzi
+Iantaffi
+Iaquinto
+Iarocci
+Ibach
+Ibarra
+Ibsen
+Iburg
+Ichizen
+Id
+Ide
+Ientile
+Iezzi
+Ifact
+Ifill
+Igarashi
+Igglesden
+Iguchi
+Ihnat
+Ikeda
+Ikotin
+Ilic
+Ilk
+Illidge
+Ilmberger
+Ilowski
+Imbemba
+Imhof
+Imming
+Inamullah
+Ince
+Incze
+Indahl
+Infocenter
+Ing
+Inge
+Ingell
+Ingersoll
+Ingle
+Ingles
+Ingling
+Inglis
+Ingram
+Ingrey
+Inhulsen
+Inman
+Innes
+Inniss
+Inoue
+Inrig
+Inscoe
+Inspection
+Instal
+Integ
+Integration
+Intemann
+Interaction
+Intihar
+Intplan
+Intune
+Ioannou
+Iocca
+Ioui
+Ip
+Ipadmin
+Ippolito
+Irani
+Irc
+IrcInternal-Docs
+Ircbellcore
+Irccar
+Ircmer
+Ircstandards
+Ireland
+Iribarren
+Irick
+Irvin
+Irvine
+Irving
+Irwin
+Isaac
+Isaacs
+Isbister
+Isenor
+Isensee
+Isert
+Isfan
+Ishak
+Ishee
+Isherwood
+Ishii
+Isip
+Iskandar
+Iskra
+Islam
+Isley
+Ismail
+Issa
+Itah
+Itas
+Itaya
+Iteam
+Ito
+Iu
+Ivan
+Ivancevic
+Ivancic
+Ivanoff
+Ivanyi
+Ivers
+Iversen
+Ives
+Ivett
+Ivey
+Iwanyk
+Iyengar
+Iyer
+Izbinsky
+Izique
+Izzo
+Izzotti
+Jaakkola
+Jablonski
+Jachym
+Jackman
+Jackson
+Jacob
+Jacobs
+Jacobsen
+Jacobson
+Jacques
+Jaenen
+Jaffer
+Jagatic
+Jager
+Jagernauth
+Jagla
+Jago
+Jagodzinski
+Jahromi
+Jain
+Jak
+Jakab
+Jakabffy
+Jakim
+Jakola
+Jakstys
+Jakubowski
+Jalaie
+Jalali
+Jalalizadeh
+Jalbert
+Jalilvand
+Jamal
+Jamaly
+Jamensky
+James
+Jamieson
+Jamison
+Jammu
+Jamnejad
+Jamroz
+Jamshidi
+Janaratne
+Jancewicz
+Jancovic
+Janczyn
+Jande
+Janecka
+Janelle
+Janes
+Jang
+Janiszewski
+Janke
+Jankowski
+Janning
+Janovich
+Janseen
+Jansen
+Janssen
+Jantz-Lee
+Jantzi
+Japp
+Jaques
+Jarboe
+Jarchow
+Jarman
+Jarmoc
+Jarmon
+Jarmul
+Jarnak
+Jarvah
+Jarvie
+Jarvis
+Jarzemsky
+Jasen
+Jasmann
+Jasmin
+Jasny
+Jasrotia
+Jasti
+Jaswal
+Jatar
+Jauvin
+Java
+Javallas
+Javallas-Ross
+Javed
+Javor
+Jawanda
+Jawor
+Jaworski
+Jaworsky
+Jayamanne
+Jazanoski
+Jcst
+Jean-Louis
+Jeanes
+Jeavons
+Jedrysiak
+Jee
+Jee-Howe
+Jefferson
+Jeffries
+Jeleniewski
+Jelinek
+Jemczyk
+Jeng
+Jenkins
+Jenkinson
+Jenner
+Jenness
+Jennette
+Jennings
+Jensen
+Jensenworth
+Jepson
+Jeremy
+Jernigan
+Jeronimo
+Jeroski
+Jervis
+Jeska
+Jesshope
+Jessup
+Jesty
+Jeter
+Jew
+Jewell
+Jewett
+Jezioranski
+Jiang
+Jimenez
+Jims
+Jindal
+Joachimpillai
+Joannidis
+Joannis
+Jobe
+Jobs
+Jodoin
+Jodoin-St.Jean
+Joe
+Joffe
+Johannes
+Johannessen
+Johannsen
+Johansen
+Johanson
+John
+Johns
+Johnsen
+Johnson
+Johnston
+Johnstone
+Joiner
+Jolicoeur
+Joll
+Jolliffe
+Joly
+Jonczak
+Jone
+Jones
+Jong
+Jonkheer
+Joplin
+Jordan
+Jorgensen
+Joron
+Joseph
+Joshi
+Josiah
+Joslin
+Joson
+Joudrey
+Jowett
+Joyce
+Joyner
+Juan
+Jubainville
+Jubb
+Jubenville
+Jubinville
+Juby
+Judd
+Juers
+Jugandi
+Juhan
+Julian
+Junaid
+Juneau
+Jung
+Jungmeisteris
+Juni
+Junkie
+Junkin
+Jurek
+Jurewicz
+Jurgutis
+Jurman
+Juscesak
+Juskevicius
+Justus
+Kabe
+Kabel
+Kabolizadeh
+Kacor
+Kacsor
+Kaczmarek
+Kaczmarska
+Kaczynski
+Kadamani
+Kaden
+Kadlecik
+Kaefer
+Kaehler
+Kahan
+Kahhale
+Kahhan
+Kahil
+Kahkonen
+Kahn
+Kahnert
+Kahtasian
+Kaid
+Kaidannek
+Kaigler
+Kajeejit
+Kaji
+Kakou
+Kakuta
+Kalab
+Kalaichelvan
+Kalair
+Kalechstein
+Kales
+Kaley
+Kaliski
+Kalitzkus
+Kalleward
+Kallio
+Kalman
+Kalnitsky
+Kalogerakos
+Kalsey
+Kalt
+Kaluzny
+Kalwa
+Kalwarowskyj
+Kalyani
+Kam
+Kamal
+Kamboh
+Kamel
+Kamerson
+Kamiyama
+Kammerer
+Kamminga
+Kamoun
+Kamyszek
+Kan
+Kanagendra
+Kandra
+Kane
+Kang
+Kang-Gill
+Kangelis
+Kanies
+Kannel
+Kannemann
+Kanno
+Kantor
+Kanungo
+Kao
+Kaoud
+Kapadia
+Kapatou
+Kapella
+Kapil
+Kaplan
+Kapp
+Kaps
+Kapsa
+Kapuscinski
+Kara
+Karaali
+Karademir
+Karam
+Karass
+Karchevski
+Kardomateas
+Kardos
+Karhuniemi
+Karia
+Karibian
+Karim
+Karkotsky
+Karmali
+Karmous-Edwards
+Karn
+Karnazes
+Karol
+Karole
+Karolefski
+Karp
+Karr
+Karsan
+Karsner
+Karsz
+Karunaratne
+Karwowski
+Kasbia
+Kasbow
+Kasdorf
+Kashani-nia
+Kashef
+Kashima
+Kashul
+Kasparian
+Kasprzak
+Kassam
+Kassissia
+Kastelberg
+Kasten
+Kastner
+Kaston
+Kasumovich
+Katcher
+Katchmar
+Kathie
+Kathnelson
+Katibian
+Kato
+Katsouras
+Katz
+Katzenelson
+Kaudel
+Kauffeldt
+Kauffman
+Kaufman
+Kaufmann
+Kaunas
+Kaura
+Kaus
+Kausche
+Kavanagh
+Kavis
+Kawa
+Kawaguchi
+Kawakami
+Kawamura
+Kawashima
+Kawauchi
+Kay
+Kaya
+Kayalioglu
+Kayar
+Kaye
+Kayle
+Kaypour
+Kayser
+Kazimierski
+Kazmierczak
+Kea
+Kealey
+Kean
+Keane
+Kearney
+Kearns
+Keast
+Keates
+Keating
+Kebede
+Kechichian
+Keck
+Kee
+Keef
+Keegstra
+Keehan
+Keehn
+Keelan
+Keeler
+Keels
+Keenan
+Keene
+Keene-Moore
+Keer
+Keever
+Keffer
+Kehler
+Kehoe
+Kehr
+Keifer
+Keighley
+Keilholz
+Keilty
+Keim
+Keiser
+Keitel
+Keith
+Kelbe
+Kelemen
+Kelkar
+Kell
+Kelland
+Kelleher
+Keller
+Kellerman
+Kellett
+Kelley
+Kellogg
+Kellum
+Kelly
+Kelsay
+Kelsch
+Kelso
+Kember
+Kemish
+Kemkeng
+Kemme
+Kemp
+Kempf
+Kempffer
+Kemppainen
+Kempski
+Kempster
+Kendall
+Kendi
+Kendrick
+Kenedi
+Kenik
+Kenkel
+Kenlan
+Kenmir
+Kennaday
+Kennard
+Kennedy
+Kenneth
+Kenney
+Kenny
+Kenol
+Kensinger
+Kent
+Kenworthy
+Kenyon
+Keogh
+Kepekci
+Kerfoot
+Keriakos
+Kerlovich
+Kernahan
+Kerner
+Kernodle
+Kerns
+Kerr
+Kerschner
+Kerwin
+Keseris
+Kesler
+Kesling
+Kessel
+Kessing
+Kessler
+Kestelman
+Kester
+Ketcham
+Ketcheson
+Ketchum
+Ketkar
+Ketley
+Ketsler
+Ketterer
+Kettles
+Keung
+Keuning
+Keveny
+Keyes
+Khadbai
+Khalaf
+Khalil
+Khalilzadeh
+Khanna
+Khatib
+Khatod
+Khatri
+Khawar
+Khesin
+Khezri
+Khimasia
+Kho
+Khodosh
+Khorami
+Khosla
+Khosravi
+Khosraviani
+Khouderchah
+Khouderchan
+Khoury
+Khouzam
+Khurana
+Kiang
+Kibler
+Kidd
+Kiebel
+Kiecksee
+Kiefer
+Kiel
+Kielstra
+Kiely
+Kiens
+Kiernan
+Kiger
+Kigyos
+Kikuchi
+Kikuta
+Kilbank
+Kilburn
+Kilby
+Kilcoin
+Kilcoyne
+Killam
+Killeen
+Killen
+Kilner
+Kilpatrick
+Kilzer
+Kim
+Kimball
+Kimbarovsky
+Kimble
+Kimbrell
+Kimbrough
+Kimler
+Kimm
+Kimma
+Kimoto
+Kinahan
+Kinamon
+Kinch
+Kindel
+Kindem
+King
+Kingaby
+Kingan
+Kingdon
+Kingrey
+Kingsbury
+Kingshott
+Kingsland
+Kingsley-Evans
+Kingston
+Kingzett
+Kinley
+Kinnaird
+Kinney
+Kinniburgh
+Kinos
+Kinoshita
+Kinrys
+Kinsella
+Kinsey
+Kinsman
+Kinstley
+Kipnis
+Kirady
+Kirby
+Kirchner
+Kirfman
+Kirkby
+Kirkendall
+Kirkham
+Kirkland
+Kirkley
+Kirkpatrick
+Kirkwood
+Kirley
+Kirn
+Kirouac
+Kish
+Kishi
+Kita
+Kittinger
+Kitzmiller
+Kivell
+Klaassen
+Klammer
+Klamner
+Klapper
+Klappholz
+Klashinsky
+Klasky
+Klassen
+Klaudinyi
+Klaudt
+Klavkalns
+Klebsch
+Klein
+Kleppinger
+Kletchko
+Kleynenberg
+Klimas
+Klimon
+Kline
+Kling
+Klingsporn
+Klodt
+Klostermann
+Kloth
+Klotz
+Klowak
+Klug
+Kluger
+Kluke
+Klutts
+Knapp
+Knappe
+Knapton
+Knecht
+Kneedler
+Kneese
+Kneeshaw
+Kneisel
+Knes-Maxwell
+Kness
+Knickerbocker
+Knieps
+Knighten
+Knighton
+Knio
+Knipe
+Knitl
+Knittel
+Knobeloch
+Knobloch
+Knorp
+Knorr
+Knouse
+Knowles
+Knox
+Knudsen
+Ko
+Koa
+Koay
+Kobeski
+Kobierski
+Koblitz
+Kobreek
+Koch
+Kochanski
+Kochis
+Kodnar
+Kodsi
+Koelbl
+Koeller
+Koellner
+Koens
+Koerner
+Kogan
+Kohalmi
+Kohalmi-Hill
+Kohl
+Kohler
+Kohm
+Kohn
+Kohnhorst
+Kohut
+Koiste
+Kok
+Kokkat
+Kokoska
+Kokosopoulos
+Kolappa
+Kolb
+Kolbe
+Kolek
+Kolenda
+Kolesnik
+Koleyni
+Kolk
+Koller
+Kollman
+Kollmorgen
+Kolodiejchuk
+Kolodziej
+Kolos
+Kolski
+Kolton
+Koman
+Komaromi
+Komatsu
+Komenda
+Komorowski
+Konarski
+Konforti
+Kong
+Kong-Quee
+Koning
+Konno
+Konomis
+Koohgoli
+Koohi
+Koolstra
+Koolwine
+Koonce
+Koontz
+Kopala
+Kopfman
+Kopke
+Koprulu
+Korbe
+Korbel
+Kordik
+Korea
+Koren
+Korest
+Korey
+Korf
+Kornachuk
+Kornitzer
+Korpela
+Kortekaas
+Kortje
+Kos
+Kosarski
+Kosasih
+Kosiorska
+Kositpaiboon
+Koskie
+Koskinen
+Koslowsky
+Kosnaskie
+Kosowan
+Koss
+Kosten
+Koster
+Kostowskyj
+Kosturik
+Kostyniuk
+Kot
+Kotamarti
+Kotler
+Kotval
+Kotyk
+Kou
+Kouba
+Kouhi
+Kovacs
+Koval
+Kovarik
+Kovats
+Koverzin
+Kowal
+Kowalczewski
+Kowaleski
+Kowalkowski
+Kowallec
+Kowalski
+Kowalsky
+Kozak
+Kozelj
+Koziol
+Kozlowski
+Kozsukan
+Kozuch
+Kozyra
+Kpodzo
+Krabicka
+Kraehenbuehl
+Krajacic
+Krajesky
+Krakowetz
+Kramar
+Kramer
+Kranenburg
+Krater
+Kratz
+Krauel
+Kraus
+Krausbar
+Krause
+Krautle
+Krawchuk
+Krawec
+Kreiger
+Kreimer
+Krenn
+Krenos
+Kresl
+Kretsch
+Krick
+Krieg
+Kriegler
+Krienke
+Krier
+Krikorian
+Krisa
+Krishnan
+Kristian
+Kristjanson
+Krivossidis
+Kriz
+Krodel
+Kroeger
+Krogh
+Krol
+Kromer
+Krone
+Krotish
+Krowlek
+Krozser
+Krten
+Krueger
+Krug
+Kruger
+Krull
+Krumwiede
+Kruse
+Kruusement
+Kruziak
+Krym
+Kryski
+Krysko
+Ku
+Kuan
+Kubash
+Kubik
+Kubitschek
+Kuchelmeister
+Kuczynski
+Kudas
+Kudrewatych
+Kuechler
+Kuehn
+Kuehne
+Kuhfus
+Kuhlkamp
+Kuhn
+Kuhns
+Kuivinen
+Kujanpaa
+Kulachandran
+Kulikowsky
+Kulinski
+Kulkarni
+Kum
+Kumagai
+Kumamoto
+Kumar
+Kummer
+Kun
+Kundel
+Kunecke
+Kung
+Kuniyasu
+Kunkel
+Kuntova
+Kunz
+Kuo
+Kupe
+Kupferman
+Kupferschmidt
+Kupidy
+Kupitz
+Kuracina
+Kurash
+Kurauchi
+Kurczak
+Kurdas
+Kurdziel
+Kureshy
+Kurian
+Kurio
+Kurita
+Kurniawan
+Kurolapnik
+Kurowski
+Kursell
+Kurth
+Kurtz
+Kuruppillai
+Kuryliak
+Kurylyk
+Kus
+Kusan
+Kushan
+Kushnir
+Kushwaha
+Kusmider
+Kusumakar
+Kusyk
+Kutac
+Kutch
+Kutschke
+Kutten
+Kuykendall
+Kuzbary
+Kuzemka
+Kuzyk
+Kuzz
+Kwa
+Kwan
+Kwant
+Kwast
+Kwee
+Kwiatkowska
+Kwissa
+Kwok
+Kwong
+Kydd
+Kyle
+Kyoung
+Kyzer
+L'Anglais
+L'Ecuyer
+L'Heureux
+L'ecuyer-Demers
+LIBRARIAN
+LLoyd
+LRCRICH
+LaBauve
+LaBelle
+LaBonte
+LaBranche
+LaFargue
+LaPierre
+LaPlante
+LaPointe
+LaRue
+LaVecchia
+LaVoie
+Laale
+Laba
+Labarge
+Labenek
+Laberge
+Labfive
+Labiche
+Labossiere
+Labrador
+Labrie
+Labrinos
+Labuhn
+Lacasse
+Lacelle
+Lachambre
+Lachance
+Lachine
+Lachowski
+Lackenbauer
+Lackie
+Lacombe
+Lacosse
+Lacoste
+Lacroix
+Ladd
+Ladet
+Ladouceur
+Ladva
+Ladymon
+Lafata
+Laferriere
+Lafever
+Lafferty
+Laflamme
+Lafleur
+Lafontaine
+Laforge
+Laframboise
+Lafrance
+Lagace
+Lagarde
+Lagrandeur
+Lahaie
+Lahaye
+Lahey
+Lahlum
+Lahteenmaa
+Lai
+Laidlaw
+Laine
+Lainesse
+Laing
+Laitinen
+Lajzerowicz
+Laker
+Lakhani
+Lakhian
+Lakier
+Lakins
+Lakshminarayan
+Lalani
+Laliberte
+Lalka
+Lally
+Lalonde
+Lalu
+Lamarche
+Lamarque
+Lamarre
+Lambregts
+Lambregtse
+Lamedica
+Lamers
+Lamey
+Lamirande
+Lamm
+Lamonde
+Lamont
+Lamontagne
+Lamothe
+Lamouche
+Lamoureux
+Lampe
+Lamphier
+Lampman
+Lamy
+Lan
+Lanava
+Lancaster
+Lanctot
+Landaveri
+Lande
+Lander
+Landers
+Landon
+Landriault
+Landry
+Laney
+Lang
+Langdon
+Lange
+Langelier
+Langenberg
+Langer
+Langett
+Langevin
+Langford
+Langlais
+Langley
+Langlois
+Langner
+Langstaff
+Langton
+Laniel
+Lanier
+Lannan
+Lanoe
+Lanoszka
+Lanoue
+Lanouette
+Lans
+Lansupport
+Lanteigne
+Lanthier
+Lantos
+Lantto
+Lantz
+Lanunix
+Lanwan
+Lanz
+Lanzkron
+Lao
+Lapchak
+Laporte
+Lapostolle
+Lappan
+Laprade
+Lapre
+Laprise
+Lara
+Larabie
+Laraia
+Larche
+Larin
+Lariviere
+Larkin
+Larkins
+Larmour
+Larner
+Laroche
+Larock
+Larocque
+Larose
+Larribeau
+Larrigan
+Larsen
+Larson
+Larstone
+Laruffa
+Larwill
+Lasch
+Laschuk
+Lashansky
+Lasher
+Lashmit
+Lask
+Laskaris
+Laskin
+Lassig
+Lassiter
+Lasson
+Lassonde
+Laster
+Latchford
+Latella
+Latessa
+Lathangue
+Lathrop
+Latif
+Latin
+Latorre
+Latour
+Latreille
+Lattanzi
+Latulippe
+Lau
+Lauderdale
+Laufer
+Laugher
+Laughlin
+Laughridge
+Laughton
+Laurence
+Laurent
+Lauria
+Lauriault
+Laurich
+Laurin
+Lauriston
+Laursen
+Lauruhn
+Lauson
+Lauten
+Laux
+Lauze
+Lauzon
+Lavallee
+Lavarnway
+Lavelle
+Lavergne
+Lavers
+Laverty
+Lavictoire
+Lavigne
+Laville
+Laviolette
+Lavorata
+Lawbaugh
+Lawler
+Lawless
+Lawlis
+Lawlor
+Lawrence
+Lawrie
+Laws
+Lawson
+Lawther
+Lawton
+Layne
+Layton
+Lazar
+Lazarou
+Lazarowich
+Lazure
+Lazzara
+Le
+LeBaron
+LeBlanc
+LeClair
+LeClaire
+LeCouteur
+LeDinh
+LeGuen
+LePage
+LeTarte
+Leader
+Leafloor
+Leahy
+Leander
+Leang
+Leapheart
+Leary
+Leatham
+Leathers
+Leavell
+Leaver
+Leavitt
+Lebars
+Lebeau
+Lebel
+Leblond
+Lebo
+Lebon
+Lecandro
+Leckie
+Leclerc
+Lecompte
+Lecours
+Ledamun
+Leder
+Lederman
+Ledet
+Ledford
+Ledou
+Ledoux
+Ledu
+Leduc
+Ledwell
+Lee
+Leenher
+Leeson
+Lefebvre
+Lefevre
+Leffler
+Leftwich
+Legeny
+Leger
+Legg
+Leggett
+Legrandvallet
+Legris
+Legros
+Legrove
+Legrow
+Lehman
+Lehmann
+Lehrbaum
+Lehtinen
+Lehtovaara
+Leiba
+Leibich
+Leibovitz
+Leibowitz
+Leicht
+Leiding
+Leigh
+Leighton
+Leiker
+Leima
+Lein
+Leinen
+Leistico
+Leitch
+Leitner
+Leitrick
+Leiwe
+Lem
+Lemaire
+Lemay
+Lemieux
+Lemky
+Lemley
+Lemyre
+Lenathen
+Lenehan
+Leney
+Lengel
+Lenhard
+Lenior
+Lenir
+Lennig
+Lenox
+Lentz
+Lenz
+Lenzi
+Leo
+Leo-Miza
+Leon
+Leonard
+Leone
+Leong
+Leonhardt
+Leonida
+Leoutsarakos
+Leow
+Lepine
+Lepore
+Leppert
+Lerch
+Leroux
+Lescot
+Leshowitz
+Lesmerises
+Lesniak
+Lesourd
+Lesperance
+Lessard
+Lessin
+Lester
+Lesway
+Letchworth
+Letendre
+Leth
+Lethbridge
+Lethebinh
+Letourneau
+Letsome
+Lett
+Letulle
+Leuenberger
+Leung
+Leuty
+Levac
+Levasseur
+Leveille
+Levere
+Levert
+Levesque
+Levey
+Levi
+Levin
+Levine
+Levisky
+Levitin
+Levo
+Lew
+Lewandowski
+Lewek
+Lewellen
+Lewinski
+Ley
+Leydig
+Li
+Lian
+Liang
+Liao
+Lias
+Liaw
+Libadmin
+Libov
+Licandro
+Licata
+Licerio
+Lichtenstein
+Liddle
+Lidster
+Lidstone
+Lieberman
+Liebrecht
+Liem
+Lienemann
+Lienhard
+Liepa
+Liesemer
+Liesenberg
+Lieure
+Liew
+Lifshey
+Lightfield
+Lightfoot
+Lighthall
+Lighthiser
+Lightowler
+Ligon
+Ligurs
+Likert
+Likourgiotis
+Lilleniit
+Lillis
+Lilly
+Limbaugh
+Limeina
+Limerick
+Lin
+Linaugh
+Linback
+Linberg
+Lind
+Lindamood
+Lindberg
+Lindbergh
+Lindell
+Lindemulder
+Linder
+Lindholm
+Lindler
+Lindow
+Lindquist
+Lindsay
+Lindsey
+Lindstrom
+Lindt
+Linebarger
+Linegar
+Lineham
+Linfield
+Ling
+Lingafelter
+Linke
+Linkletter
+Linn
+Linton
+Lipari
+Lipe
+Lippens
+Lipschutz
+Lipscomb
+Lipski
+Lisak
+Lisch
+Lischynsky
+Lisee
+Lisenchuk
+Liskoff
+Lister
+Liston
+Litherland
+Litt
+Littau
+Littlewood
+Litva
+Litz
+Litzenberger
+Liu
+Livas
+Livek
+Liverman
+Livermore
+Livezey
+Livingston
+Livingstone
+Livshits
+Lizak
+Lizzi
+Ljubicich
+Llaguno
+Llanos
+Llewellyn
+LoBue
+Loa
+Loadbuild
+Loadbuilder
+Loader
+Loadsum
+Lobasso
+Lobaugh
+Lobello
+Lobianco
+Lobin
+Locicero
+Lockard
+Locke
+Locken
+Lockett
+Lockhart
+Lockwood
+Lococo
+Lodeserto
+Loe
+Loeffler
+Loewen
+Loftis
+Logan
+Logarajah
+Loggins
+Loghry
+Logntp
+Logue
+Loh
+Lohoar
+Loi
+Loiseau
+Loisel
+Lojewski
+Loker
+Lollis
+Lombardo
+Lombrink
+Lommen
+Londhe
+London
+Lonergan
+Longbottom
+Longchamps
+Longfield
+Longhenry
+Longo
+Longpre
+Longtin
+Lonnman
+Lonsdale
+Loo
+Loong
+Loos
+Looyen
+Loper
+Loperena
+Lopes
+Lopez
+Lopiano
+Lopinski
+Loponen
+Loquercio
+Lorance
+Lorenc
+Lorenz
+Lorenzen
+Lorenzo
+Lorfano
+Lorimer
+Lorincz
+Lorint
+Lormor
+Loro
+Lortie
+Losfeld
+Losier
+Loso
+Lotan
+Lote
+Lotochinski
+Lott
+Lotz
+Lou
+Loucel
+Loudiadis
+Lough
+Loughery
+Loughran
+Loughrin
+Louie
+Louis
+Louisseize
+Louk
+Louladakis
+Loux
+Lovas
+Lovatt
+Loveday
+Lovegrove
+Lovejoy
+Lovekin
+Lovelace
+Loveland
+Loveless
+Lovell
+Lovett
+Lovin
+Lovitt
+Lowder
+Lowe
+Lowery
+Lowman
+Lowrie
+Lowther
+Loxton
+Loyd
+Loyer
+Loyola
+Loyst
+Loza
+Lozier
+Lozinski
+Lrcrtp
+Lu
+Luan
+Lucas
+Lucente
+Lucey
+Luciani
+Lucking
+Ludchen
+Ludviksen
+Ludwick
+Ludwig
+Lue
+Luetchford
+Luetke
+Luettchau
+Luff
+Lugsdin
+Lugwig
+Luhcs
+Lui
+Luin
+Luk
+Lukas
+Lukassen
+Lukaszewski
+Luke
+Luker
+Lukers
+Lukic
+Lukie
+Lukshis
+Lum
+Lum-Wah
+Lumley
+Lumsden
+Lun
+Luna
+Lund
+Lunde
+Lundhild
+Lundy
+Lunn
+Luoma
+Luong
+Lupatin
+Lupher
+Lupien
+Luquire
+Luscombe
+Lussier
+Luszczek
+Lutz
+Luu
+Luwemba
+Luxford
+Luyten
+Luzarraga
+Luzine
+Ly
+Lyall
+Lychak
+Lyliston
+Lyman
+Lynham
+Lynn
+Lynton
+Lyon
+Lyons
+Lysinger
+Lystad
+Lystiuk
+Lystuik
+Lytle
+MACKenzie
+MAINT
+MIC
+MMail
+MTL
+Maahs
+Maas
+Mabes
+Mabson
+Mabuchi
+Mac
+Mac Maid
+MacAdams
+MacArthur
+MacCarthy
+MacConaill
+MacCormack
+MacDermaid
+MacDonald
+MacDonell
+MacDougall
+MacDowall
+MacDuff
+MacElwee
+MacFarlane
+MacGillivray
+MacGregor
+MacHattie
+MacInnes
+MacInnis
+MacIsaac
+MacIver
+MacKay
+MacKinnon
+MacLaren
+MacLaurin
+MacLean
+MacLennan
+MacLeod
+MacMartin
+MacMeekin
+MacMillan
+MacMillan-Brown
+MacMullin
+MacNaughton
+MacNeil
+MacNeill
+MacPhail
+MacPherson
+MacRae
+Macalik
+Macaulay
+Maccallum
+Macchiusi
+Mach
+Machan
+Machika
+Machnicki-Hynes
+Maciejewski
+Maciel
+Mackel
+Mackey
+Mackin
+Macklem
+Macklin
+Mackzum
+Maclellan
+Macnicoll
+Macoosh
+Macpost
+Macquistan
+Macsupport
+Madan
+Madani
+Maddix
+Mader
+Madgett
+Madhavan
+Madigan
+Madill
+Madison
+Madl
+Madras
+Madsen
+Maduri
+Maennling
+Maenpaa
+Maeya
+Magbee
+Mage
+Magee
+Mages
+Maginley
+Maglione
+Magnan
+Magnuson
+Magnussen
+Magnusson
+Magrath
+Magri
+Maguire
+Mah
+Mahaffee
+Mahbeer
+Mahendra
+Maher
+Maheu
+Maheux
+Mahiger
+Mahin
+Mahlig
+Mahn
+Mahon
+Mahonen
+Mahoney
+Mahoney-Robbs
+Mai
+Maidenhead
+Maidens
+Maier
+Maijala
+Maika
+Maikawa
+Mailroom
+Mainardi
+Maine
+Mainville
+Mainwaring
+Mair
+Maisonneuve
+Maitland
+Majd
+Majeed
+Majek
+Majernik
+Majid
+Majmudar
+Majors
+Majumdar
+Majury
+Mak
+Makarenko
+Makino
+Makinson
+Makohoniuk
+Makoid
+Maksoud
+Maksuta
+Malaher
+Malam
+Malanos
+Malavia
+Malcolm
+Malec
+Malee
+Malek
+Maleski
+Malhi
+Malhotra
+Maliepaard
+Malik
+Malisic
+Maliski
+Malizia
+Malkani
+Malkiewicz
+Malkinson
+Malle
+Mallett
+Mallory
+Malloy
+Mallozzi
+Malmqvist
+Malone
+Maloney
+Malott
+Malta
+Maltby
+Maltese
+Maludzinski
+Malynowsky
+Malyszka
+Malzahn
+Mamoulides
+Management
+Manager
+Manceau
+Manchester
+Mancini
+Mand
+Mandel
+Mandeville
+Mandrusov
+Maness
+Mangione
+Mangum
+Manica
+Manickam
+Manitius
+Mankowski
+Manley
+Mann
+Manner
+Manners
+Manning
+Mannino
+Mannion
+Manno
+Manolios
+Manoukian
+Mansbridge
+Mansell
+Mansi
+Manson
+Mansourati
+Mansouri
+Mantell
+Manto
+Manuel
+Manus
+Manuszak
+Manverse
+Manwaring
+Mao
+Mapes
+Mapile
+Maracle
+Maragoudakis
+Marano
+Marasco
+Marasliyan
+Marc
+Marcanti
+Marcantonio
+Marceau
+Marcelissen
+Marcellus
+Marcey
+Marchand
+Marchant
+Marcheck
+Marchese
+Marchetti
+Marciniuk
+Marco
+Marcom
+Marconi
+Marcotte
+Marcoux
+Marcum
+Marcus
+Marengere
+Marette
+Margetson
+Marghetis
+Margittai
+Mariani
+Marineau
+Marino
+Marinos
+Marion
+Mariotti
+Maritan
+Markell
+Markes
+Marketing
+Markham
+Markiewicz
+Markland
+Markle
+Markmeyer
+Marko
+Markovic
+Markovich
+Marks
+Marleau
+Marling
+Marlow
+Marmen
+Marmillon
+Marmion
+Marneris
+Marouchos
+Maroun
+Marples
+Marquart
+Marr
+Marra
+Marrec
+Marren
+Marrett
+Marriott
+Marrone
+Marschewaki
+Marsden
+Marshall
+Marshaus
+Marshman
+Marson
+Martel
+Martell
+Martens
+Martenson
+Martenstyn
+Marti
+Martincello
+Martincich
+Martineau
+Martinez
+Marting
+Martins
+Marttinen
+Marturano
+Marui
+Marum
+Maruszak
+Marx
+Maryak
+Marzella
+Marzullo
+Masales
+Masapati
+Masciarelli
+Mashura
+Masika
+Masini
+Maskell
+Maskery
+Maslen
+Masotti
+Masse
+Massengill
+Massey
+Massicotte
+Massingale
+Masson
+Massone
+Massonneau
+Massoudian
+Massumi
+Mastellar
+Mastenbrook
+Masterplan
+Masters
+Masterson
+Mastromattei
+Mastronardi
+Masty
+Matalon
+Matatall
+Materkowski
+Materna
+Mather
+Matheson
+Mathew
+Mathews
+Mathewson
+Mathias
+Mathieson
+Mathieu
+Mathis
+Mathiue
+Mathur
+Mathurin
+Mathus
+Matibag
+Matlock
+Matney
+Matrundola
+Matson
+Matsubara
+Matsugu
+Matsunaga
+Matsushita
+Matsuzaka
+Matsuzawa
+Matteau
+Mattes
+Matthews
+Mattiussi
+Mattiuz
+Matton
+Mattson
+Matusik
+Mau
+Mauck
+Maudrie
+Mauer
+Mauldin
+Maund
+Mauney
+Maunu
+Maurer
+Maveety
+Mavis
+Mavrou
+Mawani
+Mawji
+Mawst
+Maxey
+Maxin
+Maxseiner
+Maxsom
+Maybee
+Maycock
+Maye
+Mayea
+Mayenburg
+Mayer
+Mayes
+Mayfield
+Mayhugh
+Mayman
+Maynard
+Mayne
+Maynes
+Mayo
+Mayoux
+Mayr
+Mayr-Stein
+Mays
+Mazanji
+Mazarick
+Mazey
+Mazurek
+Mazzei
+Mc
+Mc Alpine
+Mc Ginn
+McAdam
+McAdams
+McAdorey
+McAfee
+McAlear
+McAleer
+McAlister
+McAllister
+McAllum
+McAndrew
+McArthur
+McAteer
+McAulay
+McAuliffe
+McBeth
+McBrayne
+McBride
+McBroom
+McBryan
+McCabe
+McCafferty
+McCaffity
+McCaffrey
+McCaig
+McCain
+McCall
+McCallen
+McCallum
+McCampbell
+McCandless
+McCann
+McCarroll
+McCarron
+McCarthy
+McCartin
+McCartney
+McCaugherty
+McCaughey
+McCauley
+McCaw
+McClain
+McClarren
+McClary
+McClean
+McCleery
+McClellan
+McClelland
+McClendon
+McClennon
+McClintock
+McCloskey
+McCloughan
+McClure
+McCluskey
+McClymont
+McColl
+McCollam
+McCollum
+McColman
+McComb
+McCombs
+McConaghay
+McConkey
+McConnell
+McConney
+McCord
+McCorkell
+McCorkle
+McCormack
+McCormick
+McCorquodale
+McCoy
+McCracken
+McCrain
+McCraney
+McCray
+McCready
+McCreanor
+McCreath
+McCreesh
+McCrimmon
+McCuaig
+McCue
+McCullen
+McCulloch
+McCullogh
+McCullough
+McCully
+McCune
+McCurdy
+McCusker
+McDade
+McDaniel
+McDaniels
+McDavitt
+McDermott
+McDevitt
+McDonald
+McDonnell
+McDonough
+McDoom
+McDougald
+McDougall
+McDowall
+McDowell
+McDuffie
+McDunn
+McEachern
+McElderry
+McElhone
+McElligott
+McElrea
+McElroy
+McEvoy
+McEwan
+McEwen
+McEwen-Robillard
+McFadden
+McFall
+McFarland
+McFarlane
+McFeely
+McGalliard
+McGarry
+McGaughey
+McGee
+McGeown
+McGhee
+McGill
+McGillicuddy
+McGillvray
+McGilly
+McGinn
+McGlynn
+McGonagle
+McGonigal
+McGorman
+McGovern
+McGowan
+McGrath
+McGregor
+McGruder
+McGuigan
+McGuinness
+McGuire
+McGurn
+McHale
+McHan
+McHarg
+McHugh
+McInerney
+McInnis
+McIntee
+McIntire
+McIntomny
+McIntosh
+McIntyre
+McIsaac
+McIver
+McKay
+McKeage
+McKSeague
+McKearney
+McKechnie
+McKee
+McKeegan
+McKeen
+McKeithan
+McKenna
+McKenney
+McKenzie
+McKeone
+McKeown
+McKerrow
+McKibben
+McKibbin
+McKibbon
+McKie
+McKillop
+McKinlay
+McKinley
+McKinney
+McKinnon
+McKnight
+McLachlan
+McLaren
+McLauchlan
+McLaughlin
+McLawhon
+McLawhorn
+McLean
+McLellan
+McLemore
+McLenaghan
+McLendon
+McLennan
+McLeod
+McLuskie
+McMahan
+McMahon
+McMann
+McMannen
+McManus
+McMasters
+McMenamin
+McMichael
+McMillan
+McMillen
+McMillian
+McMillion
+McMinn
+McMonagle
+McMullen
+McMullin
+McMurray
+McNab
+McNabb
+McNair
+McNally
+McNamara
+McNamee
+McNaughton
+McNeal
+McNealy
+McNeely
+McNeese
+McNeil
+McNeill
+McNeilly
+McNerlan
+McNerney
+McNichol
+McNicol
+McNitt
+McNulty
+McPhaden
+McPhail
+McPhee
+McPherson
+McQuaid
+McQuaig
+McQuarrie
+McQueen
+McRae
+McRann
+McReady
+McRitchie
+McRonald
+McRuvie
+McSheffrey
+McSorley
+McSween
+McTaggart
+McTavish
+McTiernan
+McTurner
+McVay
+McVeety
+McVeigh
+McVey
+McVicar
+McVicker
+McWalter
+McWalters
+McWaters
+McWherter
+McWhinney
+McWhorter
+McWilton
+Mccoy-Cage
+Mcellistrem
+Mcgehee
+Mcginley
+Mcgrachan
+Mcilroy
+Mcmeegan
+Mcshane
+Meachum
+Meadows
+Meads
+Meagher
+Mealin
+Meany
+Measures
+Meche
+Mecher
+Meckler
+Meckley
+Mecteau
+Medefesser
+Medeiros
+Meder
+Medioni
+Medlin
+Medlock
+Mednick
+Meehan
+Meeks
+Meerveld
+Meese
+Meffe
+Meggitt
+Meghani
+Mehta
+Mei
+Meier
+Meijer
+Meikle
+Meilleur
+Mein
+Meiser
+Meisner
+Meissner
+Mejdal
+Mejia
+Mejury
+Melanson
+Meldrum
+Meleg
+Meleski
+Meleskie
+Meletios
+Melfi
+Melkild
+Mellor
+Melnyk
+Meloling
+Melton
+Melucci
+Menaker
+Menard
+Menasce
+Menashian
+Mencer
+Mendelsohn
+Mendez
+Mendolia
+Mendonca
+Mendorf
+Mendy
+Menechian
+Menendez
+Menna
+Mennie
+Menon
+Mensinkai
+Menyhart
+Menzel
+Menzies
+Mercier
+Merciline
+Meredith
+Meres
+Mereu
+Meridew
+Merinder
+Meriwether
+Merizzi
+Merklinger
+Mermelstein
+Merrett
+Merrick
+Merrill
+Merrills
+Merrils
+Merrithew
+Merritt
+Merryweather
+Mersch
+Mersinger
+Merwin
+Merworth
+Meseberg
+Meskimen
+Mesko
+Mesquita
+Messer
+Messerian
+Messick
+Messier
+Meszaros
+Metcalf
+Metcalfe
+Metelski
+Metheny
+Metherell
+Methiwalla
+Methot
+Metler
+Metrailer
+Metrics
+Mettrey
+Metz
+Metzger
+Meubus
+Meunier
+Mevis
+Meyer
+Meyerink
+Mezzano
+Mezzoiuso
+Mgmt
+Mia
+Miao
+Miasek
+Miceli
+Michael
+Michaelides
+Michaels
+Michaelson
+Michailov
+Michalos
+Michaud
+Michels
+Michelsen
+Michelson
+Michelussi
+Mickens
+Micklos
+Micucci
+Middlebrooks
+Middleton
+Midha
+Mielke
+Miello
+Miernik
+Miers
+Mierwa
+Miezitis
+Mignault
+Mihan
+Mihara
+Mihm
+Mikelonis
+Mikhail
+Miki
+Miko
+Miksik
+Mikulka
+Milaknis
+Milakovic
+Milan
+Milanovich
+Milar
+Milburn
+Miles
+Milford
+Milian
+Milinkovich
+Millaire
+Millar
+Millard
+Millen
+Millerwood
+Millette
+Milligan
+Milloy
+Mills
+Millspaugh
+Millward
+Milmine
+Milne
+Milotte
+Milstead
+Milston
+Miltenburg
+Milton
+Milway
+Mims
+Mina
+Minai
+Minard
+Mincey
+Minck
+Minegishi
+Miner
+Minetola
+Minichilli
+Minichillo
+Minkus
+Minos
+Minter
+Minthorne
+Minyard
+Mior
+Miotla
+Mir
+Miranda
+Mirande
+Mirarchi
+Mirek
+Miron
+Mirza
+Mis
+Misczak
+Mishina
+Miskelly
+Misko
+Misra
+Missailidis
+Misslitz
+Mistry
+Mistuloff
+Misutka
+Mitalas
+Mitchell
+Mitchelson
+Mitchler
+Mitrani
+Mitrou
+Mitsui
+Mittleider
+Mivehchi
+Mizerk
+Mlacak
+Mlcoch
+Mo
+Mobasheri
+Mobley
+Mocock
+Modafferi
+Modi
+Modigh
+Modl
+Modotto
+Moebes
+Moen
+Moening
+Moeschet
+Moetteli
+Moffatt
+Moffet
+Moffett
+Mofina
+Moghe
+Moghis
+Mogridge
+Moh
+Mohajeri
+Mohammad
+Mohammed
+Mohan
+Moharram
+Mohideen
+Mohr
+Mohrmann
+Moizer
+Mojgani
+Mok
+Mok-Fung
+Mokbel
+Mokros
+Molani
+Moledina
+Mollerus
+Molloy
+Molnar
+Molochko
+Moloney
+Molson
+Molyneux
+Mombourquette
+Momon
+Momtahan
+Monachella
+Monaco
+Monaghan
+Monahan
+Moncion
+Monck
+Moncur
+Mondor
+Monet
+Monette
+Moneypenny
+Monforton
+Monfre
+Mong
+Moniter
+Monn
+Monroe
+Monson
+Montag
+Montague
+Montaldo
+Montange
+Montanino
+Montcalm
+Monteggia
+Montelli
+Montero
+Monterosso-Wood
+Montgomery
+Montijo
+Montmorency
+Montor
+Montoute
+Montoya
+Montreal
+Montreuil
+Montsion
+Montsko
+Montuno
+Mony
+Monzo
+Moo-Young
+Mooder
+Moogk
+Mooken
+Moomey
+Mooney
+Moorcroft
+Moore
+Moore-Vigeant
+Moorefield
+Moorer
+Moores
+Moorhouse
+Moosavi
+Moraetes
+Morais
+Morales
+Moran
+Morcinelli
+Mordecai
+Morden
+Mordy
+Moreau
+Moree
+Morek
+Moreland
+Morelli
+Moreno
+Morettin
+Morey
+Morgan
+Morgan-Cavallaro
+Morglan
+Mori
+Moriarty
+Morimoto
+Morin
+Morini
+Morino
+Morissette
+Moritz
+Moriyama
+Morley
+Morneau
+Morocz
+Moroz
+Morra
+Morreale
+Morrin
+Morrison
+Morrissette
+Morse
+Morson
+Mortimer
+Morton
+Moschopoulos
+Moseby
+Moser
+Moshinsky
+Moshiri
+Moshtagh
+Moskalik
+Mosley
+Mostovac
+Motashaw
+Mote
+Mototsune
+Mott
+Mou
+Mouat
+Moubarak
+Mouillaud
+Moulds
+Moulsoff
+Moulton
+Mousseau
+Moussette
+Mowat
+Mowbray
+Mowle
+Moxham
+Moxley
+Moxon
+Moy
+Moyano
+Moyce
+Moyer
+Moyers
+Moynihan
+Mozek
+Mozeleski
+Mraz
+Mrozinski
+Msg
+Mtcbase
+Mtlipadm
+Mtnview
+Mucci
+Muchow
+Mucklow
+Mudry
+Muehle
+Mueller
+Muenstermann
+Mufti
+Mugniot
+Muhammed
+Mui
+Muir
+Muise
+Mujahed
+Mukai
+Mukhar
+Mukherjee
+Mukhopadhyay
+Mulder
+Mulders
+Muldoon
+Mulero
+Mulherkar
+Mulholland
+Mullaly
+Mullaney
+Mullarney
+Mullen
+Muller
+Mullett
+Mullin
+Mullinix
+Mullins
+Mulot
+Mulqueen
+Mulroney
+Mulvie
+Mumford
+Mumma
+Mummy-Craft
+Munden
+Mundi
+Mundy
+Munikoti
+Muniz
+Munn
+Munns
+Munro
+Munroe
+Munsey
+Munter
+Munz
+Muradia
+Murash
+Murat
+Murawski
+Murchison
+Murdaugh
+Murdeshwar
+Murdoch
+Murdock
+Murison
+Murnaghan
+Muro
+Murock
+Murphin
+Murphy
+Murphy-king
+Murray
+Murrell
+Murris
+Murtagh
+Murton
+Musa
+Musca
+Musclow
+Muselik
+Musgrove
+Musick
+Mussallem
+Mussar
+Musselwhite
+Musser
+Mustafa
+Mustillo
+Mutcher
+Muthuswamy
+Muttaqi
+Muus
+Muzio
+Myatt
+Myer
+Myers
+Myers-Pillsworth
+Myhill
+Mykityshyn
+Mymryk
+Myrah
+Myre
+Myrick
+Myrillas
+Myroon
+Mystkowski
+NCC
+NE-Region
+NTINASH
+NTPADMIN
+Nabors
+Nace
+Nadeau
+Nadeau-Dostie
+Nadler
+Nadolny
+Nadon
+Naem
+Nafezi
+Nagai
+Nagaraj
+Nagarur
+Nagel
+Nagendra
+Nagenthiram
+Nagle
+Naguib
+Nagy
+Nagys
+Nahabedian
+Nahas
+Nahata
+Nahmias
+Nahorniak
+Naimpally
+Nair
+Nairn
+Naismith
+Najafi
+Nakamura
+Nakano
+Nakatsu
+Nakhla
+Nakhoul
+Nakonecznyj
+Naldrett
+Nall
+Nallengara
+Nambride
+Namiki
+Namont
+Nanamiya
+Nance
+Nandi
+Naolu
+Naor
+Naoum
+Napert
+Naphan
+Napier-Wilson
+Napke
+Napper
+Narasimhan
+Narayan
+Narayana
+Narayanan
+Nardiello
+Naro
+Narraway
+Narron
+Nasato
+Nash
+Nashville
+Nasir
+Nason
+Nass
+Nassoy
+Nassr
+Natale
+Nath
+Nathoo
+Natiuk
+Naufal
+Naugle
+Naujokas
+Naujoks
+Nault
+Nava
+Navalta
+Navaratnam
+Navarre
+Navarro
+Naveda
+Nawaby
+Naybor
+Naylor
+Nazardad
+Ndububa
+NeKueey
+Neal
+Neander
+Nearing
+Neate
+Neault
+Nebel
+Nedderman
+Needham
+Neefs
+Neely
+Negandhi
+Negrich
+Nehring
+Neibauer
+Neider
+Neidy
+Neifert
+Neil
+Neill
+Neilly
+Neilsen
+Neilson
+Neisius
+Neitzel
+Nelon
+Nelson
+Nemec
+Nemes
+Nemeth
+Nentwich
+Nerby
+Nerem
+Nesbitt
+Nesrallah
+Ness
+Nessman
+Nestor
+NetTeam
+Netas
+Netdbs
+Netdev
+Nethersole
+Netlink
+Neto
+Nettles
+Netto
+Network-Ops
+Networkroom
+Networks
+Netzke
+Neubauer
+Neufeld
+Neuman
+Neumann
+Neumeister
+Neuschwander
+Neustifter
+Neusy
+Nevardauskis
+Neville
+Nevins
+Nevison
+Nevrela
+NewLab
+Newberry
+Newbold
+Newby
+Newcomb
+Newcombe
+Newell
+Newham
+Newhook
+Newkirk
+Newland
+Newman
+Newnam
+Newport
+News
+Newsom
+Newsome
+Neyman
+Neywick
+Nezon
+Ng
+Ngo
+Nguyen
+Nguyen-The
+Nhan
+Nicandro
+Niccolls
+Nicholas
+Nicholl
+Nichols
+Nicholson
+Nickell
+Nickells
+Nickels
+Nickerson
+Nickle
+Nickonov
+Nicol
+Nicolaou
+Nicoletta
+Nicolle
+Nicosia
+Nie
+Niebudek
+Niedens
+Niedra
+Niedzwiecki
+Nielsen
+Nielson
+Niemi
+Niepmann
+Niergarth
+Nigam
+Nikfarjam
+Nikiforuk
+Nikolopoulos
+Nilakantan
+Niles
+Nill
+Nilson
+Nilsson
+Nimmo
+Ninety-one
+Ning
+Nipper
+Niro
+Nisbet
+Nischuk
+Nishida
+Nishiguchi
+Nishihara
+Nishimura
+Nishith
+Nishiyama
+Nitschky
+Niu
+Nix
+Nixon
+Nizamuddin
+Nizman
+Njo
+Noah
+Nobes
+Nock
+Noddin
+Noel
+Noffke
+Noguchi
+Nokes
+Nolan
+Nolan-Moore
+Nolen
+Noles
+Nolet
+Noll
+Nolter
+Nomura
+Nonkes
+Noone
+Noorani
+Noorbhai
+Norby
+Norczen
+Nordskog
+Nordstrom
+Norfleet
+Norgaard
+Noris
+Norman
+Normandin
+Norment
+Norndon
+Noronha
+Norris
+Northam
+Northcott
+Norton
+Norwood
+Noseworthy
+Notley
+Nou
+Noujeim
+Novak
+Novia
+Novisedlak
+Novotny
+Nowak
+Nowell
+Nowina-Konopka
+Nowlin
+Noy
+Noye
+Noyes
+Npi
+Nss
+Ntelpac
+Ntlc
+Ntprel
+Nttest
+Nuber
+Nuetzi
+Nugent
+Nunes
+Nunez
+Nunn
+Nunnally
+Nunold
+Nurmi
+Nuttall
+Nyberg
+Nyce
+Nyland
+Nyre
+Nys
+O Karina
+O'Brecht
+O'Brian
+O'Brien
+O'Carroll
+O'Colmain
+O'Connell
+O'Conner
+O'Connor
+O'Dacre
+O'Dale
+O'Dea
+O'Doherty
+O'Donnell
+O'Donovan
+O'Dwyer
+O'Farrell
+O'Grady
+O'Hagan
+O'Hara
+O'Hearn
+O'Heocha
+O'Higgins
+O'Keefe
+O'Keeffer
+O'Kelly
+O'Leary
+O'Malley
+O'Meara
+O'Murchu
+O'Neal
+O'Neall
+O'Neil
+O'Neill
+O'Regan
+O'Reilly
+O'Rourke
+O'Shaughnessey
+O'Shaughnessy
+O'Shea
+O'Sullivan
+O'Toole
+OConnor
+OFCPARM
+OFCPARMS
+OPERATIONS
+OPS
+OPSPLNG
+OKelly
+Oakley
+Oaks
+Oam
+Oanes
+Oastler
+Oates
+Obeda
+Obeidat
+Obenauf
+Ober
+Oberhammer
+Obermeyer
+Obermyer
+Oberpriller
+Oblak
+Obrecht
+Obrien
+Obrusniak
+Ocampo
+Ochman
+Ochoa
+Ochs
+Ocone
+Odac
+Odden
+Odecki
+Odegaard
+Oden
+Odgers
+Odum
+Oertelt
+Oesterreicher
+Oestreich
+Oetting
+Oey
+Offers
+Ogan
+Ogburn
+Ogilvie
+Ogrodnik
+Oguz
+Ohandley
+Ohashi
+Ohmaru
+Ohmayer
+Ohsone
+Ojerholm
+Oka
+Okada
+Okafo
+Okai
+Okamoto
+Oke
+Okon
+Okun
+Okura
+Okuzawa
+Older
+Oldfield
+Oldham
+Oldright
+Oleksyshyn
+Olesen
+Olesko
+Olinger
+Olinyk
+Olivares
+Oliveira
+Oliver
+Olivier
+Olliff
+Olmstead
+Olsen
+Olsheski
+Olson
+Olszewski
+Olynyk
+Oman
+Omura
+Onder
+Ondovcik
+Ong
+Onsiteteam
+Onsy
+Onufrak-Stoner
+Onyshko
+Ooi
+Oost
+Op
+Opalski
+Oper
+Operator
+Operators
+Oplinger
+Oran
+Ord
+Ordas
+Orders
+Ordog
+Ordway
+Oreffice
+Oreilly
+Orfano
+Orford
+Organization
+Orgren-Streb
+Orlando
+Orme
+Ormesher
+Ormsby
+Ornburn
+Orol
+Oros
+Orr
+Orser
+Orsini
+Orth
+Ortiz
+Orton
+Osadciw
+Osatuik
+Osborn
+Osborne
+Osburn
+Osgood
+Oshinski
+Oshiro
+Osiakwan
+Oskorep
+Oslund
+Osman
+Ostapiw
+Ostarello
+Ostaszewski
+Oster
+Osterberg
+Osterhout
+Osterman
+Ostifichuk
+Oswald
+Oswalt
+Otsuka
+Ottco
+Ottosson
+Ottowa
+Oturakli
+Otway
+Oucharek
+Ouellet
+Ouellette
+Ouimet
+Outage
+Outhwaite
+Outram
+Ouzas
+Ovans
+Ovas
+Overby
+Overcash
+Overdyke
+Overton
+Oviedo
+Owen
+Owens
+Owensby
+Owsiak
+Oxendine
+Oyama
+Ozer
+Ozersky
+Oziemblo
+Oziskender
+Ozkan
+Ozmizrak
+Ozmore
+Ozyetis
+PATCOR
+PCBOARDS
+PCBTOOLS
+PKDCD
+Paar
+Pacey
+Pachal
+Pacheco
+Pachek
+Pachner
+Pachulski
+Packager
+Packard
+Pacon
+Paczek
+Paczynski
+Paddon
+Padgett
+Padilla
+Padiou
+Paes
+Paetsch
+Pafilis
+Pagani
+Pageau
+Paget
+Pagi
+Paglia
+Pagliarulo
+Pai
+Paialunga
+Paige
+Paine
+Painter
+Pak
+Paksi
+Pakulski
+Palacek
+Palamar
+Palasek
+Palczuk
+Palermo
+Paley
+Palfreyman
+Palidwor
+Paliga
+Palik
+Paliwal
+Pallen
+Palmer
+Paluso
+Pambianchi
+Pamperin
+Pancewicz
+Panch
+Panchen
+Panchmatia
+Pancholy
+Pandey
+Pandolfo
+Pandrangi
+Pandya
+Panek
+Panesar
+Panger
+Pangia
+Panizzi
+Panke
+Pankhurst
+Pankiw
+Panko
+Pankratz
+Pannell
+Panosh
+Pantages
+Pantalone
+Panter
+Panton
+Pao
+Paoletti
+Paone
+Papadopulos
+Papageorges
+Papageorgiou
+Papajanis
+Papalitskas
+Papantonis
+Paparella
+Pape
+Paperno
+Papers
+Papiez
+Papineau
+Papp
+Pappas
+Papper
+Pappu
+Paprocki
+Paquette
+Paquin
+Paracha
+Paradis
+ParadiseNTMVAA
+Parasiliti
+Pardi
+Parham
+Parihar
+Parikh
+Paris
+Parise
+Parisen
+Parisi
+Parisien
+Parkash
+Parker
+Parker-Shane
+Parkes
+Parkhill
+Parkin
+Parkins
+Parkinson
+Parks
+Parmaksezian
+Parman
+Parmar
+Parmenter
+Parmigiani
+Parn
+Parnell
+Parniani
+Parnigoni
+Parow
+Parr
+Parra
+Parrilli
+Parrillo
+Parrish
+Parrish-Bell
+Parrott
+Parsloe
+Parsons
+Partello
+Parthasarathy
+Partin
+Parton
+Partovi
+Parulekar
+Paryag
+Parypa
+Pascal
+Pascale
+Pascali
+Pascas
+Paschall
+Pasher
+Pashia
+Pashmineh
+Pasquale
+Passier
+Passin
+Pasternak
+Pastore
+Pastorek
+Pastuszok
+Pasvar
+Patacki
+Patchcor
+Patchett
+Patchor
+Patchsqa
+Patel
+Patenaude
+Paterson
+Patey
+Pathak
+Patner
+Patoka
+Paton
+Patoskie
+Patriarche
+Patrick
+Patrizio
+Patry
+Patte
+Patten
+Patterson
+Patteson
+Pattison
+Pattullo
+Patwardhan
+Pau
+Paul
+Paulett
+Pauley
+Paulhus
+Pauli
+Paulich
+Paulin
+Pauling
+Paulk
+Paulovics
+Paulus
+Paunins
+Pavitt
+Pavlic
+Pavlovic
+Pawelchuk
+Pawlikowski
+Pawliw
+Paye
+Payette
+Paylor
+Payn
+Payne
+Paynter
+Payton
+Pazos
+Pbx
+PcSupport
+Pde
+Pdesupport
+Peacemaker
+Peacocke
+Peake
+Pearce
+Pearcy
+Pearse
+Pearson
+Peart
+Peate
+Peaugh
+Peavoy
+Pecic
+Peckel
+Peckett
+Pedigo
+Pedley
+Pedneault
+Peebles
+Peedin
+Peerman
+Peers
+Peeters
+Peets
+Pegler
+Peirce
+Peiser
+Peixoto
+Peleato
+Pelissier
+Pelkie
+Pell
+Pelland
+Pellegrini
+Pelletier
+Pellizzari
+Pellizzeri
+Pelosi
+Pelot
+Pelton
+Peluso
+Pelz
+Pena
+Pena-Fernandez
+Pendergraft
+Pendergrass
+Pendharkar
+Pendleton
+Penfield
+Peng
+Penland
+Penn
+Pennell
+Penner
+Penney
+Pennington
+Peon
+Peoples
+Pepe
+Pepin
+Pepler
+Pepper
+Pepple
+Peptis
+Pera
+Peralta
+Perazzini
+Perchthold
+Percy
+Pereira
+Perenyi
+Perez
+Perfetti
+Pericak
+Perina
+Perkins
+Perkinson
+Perlmutter
+Pernell
+Perras
+Perrault
+Perreault
+Perrella
+Perrier
+Perrin
+Perrine
+Perron
+Perrotta
+Perry
+Perryman
+Persaud
+Perschke
+Persechino
+Personna
+Peschke
+Pesik
+Pesold
+Pestill
+Peter
+Peterman
+Peters
+Petersen
+Peterson
+Petillion
+Petras
+Petrea
+Petree
+Petrescu
+Petretta
+Petrey
+Petrick
+Petrie
+Petrinack
+Petro
+Petrovic
+Petrunewich
+Petrunka
+Petschenig
+Pettinger
+Pettitt
+Petzold
+Pevec
+Pevzner
+Pewitt
+Pezzoli
+Pezzoni
+Pezzullo
+Pfeffer
+Pfeilschifter
+Pfieffer
+Pfifferling
+Pfitzner
+Phagan
+Phair
+Phalen
+Phalpher
+Pham
+Phan
+Pharr
+Pharris
+Phelan
+Phelps
+Phifer
+Philbeck
+Philion
+Philip
+Philips
+Phillip
+Phillips
+Philp
+Phipps
+Phung
+Piasecki
+Piatt
+Picard
+Piche
+Pichocki
+Pickens
+Pickett
+Pickles
+Piecaitis
+Piecowye
+Piel
+Pien
+Piercey
+Piercy
+Pieron
+Pierosara
+Pieroway
+Pierre
+Pierret
+Piersol
+Pierson
+Pietromonaco
+Pietropaolo
+Pietrzak
+Piette
+Pifko
+Piggott
+Pighin
+Piitz
+Pilch
+Pilcher
+Pilip
+Pilipchuk
+Pilkington
+Pillars
+Pillman
+Pillsworth
+Pilmoor
+Pilon
+Pilote
+Pilotte
+Piltz
+Pimentel
+Pimiskern
+Pimpare
+Pinalez
+Pinchen
+Pinder
+Pindur
+Pineau
+Pinel
+Pinizzotto
+Pinkerton
+Pinnegar
+Pinney
+Pino
+Pinsonneault
+Pintado
+Pinto-Lobo
+Pintwala
+Piotto
+Piper
+Piperni
+Piperno
+Pipit
+Pipkins
+Pippin
+Pippy
+Piqueras
+Piraino
+Pirkey
+Pirkle
+Pisani
+Piske
+Pissot
+Pistilli
+Pitcairn
+Pitcavage
+Pitcher
+Pitre
+Pitt
+Pittam
+Pittges
+Pittman
+Pittner
+Pitton
+Pitts
+Pituley
+Piwkowski
+Piyasena
+Pizzanelli
+Pizzarello
+Pizzimenti
+Pkg
+Placido
+Plaisance
+Plaisant
+Plambeck
+Plamondon
+Planas
+Planche
+Plantamura
+Plante
+Planthara
+Planting
+Plaskie
+Plasse
+Plastina
+Plater-Zyberk
+Plato
+Platt
+Platthy
+Platts
+Playatuna
+Plenderleith
+Plett
+Pleydon
+Plmcoop
+Ploeg
+Ploof
+Plotter
+Plouffe
+Plourde
+Plsntp
+Plssup
+Plucinska
+Plummer
+Plyler
+Podlesna
+Podmaroff
+Podolski
+Poe
+Poettcker
+Poff
+Pohlmann
+Poindexter
+Pointner
+Poirier
+Poissant
+Pokinko
+Pokrifcak
+Pokrywa
+Pokusay
+Polak
+Polakowski
+Polanco
+Polashock
+Polder
+Poleretzky
+Poley
+Poliwoda
+Polk
+Pollack
+Pollinzi
+Pols
+Polsha
+Polson
+Poluchowska
+Polulack
+Poma
+Pomerleau
+Pomeroy
+Pommainville
+Pompeo
+Pomposelli
+Pon
+Ponthieux
+Poole
+Poon
+Poorman
+Popa
+Popadick
+Popel
+Popela
+Popescu
+Popieraitis
+Popoff
+Popovich
+Popovics
+Popowicz
+Popowycz
+Popp
+Popper
+Porebski
+Porecha
+Portelance
+Porter
+Porterfield
+Portigal
+Portwood
+Portz
+Posavad
+Pospisil
+Posta
+Postavsky
+Posthumus
+Postlethwaite
+Postolek
+Potesta
+Potocki
+Potter
+Pottle
+Potts
+Potvin
+Pouhyet
+Poulin
+Pouliot
+Poulos
+Poulsen
+Poulter
+Poustchi
+Powell
+Powers
+Pownall
+Powney
+Poyer
+Poyner
+Pozzi
+Pracht
+Prada
+Prado
+Praeuner
+Prakash
+Prang
+Prasad
+Prasada
+Prashad
+Prashaw
+Pratt
+Prattico
+Pravato
+Praysner
+Prchal
+Precoda
+Predel
+Predon
+Preece
+Preo
+Prescott
+Presgrove
+Presley
+Presner
+Presson
+Presti
+Prestia
+Prestipino
+Preston
+Preston-Thomas
+Prestrud
+Presutti
+Preuss
+Prevatt
+Preville
+Prevost
+Prewitt
+Prichard
+Prickett
+Pridgen
+Priede
+Priestley
+Prikkel
+Primeau
+Pringle
+Prints
+Printsupport
+Pritchard
+Privett
+Privitera
+Problems
+Probs
+Procaccio
+Procca
+Procner
+Procter
+Prodmfg
+Prodmgmt
+Production
+Proffit
+Prog
+Program-Office
+Projects
+Projofc
+Prokes
+Prokop
+Prokopenko
+Propes
+Prosise
+Prosperi
+Pross
+Prosyk
+Proudfoot
+Proulx
+Provencal
+Provencher
+Pruett
+Prunier
+Prupis
+Prybyla
+Prymack
+Pryor
+Prystie
+Pryszlak
+Przewlocki
+Przybycien
+Psklib
+Psutka
+Ptefs
+Puchala
+Puckett
+Puddington
+Pue-Gilchrist
+Puelma
+Puent
+Puett
+Puetz
+Pufpaff
+Pugh
+Puglia
+Pujara
+Pulcher
+Pulcine
+Pullan
+Pullum
+Pulver
+Pundyk
+Pung
+Purcell
+Purchasing
+Purdy
+Purgerson
+Purington
+Purnell
+Purohit
+Purson
+Puryear
+Pushelberg
+Pusztai
+Putman
+Putnam
+Puukila
+Pye
+Pyle
+Pyles
+Pyron
+Qadir
+Qadri
+Qainfo
+Qu
+Quan
+Quane
+Quante
+Quantrill
+Quartermain
+Quattrucci
+Quayle
+Quek
+Quelch
+Quenneville
+Querengesser
+Queries
+Quesnel
+Questell
+Quevillon
+Quigley
+Quilty
+Quinlan
+Quinn
+Quinones
+Quintana
+Quintero
+Quintin
+Quinz
+Quizmaster
+Quon
+Qureshi
+RK
+RTP
+Raab
+Raaflaub
+Rabadi
+Rabaglia
+Rabatich
+Rabecs
+Rabenstein
+Rabiasz
+Rabie
+Rabipour
+Rabjohn
+Rabon
+Rabzel
+Racette
+Racicot
+Racine
+Racioppi
+Racz
+Radcliffe
+Raddalgoda
+Radford
+Radick
+Radko
+Radojicic
+Radovnikovic
+Radulovich
+Radvanyi
+Rafael
+Rafek
+Rafferty
+Rafflin
+Rafol
+Rafter
+Raftery
+Ragan
+Ragbir
+Ragde
+Raghunath
+Ragland
+Raglin
+Ragsdale
+Raha
+Rahal
+Rahdar
+Rahimtoola
+Rahm
+Rahman
+Rahmani
+Rahmany
+Rahn
+Rahrer
+Raila
+Railey
+Raine
+Raines
+Rainey
+Raing
+Rains
+Rainsforth
+Raissian
+Raiswell
+Rajala
+Rajapakse
+Rajcher
+Rajchgod
+Rajchwald
+Rajczi
+Rajguru
+Rajwani
+Raker
+Rakesh
+Rakochy
+Rakotomalala
+Raley
+Ralph
+Rama
+Ramachandran
+Ramage
+Ramakesavan
+Ramakrishna
+Raman
+Ramanathan
+Ramaswamy
+Rambo
+Rambow
+Ramee
+Ramey
+Ramirez-Chavez
+Ramkissoon
+Ramlogan
+Ramnarine
+Ramondt
+Ramos
+Rampaul
+Ramroop
+Ramsaran
+Ramsay
+Ramsayer
+Ramsden
+Ramsey
+Ramseyer
+Ranahan
+Rance
+Rand
+Randall
+Randecker
+Randell
+Randhawa
+Randolph
+Raney
+Rangaswami
+Rangel
+Ranger
+Rangooni
+Ranieri
+Ranjan
+Rankin
+Rannells
+Ranoa
+Ranoska
+Rantala
+Rao
+Raphael
+Rappoport
+Rasberry
+Raschig
+Rashed
+Rashid
+Rashidi
+Rasmus
+Rasmussen
+Rassell
+Rastelli
+Rastogi
+Ratcliffe
+Rathbun
+Ratnam
+Ratnayake
+Rattray
+Ratz
+Rau
+Raud
+Rauen
+Rausa
+Rausch
+Raves
+Ravi
+Ravindranath
+Ravji
+Raxter
+Rayl
+Rayment
+Raymond
+Raynard
+Rayner
+Raynor
+Rch
+Rea
+Reade
+Readling
+Realtime
+Reaume
+Reaves
+Reavis
+Recabarren
+Receiving
+Reckhard
+Recktenwald
+Records
+Recsnik
+Recycling
+Reda
+Reddick
+Reddigan
+Redding
+Reddington
+Reddy
+Redfoot
+Redish
+Redman
+Redmond
+Redshaw
+Redway
+Reece
+Reeder
+Rees
+Reese
+Reetz
+Reeves
+Rega
+Regan
+Rege
+Regier
+Regimbald
+Register
+Regnier
+Rego
+Rehbein
+Rehder
+Rehel
+Reich
+Reichenbach
+Reichinger
+Reichman
+Reichow
+Reid
+Reidelberger
+Reifschneider
+Reijerkerk
+Reilly
+Reiman
+Reimann
+Reimbursement
+Reinboth
+Reinhold
+Reinink
+Reinke
+Reinlie
+Reinwald
+Reis
+Reiser
+Reiss
+Reist
+Reiter
+Reitfort
+Reith
+Reitlingshoefer
+Rekowski
+Relations
+Reller
+Rembecki
+Rembish
+Remedios
+Remers
+Remillard
+Remon
+Remrey
+Remson
+Renaud
+Rendon
+Reneau
+Renfro
+Renfroe
+Renken
+Rennie
+Reno
+Renton
+Renwick
+Repeta
+Reporting
+Reports
+Requests
+Research
+Resnick
+Ress
+Ressner
+Rester
+Restore
+Restrepo
+Results
+Retallack
+Rettie
+Reuben
+Reuss
+Reva
+Revah
+Revelle
+Revill
+Revis
+Rewitzer
+Rexroad
+Rey
+Reydman
+Reyes
+Reynolds
+Rezaian
+Rezansoff
+Reznechek
+Reznick
+Rezzik
+Rfa
+Rff
+Rhattigan
+Rheault
+Rheaume
+Rhew
+Rhoades
+Rhoads
+Rhodenizer
+Rhodes
+Rhyndress
+Rialland
+Ribakovs
+Ribaldo
+Ribi
+Ribot
+Riccitelli
+Ricciuto
+Richard
+Richards
+Richardson
+Richer
+Richlark
+Richman
+Richmond
+Rickard
+Rickborn
+Rickel
+Ricker
+Ricketson
+Ricketts
+Rickey
+Ricks
+Riddall
+Riddick
+Rider
+Ridgeway
+Ridgewell
+Ridgway
+Ridley
+Riebl
+Riedel
+Riehle
+Rigdon
+Riggins
+Riggs
+Riggsbee
+Rightmire
+Rigsbee
+Rigstad
+Rikley
+Riley
+Riml
+Rimmler
+Rimsa
+Rintala
+Rintoul
+Riopel
+Riopelle
+Riordan
+Rios
+Riou
+Rioux
+Ripa
+Ripley
+Risdal
+Risher
+Rishy-Maharaj
+Risko
+Risler
+Rist
+Risto
+Ritchey
+Ritchie
+Ritenour
+Rittenhouse
+Ritter
+Rittmann
+Ritz
+Ritza
+Ritzmann
+Riva
+Rivard
+Rivera
+Rivers
+Rivest
+Rix
+Rizewiski
+Rizk
+Rizzuti
+Robart
+Robb
+Robbins
+Roberge
+Roberson
+Robert
+Roberto
+Roberts
+Robertson
+Robieux
+Robillard
+Robins
+Robinson
+Robitaille
+Robles
+Robson
+Robustness
+Roch
+Roche
+Rochelle
+Rochon
+Rockley
+Roddick
+Roddy
+Rodely
+Roden
+Rodenfels
+Rodger
+Rodgers
+Rodi
+Rodkey
+Rodney
+Rodrigue
+Rodrigues
+Rodriguez
+Rodriques
+Rodriquez
+Rodschat
+Roehl
+Roehrig
+Roelofs
+Roerick
+Roesler
+Roeten
+Rogan
+Roger
+Rogers
+Rogge
+Rogne
+Rogness
+Rognlie
+Rohal
+Rohan
+Rohtert
+Roig
+Roithmaier
+Rojas
+Rok
+Rokas
+Roldan
+Roleson
+Rolfes
+Rolland
+Rollin
+Rollins
+Rollinson
+Rollo
+Rolls
+Rolnick
+Rolph
+Rolston
+Romagnino
+Roman
+Romanchuck
+Romano
+Romanowski
+Rombeek
+Romberg
+Rombough
+Romero
+Ronald
+Ronaldson
+Ronan
+Rondeau
+Roney
+Roob
+Roohy-Laleh
+Rooney
+Roots
+Roper
+Rorie
+Rosa
+Rosado
+Rosch
+Rosche
+Roscoe
+Roseland
+Rosenberg
+Rosenblum
+Rosenfeld
+Rosenquist
+Rosenthal
+Rosewell
+Rosien
+Rosko
+Rospars
+Ross
+Ross-Adams
+Ross-Ross
+Rossanese
+Rosser
+Rossi
+Rossignol
+Rosson
+Rosvick
+Roszko
+Rotenberg
+Roth
+Rothamel
+Rothey
+Rothwell
+Rotondo
+Rotzjean
+Rouer
+Rouleau
+Roulez
+Roundy
+Rourk
+Rourke
+Rous
+Rousseau
+Roussier
+Roussin
+Roussy
+Routhier
+Routing
+Rowan
+Rowatt
+Rowe
+Rowell
+Rowen
+Rowhani
+Rowland
+Rowlandson
+Rowley
+Rowsell
+Roy
+Royals
+Royer
+Royle
+Royster
+Rozen
+Rozier
+Rozon
+Rozumna
+Rtpbuild
+Rtprel
+Rtprelb
+Ruane
+Ruaud
+Ruban
+Ruben
+Rubin
+Rubinov
+Rubinstein
+Rubio
+Ruck
+Ruckman
+Rud
+Rudd
+Ruddell
+Ruddick
+Ruddle
+Rudiak
+Rudis
+Rudisill
+Rudzinski
+Rudzitis
+Ruel
+Ruest
+Ruffolo
+Rufino
+Ruigrok
+Ruiz
+Rumley
+Rummans
+Rummel
+Rummell
+Runciman
+Rundle
+Rundstein
+Runkel
+Runnels
+Running
+Runyon
+Rupert
+Rupnow
+Rupp
+Ruppert
+Ruprecht
+Rusch
+Ruschmeier
+Rushing
+Rushton
+Rusin
+Russell
+Russett
+Ruth
+Ruthart
+Rutherford
+Rutland
+Rutledge
+Rutt
+Ruttan
+Rutter
+Rutulis
+Rutyna
+Ruyant
+Ruzicka
+Ruzycki
+Ryall
+Ryals
+Ryan
+Rybczynski
+Rychlicki
+Ryde
+Ryder
+Rydhan
+Ryerson
+Rygwalski
+Rykwalder
+Rylott
+Rymkiewicz
+Rynders
+Rynties
+Rzepczynski
+SHOP
+SOS
+SUPPORT
+SVM-BNRMTVA
+SVM-BNRMTVB
+SWR
+SYS
+SYSINT
+Saad
+Saatcioglu
+Sabadash
+Sabat
+Sabatini
+Sabbagh
+Saberi
+Sabety
+Sabo
+Sabol
+Saboorian
+Sabourin
+Sabri
+Sabry
+Sacarello
+Sacchetti
+Sadeghi
+Sadler
+Sadorra
+Sadri
+Sadroudine
+Saed
+Safah
+Sagan
+Sager
+Sagris
+Saha
+Sahinalp
+Sahli
+Saidzadeh
+Saifullah
+Saikaley
+Sails
+Saini
+Sainsbury
+Saisho
+Saito
+Saiyed
+Sakaguchi
+Sakauye
+Sakus
+Salada
+Saladna
+Salam
+Salamon
+Salapek
+Salazar
+Salb
+Saleh
+Salehi
+Salem
+Salembier
+Salemi
+Sales
+Salgado
+Salhany
+Salibi
+Salim-Yasuda
+Salimi
+Salinas
+Salkilld
+Salkini
+Salkok
+Salladay
+Sallee
+Salomon
+Salsbery
+Saltamartini
+Salter
+Saltsider
+Salva
+Salvato
+Salvin
+Salyer
+Salyniuk
+Salzillo
+Samac
+Samalot
+Samaroo
+Sambi
+Samhaber
+Samieian
+Sammon
+Sammons
+Samora
+Sampaleanu
+Sampat
+Sampson
+Sampson-Cobb
+Samson
+Samsonenko
+Samuel
+Sanabria
+Sanborn
+Sanche
+Sanchez
+Sandberg
+Sandell
+Sanders
+Sanderson
+Sandford
+Sandhar
+Sandhu
+Sandiford
+Sandison
+Sandler
+Sandlford
+Sandner
+Sandness
+Sandre
+Sandrock
+Sanford
+Sanford-Wright
+Sangha
+Sanh
+Sanity
+Sankey
+Sanks
+Sanoy
+Sanramon
+Sanschagrin
+Sansom
+Sanson
+Santabarbara
+Santella
+Santi
+Santiago
+Santitoro
+Santos
+Sanzone
+Sapena
+Saran
+Saran-Brar
+Saravanos
+Sarbutt
+Sargent
+Sargeson
+Sarin
+Sarioglu
+Sarkari
+Sarlos
+Sarma
+Sarna
+Sarrasin
+Sarrazin
+Sarson
+Sartin
+Sarto
+Sartor
+Sarubbi
+Sasaki
+Sasore
+Sassine
+Sasson
+Sastry
+Sathe
+Satkamp
+Satkunaseelan
+Sato
+Satta
+Satterfield
+Sattler
+Satya
+Saucerman
+Sauck
+Sauder
+Sauer
+Saul
+Saulnier
+Sauls
+Saumure
+Saundercook
+Saunders
+Saunderson
+Saungikar
+Sauvageau
+Sauve
+Savadkouhi
+Savanh
+Savard
+Savarimuthu
+Savaryego
+Savino
+Savoie
+Sawada
+Saward
+Sawaya
+Sawchuk
+Sawczyn
+Sawyers
+Saxena
+Sayar
+Sayed
+Sayegh
+Sayer
+Sayre
+Scalabrini
+Scalera
+Scales
+Scammerhorn
+Scamurra
+Scandrett
+Scanga
+Scanlan
+Scanlon
+Scapin
+Scarborough
+Scarbrough
+Scarffe
+Scarlett
+Scarrow
+Scatena
+Scates
+Schacham
+Schachtler
+Schack
+Schadan
+Schaefer
+SchaeferNTMVAA
+Schaefers
+Schafer
+Schaffel
+Schallenberg
+Schaller
+Schanck
+Schanne
+Scharf
+Schartmann
+Schatzberg
+Schauer
+Schavo
+Schavone
+Scheck
+Schecter
+Schedulers
+Scheduling
+Scheer
+Scheffler
+Scheible
+Scheidt
+Scheifele
+Schejbal
+Schell
+Schellenberger
+Schembri
+Schemena
+Schenck
+Schenk
+Schenkel
+Schepps
+Scherbinsky
+Scherer
+Scherzinger
+Schesvold
+Scheuermann
+Schick
+Schieber
+Schiefer
+Schiegl
+Schierbaum
+Schill
+Schiller
+Schilling
+Schiltz
+Schinkel
+Schipper
+Schirmer
+Schirtzinger
+Schissel
+Schittl
+Schlachter
+Schlagenhauf
+Schlange
+Schledwitz
+Schlegel
+Schlemmer
+Schlichtherle
+Schlichting
+Schlobohm
+Schluter
+Schmadtke
+Schmeing
+Schmeler
+Schmelzel
+Schmidt
+Schmitigal
+SchmitigalNTMVAA
+Schmitt
+Schmitz
+Schmoe
+Schnackenberg
+Schnaithman
+Schneider
+Schneiders
+Schnell
+Schnirer
+Schnob
+Schnupp
+Schnurmann
+Schober
+Schoch
+Schoen
+Schoenermarck
+Schoenling
+Schofield
+Scholes
+Scholey
+Scholman
+Scholtz
+Schonberger
+Schooley
+Schousboe
+Schrader
+Schrage
+Schram
+Schraner
+Schrang
+Schreiber
+Schreier
+Schreiner
+Schrier
+Schroeder
+Schroer
+Schroff
+Schryburt
+Schubert
+Schuck
+Schuddeboom
+Schuett
+Schuette
+Schuld
+Schulte
+Schultz
+Schultze
+Schulze
+Schumacher
+Schumann
+Schuster
+Schute
+Schutte
+Schutz
+Schvan
+Schwab
+Schwaderer
+Schwalbach
+Schwane
+Schwantes
+Schwartz
+Schwarz
+Schwenk
+Schyving
+Scibek
+Scissons
+Scodras
+Scomello
+Sconzo
+Scorziello
+Scott
+Scotti
+Scourneau
+Scovell
+Scovill
+Scp
+Scrantom
+Scrbacic
+Screener
+Scribner
+Scrivens
+Scroger
+Scss
+Scssdev
+Scully
+Scurlock
+Seabrook
+Seager
+Seagle
+Seagraves
+Seagroves
+Seale
+Sealy
+Seamster
+Searl
+Searles
+Sears
+Seatter
+Seawell
+Seay
+Sebastian
+Sebastien
+Sebeh
+Secrest
+Security
+Seddigh
+Sedran
+Seeds
+Seegobin
+Seelaender
+Seeler
+Seeley
+Seery
+Sees
+Seetharaman
+Seggie
+Seguin
+Sehgal
+Sehinson
+Sehmbey
+Sei
+Seidl
+Seidman
+Seifers
+Seifert
+Seifried
+Seiler
+Seiple
+Seitz
+Seiz
+Sej
+Selbrede
+Selby
+Selchow
+Selent
+Selic
+Seliske
+Selisker
+Selkirk
+Sellars
+Sellers
+Sells
+Sellwood
+Sembi
+Semeniuk
+Semler
+Semmens
+Sendyk
+Senecal
+Senese
+Sengoba
+Sengupta
+Seniuk
+Senten
+Sentner
+Senyildiz
+Senyshyn
+Sepe
+Sepko
+Serack
+Serapin
+Serber
+Serbin
+Serbus
+Seregelyi
+Sergent
+Sergi
+Sergo
+Seroka
+Serour
+Serraf
+Serrano
+Serre
+Servais
+Servance
+Services
+Servidio
+Serville
+Seshadri
+Sethi
+Sethian
+Setiawan
+Seto
+Settels
+Setterfield
+Settles
+Seufert
+Severin
+Severinac
+Severn
+Severns
+Sevigny
+Sevilla
+Sewell
+Seymour
+Sezer
+Sfiroudis
+Sgornikov
+Sguigna
+Shabatura
+Shabo
+Shackelford
+Shackleford
+Shackleton
+Shackley
+Shaddock
+Shafer
+Shaffer
+Shafik
+Shahen
+Shahroodi
+Shakib
+Shalla
+Shalmon
+Shamblin
+Shames
+Shamji
+Shamshiri
+Shan
+Shanahan
+Shane
+Shaner
+Shang
+Shankar
+Shannon
+Shantz
+Shao
+Shapcott
+Shapin
+Shapiro
+Shapland
+Shariff
+Sharkey
+Sharma
+Sharman
+Sharpe
+Sharratt
+Shastri
+Shastry
+Shattuck
+Shaughnessy
+Shaver
+Shayanpour
+Shea
+Sheaffer
+Shealy
+Shearer
+Shearin
+Shearman
+Shears
+Shechtman
+Shedd
+Sheehan
+Sheergar
+Sheets
+Sheffey
+Sheffield
+Sheidafar
+Sheikh
+Shein
+Shek
+Shelby
+Sheldon
+Shelegey
+Shelley
+Shellman
+Shelton
+Shemwell
+Shen
+Shennan
+Shepard
+Sheppard
+Sherali
+Sherard
+Sherban
+Shere
+Sheremeto
+Sheridan
+Sherif
+Sherk
+Sherlock
+Sherman
+Sherow
+Sherrard
+Sherrell
+Sherrer
+Sherrill
+Sherwin
+Sheth
+Sheu
+Shew
+Shewchenko
+Shi
+Shibahara
+Shibata
+Shibberu
+Shieff
+Shiel
+Shields
+Shiell
+Shier
+Shiffer
+Shiflett
+Shigemura
+Shiley
+Shillinger
+Shillingford
+Shimandle
+Shinder
+Shingler
+Shinjo
+Shipe
+Shipp
+Shippen
+Shirai
+Shireman
+Shirey
+Shirinlou
+Shirley
+Shishakly
+Shishido
+Shiu
+Shiue
+Shivcharan
+Shivnan
+Shnay
+Shnider
+Shoaf
+Shoemaker
+Shonka
+Shonuck
+Shorgan
+Shostak
+Shoulars
+Shoun
+Showers
+Shreve
+Shrieves
+Shtivelman
+Shtulman
+Shu
+Shubaly
+Shue
+Shukor
+Shukster
+Shuler
+Shull
+Shultz
+Shum
+Shuman
+Shumate
+Shunmugam
+Shupe
+Shurtleff
+Shuster
+Shute
+Shutler
+Shwed
+Shylo
+Shypski
+Shyu
+Sibbet
+Sibiga
+Sibincic
+Sicard
+Sich
+Sickler
+Sicotte
+Siddall
+Siddell
+Siddiqui
+Sides
+Sidhu
+Sidor
+Sidorovsky
+Sieben
+Sieber
+Siefert
+Siegel
+Siegle
+Siehl
+Sieling
+Siemens
+Siemer
+Siew
+Siewert
+Sigda
+Sigmon
+Sigurdson
+Sigut
+Sikes
+Sikri
+Silang
+Silburt
+Silgardo
+Silianu
+Silieff
+Silins
+Sills
+Sils
+Silva
+Silverman
+Silverstone
+Silverthorn
+Silvester
+Silvestri
+Silwer
+Sim
+Simanskis
+Simard
+Simard-Normandin
+Simcoe
+Simcox
+Simeone
+Simhan
+Simkin
+Simmonds
+Simmons
+Simms
+Simon
+Simonovich
+Simons
+Simonsen
+Simpkin
+Simpson
+Sims
+Simser
+Simson
+Simzer
+Sinanan
+Sinasac
+Sinchak
+Sinclair
+Singbeil
+Singer
+Singh
+Singhal
+Singham
+Sinha
+Sinkfield
+Sinkovits
+Sinnett
+Sinnott
+Sinoyannis
+Sinyor
+Siomalas
+Siperco
+Sipple
+Sirevicius
+Sisk
+Sist
+Sitar
+Sitch
+Sitler
+Siu
+Siu-Kwok
+Sivaji
+Sivasothy
+Sizto
+Sjerps
+Skaff
+Skaftason
+Skaggs
+Skalski
+Skanes
+Skaret
+Skedelsky
+Skelly
+Skene
+Skeoch
+Skerlak
+Skerry
+Skeuse
+Skiba
+Skiclub
+Skillen
+Skillman
+Skinner
+Skoberne
+Skof
+Skopliak
+Skrebels
+Skrobanski
+Skrobecki
+Skruber
+Skuce
+Skwarok
+Slaa
+Slabaugh
+Slaby
+Slade
+Sladek
+Slagel
+Slattery
+Slautterback
+Slavin
+Sldisk
+Sleeman
+Sleeth
+Slempers
+Slinkard
+Slinowsky
+Sliter
+Sloan
+Sloane
+Sloboda
+Slobodian
+Slobodrian
+Slonosky
+Slotnick
+Slozil
+Slunder
+Slusser
+Slyteris
+Smale
+Smallwood
+Smecca
+Smedema
+Smeenk
+Smelters
+Smerdell
+Smerek
+Smid
+Smit
+Smith
+Smithdeal
+Smithson
+Smits
+Smolin
+Smook
+Smoot
+Smothers
+Smrke
+Smrke-Surbey
+Smuda
+Smulders
+Smyth
+Smythe
+Snair
+Snapper
+Snarr
+Snead
+Snedden
+Snedeker
+Snelgrove
+Snelling
+Snider
+Snipes
+Snodgrass
+Snoke
+Snuggs
+Snyder
+Soard
+Sobchuk
+Sobczak
+Sobeck
+Sobel
+Sobiesiak
+Sobkow
+Sobolak
+Sobolewski
+Sobon
+Sochovka
+Sodano
+Sodhi
+Soh
+Sohal
+Sohni
+Sohns
+Sohota
+Soin
+Sojka
+Sojkowski
+Sokolowski
+Sokyrko
+Soldera
+Solheim
+Soliman
+Solkoff
+Sollee
+Sollman
+Solman
+Solodko
+Solomon
+Solski
+Somani
+Somera
+Somers
+Somerville
+Somisetty
+Sommer
+Sommerdorf
+Sommerfeld
+Sompong
+Somppi
+SonHing
+Sonier
+Sonne
+Sonoda
+Soo
+Sood
+Sookdeo
+Sooley
+Soong
+Sorathia
+Soreanu
+Sorensen
+Soriano
+Sorrell
+Sorrentino
+Sosa
+Sossaman
+Sotelo
+Sotiriadis
+Soto
+Souba
+Soucie
+Soucy
+Soules
+Soulliere
+Soumis
+Sourisseau
+Sourour
+Sousa
+Soussa
+Souther
+Southon
+Southworth
+Souza
+Sova
+Soweidan
+Sowry
+Soyland
+Soyster
+Spallin
+Spann
+Spannbauer
+Sparacio
+Sparkes
+Sparks
+Sparksman
+Spaugh
+Speaker
+Spearman
+Spearpoint
+Spears
+Specs
+Speer
+Speers
+Speight
+Spejewski
+Spence
+Spencer
+Spessot
+Spicer
+Spieker
+Spilchak
+Spillane
+Spindler
+Spinelli
+Spingola
+Spinks
+Spisak
+Spitzer
+Splitt
+Spohn
+Spolar
+Sponagle
+Sponchia
+Spooner
+Spragg
+Spraggins
+Sprayberry
+Spriggs
+Sprigings
+SpringThorpe
+Sprott
+Sproule
+Spruell
+Sprules
+Sprunger
+Spurlin
+Spurlock
+Spurway
+Squires
+Squizzato
+Sreedhar
+Sridaran
+Sridhar
+Srikrishna
+Srinivasan
+Srivastava
+Sroczynski
+St
+St-Amour
+St-Denis
+St-Louis
+St-Martin
+St-Onge
+St-Pierre
+St.
+St.Clair-Holmes
+St.Denis
+St.Germain
+St.Jacques
+St.John
+St.Laurent
+St.Louis
+St.Pierre
+St.Vil
+StJohn
+StJames
+Staats
+Stability
+Stacey
+Stach
+Stachowiak
+Stackpole
+Stadelmeier
+Stadler
+Staffeld
+Staffing
+Staggs
+Stagmier
+Stahl
+Stahly
+Stainback
+Stallabrass
+Stallcup
+Stallings
+Stalter
+Stambouli
+Stamboulie
+Stampfl
+Stampley
+Stamps
+Stanciu
+Standards
+Standel
+Standen
+Standrin
+Stanfield
+Stanke
+Stanley
+Stansbury
+Stansby
+Stansell
+Stansfield
+Stanton
+Stanulis
+Stapenhorst
+Staples
+Staring
+Starkauskas
+Starkebaum
+Starkes
+Starks
+Starnes
+Starowicz
+Starr
+Stars
+Starsdps
+Starzman
+Stasaski
+Stasiak
+Stasney
+Stastny
+Stasyszyn
+Stat
+Staten
+Statile
+Staton
+Stayton
+Stctest
+Stds
+Ste
+Ste-Marie
+Steckley
+Steede
+Steele
+Steelman
+Steen
+Steenburg
+Steene
+Steeves
+Stefanac
+Steffens
+Steffes
+Steffey
+Steffy
+Stegall
+Stegman
+Stegmueller
+Steiert
+Steinbacher
+Steinberg
+Steinhart
+Steip
+Stejskal
+Steklasa
+Stelcner
+Stellitano
+Stellwag
+Stemmler
+Stender
+Stennett
+Stenson
+Stensrud
+Stepchuk
+Stephans
+Stephen
+Stephens
+Stephenson
+Stepler
+Stepp
+Steranka
+Stercyk
+Sterczyk
+Sterescu
+Sterian
+Stern
+Steski
+Stetner
+Stetter
+Stevanovic
+Steven
+Stevens
+Stevenson
+Stewart
+Stewart-Guthrie
+Stickland
+Stiglitz
+Stiles
+Stillwell
+Stinson
+Stinziani
+Stirling
+Stirrett
+Stites
+Stocker
+Stocks
+Stockwell
+Stodart
+Stoddard
+Stoffels
+Stokes
+Stokker
+Stokoe
+Stokoski
+Stoner
+Stonos
+Stooke
+Storace
+Storey
+Storrie
+Stotts
+Stotz
+Stouder
+Stough
+Stovall
+Stover
+Stowe
+Stoyles
+Strachan
+Strackholder
+Strader
+Strandberg
+Strandlund
+Strannemar
+Strasser
+Stratton
+Straub
+Strauch
+Strauss
+Strautman
+Strawczynski
+Strayhorn
+Streater
+Streatfield
+Streibel
+Streight
+Streng
+Strickland
+Strider
+Strober
+Strock
+Stroemer
+Strohmeyer
+Stronski
+Stropp
+Stroud
+Stroupe
+Strube
+Struble
+Strucchelli
+Strudwick
+Struzynski
+Stuart
+Stubblefield
+Stubbs
+Stuckey
+Studer
+Stults
+Stumpf
+Sturdivant
+Sture
+Sturrock
+Stutts
+Su
+Subasinghe
+Subick
+Subissati
+Subramanian
+Subsara
+Suchocki
+Suda
+Sudan
+Sudbey
+Sudbury
+Suddarth
+Suen
+Suer
+Suess
+Sufcak
+Suffern
+Sugandi
+Sugarbroad
+Sugarman
+Sugihara
+Suh
+Suharly
+Suitt
+Sulatycki
+Sulewski
+Sullivan
+Sumi
+Summach
+Summerlin
+Summers
+Sumner
+Sumpter
+Sunatori
+Sundar
+Sundaram
+Sunderland
+Sunstrum
+Suomela
+Supervisor
+Suprick
+Sura
+Surazski
+Surber
+Surowaniec
+Surray
+Surreau
+Surridge
+Sursal
+Susanto
+Susick
+Sutarwala
+Sutcliffe
+Suter
+Sutera
+Sutherland
+Suthers
+Sutphen
+Sutter
+Sutterfield
+Sutterlin
+Sutton
+Suwala
+Suwanawongse
+Suyama
+Suykens
+Suzuki
+Svalesen
+Svilans
+Svo
+Swact
+Swaden
+Swails
+Swaine
+Swann
+Swanson
+Swanston
+Swartz
+Swazey
+Swearingen
+Sweeney
+Sweetnam
+Swepston
+Swiat
+Swiatkowski
+Swick
+Swiderski
+Swinamer
+Swinburne
+Swinkels
+Swinks
+Swinney
+Swinson
+Swinwood
+Swisher
+Switching
+Switzer
+Swope
+Swyer
+Sy
+Syal
+Sydnor
+Sydor
+Sydoryk
+Syed
+Sykes
+Sylvain
+Sylvester
+Sylvestre
+Sylvie
+Sym
+Symons
+Synness
+Syposz
+Syres
+Syrett
+Sysadmin
+Syssupport
+Systems
+Systest
+Szabo
+Szamosi
+Szaplonczay
+Szaran
+Szeto
+Sziladi
+Szkarlat
+Szopinski
+Szot
+Szpakowski
+Szpilfogel
+Sztein
+Szuminski
+Szura
+Szymanski
+Szypulski
+TAS
+TRIAL
+Ta
+Tabaja
+Tabalba
+Tabbert
+Tabl
+Tabler
+Tables
+Tabor
+Tac
+Taddio
+Tadevich
+Tadge
+Tadlock
+Taggart
+Taghizadeh
+Tahamont
+Taharuddin
+Taheri
+Tahir
+Tai
+Tait
+Tajbakhsh
+Takagi
+Takahashi
+Takashima
+Takata
+Takefman
+Takeuchi
+Taki
+Takiyanagi
+Talbert
+Talbot
+Talbott
+Talevi
+Talis
+Tallett
+Talmont
+Talton
+Tamarelli
+Tamasi
+Tammaro
+Tamrazian
+Tamura
+Tan-atichat
+Tanaka
+Tanchak
+Tancordo
+Tandberg
+Tandiono
+Tanferna
+Tanglao
+Tangren
+Tanio
+Tanir
+Tanner
+Tanniere
+Tapner
+Tapp
+Tappenden
+Tappert
+Tapsell
+Taralp
+Tarant
+Taranto
+Taraschuk
+Tarasewicz
+Tardif
+Tardiff
+Tardioli
+Taren
+Targosky
+Tariq
+Tarlamis
+Tarle
+Tarnai
+Tarof
+Tarquinio
+Tarrant
+Tarsky
+Tarver
+Taschereau
+TaskForce
+Taspatch
+Tassi
+Tasso
+Tassy
+Tasuk
+Tatangsurja
+Tatar
+Tatemichi
+Tates
+Tatsdocn
+Tattenbaum
+Tatum
+Tauberg
+Taul
+Tauscher
+Tavana
+Tavares
+Taverner
+Tawfik
+Tay
+Taylor
+Taylor-Hudson
+Tchir
+Td
+Teacher
+Teague
+Teasley
+Tebbe
+Tebinka
+Technosoft
+Tecklenburg
+Teder
+Tedrick
+Teed
+Teerdhala
+Teh
+Teichman
+Teitelbaum
+Tejada
+Tejani
+Tel
+Telco
+Telecom
+Telesis
+Telex
+Telfer
+Telidis
+Telke
+Tello
+Tellup
+Telos
+Temp
+Temple-Downing
+Templeton
+Tena
+Tencer
+Teniola
+Tennant
+Teo
+Terakado
+Teran
+Terminals
+Terneus
+Terrade
+Terranella
+Terranova
+Terrel
+Terrell
+Terwey
+Terwilligar
+Terzian
+Tesch
+Tesfamariam
+Tesh
+Tessier
+Tessler
+TestNTMVAA
+TestNTMVAB
+Testa
+Testagc
+Tester
+Testing
+TestingPOSTTEST
+Testsds
+Testtools
+Tetrault
+Tetreault
+Teufel
+Tevlin
+Thabet
+Thacker
+Thain
+Thaker
+Thakur
+Tham
+Thames
+Tharby
+Tharrington
+Thaxton
+Thayer
+Thedford
+Theis
+Theloosen
+Themann
+Theocharakis
+Theodore
+Theoret
+Theriault
+Therien
+Theriot
+Therrien
+Thevenard
+Thevenart
+Thibault
+Thibeault
+Thibert
+Thibodeaux
+Thibon
+Thieken
+Thiel
+Thierry
+Thifault
+Thill
+Thirugnanam
+Thisdel
+Thom
+Thomaier
+Thomas
+Thomason
+Thomassin
+Thomey
+Thomlinson
+Thompson
+Thoms
+Thomsen
+Thomson
+Thorley
+Thorman
+Thornber
+Thornburg
+Thorne
+Thornley
+Thornton
+Thorpe
+Thorsen
+Thorslund
+Thorson
+Threader
+Throgmorton
+Thuesen
+Thum
+Thumm
+Thurgood
+Thurley
+Thurman
+Thurston
+Thuswaldner
+Tiberghien
+Tibi
+Tice
+Ticzon
+Tidball
+Tidd
+Tiedemann
+Tiegs
+Tien
+Tierney
+Tieu
+Tigg
+Tihanyi
+Tilbenny
+Tilden
+Tilk
+Tille
+Tiller
+Tilley
+Tillman
+Tilmon
+Tilson
+Tilton
+Timesheet
+Timleck
+Timler
+Timm
+Timmer
+Timmerman
+Timmins
+Timmons
+Timms
+Timpson
+Tims
+Timsit
+Timtschenko
+Tiner
+Ting
+Tinney
+Tintor
+Tio
+Tippett
+Tipping
+Tipton
+Tischhauser
+Tischler
+Tisdale
+Tisdall
+Tiseo
+Titus
+Tiu
+Tiwari
+Tjia
+ToDo
+Toastmasters
+Tobias
+Tod
+Todaro
+Todd
+Todloski
+Todorovic
+Toews
+Togasaki
+Tognoni
+Tohama
+Tolar
+Toles
+Toletzka
+Tolgyessy
+Tolle
+Tolson
+Toly
+Tom
+Tomacic
+Tomar
+Tomasetti
+Tomassi
+Tomaszewska
+Tombul
+Tomes
+Tomlin
+Tomlinson
+Tompkins
+Tonelli
+Tonkovich
+Tookey
+Toole
+Tooley
+Toolroom
+Tools
+Toolset
+Toomer
+Toone
+Toop
+Toothman
+Tooyserkani
+Toperzer
+Topgun
+Toplis
+Topol
+Topp
+Torain
+Torbert
+Tordocs
+Torian
+Tornes
+Tornqvist
+Torok
+Torrance
+Torrealba
+Torrell
+Torrens
+Torres
+Toscano
+Tosczak
+Toshach
+ToshachNTMVAA
+Tostenson
+Tota
+Totaro
+Toth
+Totino
+Totman
+Totten
+Totti
+Touchette
+Tougas
+Toulson
+Tousignant
+Towaij
+Towers
+Towill
+Towler
+Towles
+Townley
+Towns
+Townsel
+Townsend
+Townson
+Towsley
+Toyooka
+Tracey
+Tracey-McCabe
+Trachsel
+Tracz
+Trader
+Trafford
+Trahan
+Trainer
+Training
+Trainor
+Trame
+Tran
+Tranfaglia
+Translations
+Tranter
+Trasmundi
+Traugott
+Traulich
+Traut
+Trautman
+Travers
+Travis
+Traxler
+Trayer
+Traylor
+Traynor
+Tredway
+Treen
+Trefry
+Trefts
+Tregenza
+Tremaine
+Tremblay
+Tremewan
+Trent
+Trentadue
+Tres-Support
+Trese
+Tresrch
+Trevethan
+Trevitt
+Trickett
+Trieu
+Trif
+Trifiro
+Triggiano
+Trimble
+Trinh
+Trinidad
+Trink
+Tripier
+Triplehorn
+Tripp
+Tripps
+Trisic
+Trisko
+Trittler
+Tritton
+Trivedi
+Trocchi
+Trochu
+Troesch
+Trojak
+Tromm
+Tropea
+Tropeano
+Trotsky
+Trotter
+Trottier
+Trouborst
+Troup
+Trowbridge
+Trpisovsky
+Truchon
+Trudel
+Trudell
+Truelove
+Truesdale
+Truffer
+Trull
+Trumble
+Trunley
+Truong
+Truran
+Trussler
+Trutschel
+Tsai
+Tsakalis
+Tsalikis
+Tsang
+Tsao
+Tschaja
+Tse
+Tseng
+Tsenter
+Tsiakas
+Tsitsior
+Tsitsonsis
+Tso
+Tsonos
+Tsortos
+Tsui
+Tsuji
+Tsuk
+Tsunoda
+Ttisupport
+Tu
+Tuan
+Tubb
+Tucker
+Tudo
+Tue
+Tuen
+Tufford
+Tulk
+Tull
+Tullius
+Tullo
+Tully
+Tunali
+Tunon
+Tuok
+Tupas
+Tupling
+Turbes
+Turbyfill
+Turchan
+Turcot
+Turcotte
+Turkki
+Turkovic
+Turnbull
+Turner
+Turney
+Turpin
+Turrubiarte
+Turunen
+Tussey
+Tutt
+Tuttle
+Tuxford
+Twa
+Twarog
+Tweddle
+Twidale
+Twiss
+Twitty
+Twolan
+Twx
+Twyman
+Twynham
+Twyver
+Tyda
+Tye
+Tyler
+Tymchuk
+Tynan
+Tyndall
+Tyner
+Typer
+Tyrance
+Tyroler
+Tyrrell
+Tyson
+Tzaneteas
+Tzuang
+US
+USER
+Uae
+Uberig
+Uchida
+Uchiyama
+Udall
+Udayasekaran
+Ude
+Uecker
+Ueda
+Ueyama
+Uffner
+Ufomadu
+Uguccioni
+Ugwa
+Uhl
+Uhlhorn
+Uhlig
+Ukena
+Ulgen
+Ully
+Ulrich
+Uludamar
+Umeeda
+Umeh
+Umetsu
+Umphres
+Underwood
+Unger
+Unitt
+Unix
+Unixsupport
+Unkefer
+Unkles
+Unsoy
+Unxlb
+Upchurch
+Updt
+Uppal
+Upshaw
+Upton
+Urbanowich
+Urbick
+Urbielewicz
+Urbshas
+Urnes
+Urow
+Urquhart
+Urwin
+Usrouter
+Usyk
+Utas
+Utsumi
+Uyar
+VMXA
+VO
+Vacher
+Vachon
+Vaculik
+Vadala
+Vafaie
+Vaglio-Laurin
+Vahary
+Vahdat
+Vaid
+Vaillancourt
+Vaillant
+Vairavan
+Vajentic
+Vakili
+Valcourt
+Valdez
+Valenta
+Valente
+Valentik
+Valentin
+Valenziano
+Valerien
+Valerio
+Valerius
+Valia
+Valin
+Valiquette
+Valiveti
+Valko
+Vallee
+Vallentyne
+Vallet
+Valliani
+Valliere
+Vallieres
+Vallozzi
+Valois
+Valvasori
+Van Alphen
+Van Alstine
+Van Atta
+Van Bakel
+Van Benthem
+Van Cooney
+Van Den
+Van Der
+Van Dyke
+Van Es
+Van Eyk
+Van Fleet
+Van Gaal
+Van Haste
+Van Holst
+Van Hoy
+Van Hulst
+Van Kast
+Van Kessel
+Van Klink
+Van Laten
+Van Loon
+Van Mansum
+Van Meter
+Van Neste
+Van Oorschot
+Van Orden
+Van Phil
+Van Rijn
+Van Rijswijk
+Van Schouwen
+Van Schyndel
+Van Sickle
+Van Terrie
+Van Veen
+Van Vrouwerff
+Van Weringh
+VanDenKieboom
+VanDerBoom
+VanDommele
+VanGastel
+VanLaar
+VanPatten
+VanStaalduinen
+Vanaman
+Vanasse
+Vance
+Vandagriff
+VandenHeuvel
+Vandenberg
+Vandenborre
+Vandenheede
+VanderVeen
+Vanderburg
+Vandergeest
+Vanderhelm
+Vanderheyden
+Vanderhoeven
+Vanderhooft
+Vanderpol
+Vandervelde
+Vanderwel
+Vandevalk
+Vandeven
+Vandewouw
+Vandoorne
+Vandusen
+Vankooten
+Vanliew
+Vanstory
+Vanta
+Vanwormhoudt
+Vanwychen
+Vanzella
+Varady
+Varano
+Vardy
+Vargas
+Vargo
+Varia
+Varkel
+Varkey
+Varley
+Varmazis
+Varsava
+Vartanesian
+Varughese
+Vasarhelyi
+Vaserfirer
+Vasil
+Vasile
+Vasiliadis
+Vasilopoulos
+Vastine
+Vasudeva
+Vaters
+Vaughan
+Vaughn
+Vavroch
+Vawter
+Vea
+Veale
+Veals
+Veck
+Veedell
+Vega
+Vehling
+Veillette
+Veilleux
+Veit
+Veksler
+Vela
+Velasquez
+Vele
+Vella
+Vellino
+Veloria
+Veloz
+Velsher
+Vempati
+Vendette
+Veness
+Venguswamy
+Venier
+Venjohn
+Venne
+Venning
+Vennos
+Ventrone
+Ventura
+Venturini
+Verardi
+Verch
+Verdonselli
+Verheyden
+Verhoeven
+Verhotz
+Verification
+Verkroost
+Verma
+Vermeesch
+Vermette
+Vernon
+Verreau
+Verrenneau
+Verrilli
+Versace
+Versteeg
+Vertolli
+Verville
+Verzilli
+Veselko
+Vesterdal
+Vetil
+Vetrano
+Vetrie
+Vetter
+Vettese
+Vexler
+Vey
+Veyrat
+Vezeau
+Vezina
+Viano
+Viau
+Vick
+Vickers
+Videa
+Vidmer
+Viduya
+Viegas
+Vieger
+Viehweg
+Vieillard-Baron
+Vieira
+Vieiro
+Viens
+Vieregge
+Vigeant
+Viitaniemi
+Vilayil
+Vilhan
+Vilis
+Villanueva
+Villella
+Villeneuve
+Vilmansen
+Vilozny
+Vinas
+Vincent
+Vinet
+Viney
+Vinnell
+Vinson
+Virani
+Virant
+Virchick
+Virgoe
+Viriato
+Virk
+Visentin
+Vish
+Visockis
+Visser
+Visvanatha
+Vitacco
+Vitaglian
+Vivian
+Vivier
+Vlad
+Vlahos
+Vmsupport
+Voadmin
+Vodicka
+Voduc
+Voelcker
+Vogel
+Vogt
+Voight
+Voitel
+Volchegursky
+Volfe
+Volk
+Volker
+Volkmann
+Volkmer
+Volz
+Von Semmler
+Von Zuben
+VonCannon
+VonLehmden
+Vonck
+Vonderhaar
+Vonderscher
+Vonderweidt
+Vonreichbauer
+Vopalensky
+Vopni
+Vosberg
+Vosburg
+Voss
+Vosu
+Vosup
+Vowels
+Vrabel
+Vradmin
+Vrbetic
+Vreugdenhil
+Vu
+Vucinich
+Vuignier
+Vuncannon
+Vuong
+Vyas
+WAR
+WGA
+WPMS
+Wa
+Wacheski
+Wachtstetter
+Wacker
+Wada
+Wadasinghe
+Waddell
+Wadden
+Waddick
+Waddington
+Wadkins
+Wadsworth
+Waespe
+Waeyen
+Wager
+Wagers
+Waggoner
+Waghorne
+Waghray
+Wagle
+Wagner
+Wagoner
+Wahab
+Wai
+Waid
+Waidler
+Waigh
+Waines
+Wainwright
+Waissman
+Waitman
+Wakabayashi
+Wakefield
+Wakeham
+Wakim
+Walas
+Walbridge
+Walchli
+Waldick
+Waldie
+Waldron
+Wales
+Waletzky
+Walford
+Walia
+Walker
+Walkins
+Walkley
+Walkowiak
+Wallace
+Wallaert
+Wallbank
+Wall
+Waller
+Walles
+Wallgren
+Wallis
+Walls
+Waloff
+Walpole
+Walrond
+Walser
+Walsh
+Walston
+Walta
+Walter
+Walters
+Walton
+Walz
+Wambsganz
+Wanda
+Wandel
+Wandscher
+Wang
+Wanner
+Wans
+Wanzeck
+Warburton
+Wardle
+Wardrop
+Wares
+Warfel
+Wargnier
+Wark
+Warkentin
+Warner
+Warnock
+Warrellow
+Warriner
+Warshawsky
+Wartman
+Warun
+Warwick
+Waschuk
+Washburn
+Washington
+Wasitova
+Wasmeier
+Wassel
+Wasserman
+Wasson
+Wasylenko
+Wasylyk
+Watanabe
+Watchmaker
+Watchorn
+Waterhouse
+Waterman
+Waters
+Watford
+Watkins
+Watkinson
+Watmore
+Watson
+Watters
+Wattier
+Watts
+Watznauer
+Waucheul
+Waugh
+Waybright
+Wayler
+Wayling
+Wayman
+Waytowich
+Weagle
+Weakley
+Wealch
+Weare
+Wease
+Weatherly
+Weathersbee
+Weatherspoon
+Weaver
+Webb
+Webber
+Webster
+Weckwerth
+Weddell
+Weedmark
+Weeks
+Wefers
+Wegener
+Weger
+Wegrowicz
+Wei
+Weibust
+Weidenborner
+Weidenfeller
+Weidinger
+Weihs
+Weil
+Weinbender
+Weinkauf
+Weirich
+Weisenberg
+Weiser
+Weiss
+Weist
+Weitz
+Weitzel
+Welch
+Weldon
+Welham
+Welker
+Wellard
+Welling
+Wells
+Welsch
+Welsford
+Welten
+Wen
+Wendling
+Wenham
+Wennerstrom
+Wentworth
+Wenyon
+Wenzel
+Wepf
+Weppler
+Werewolf
+Werick
+Werling
+Werner
+Wernik
+Werth
+Wertz
+Wery
+Wesenberg
+Wesoloski
+Wesolowski
+Wessel
+Wessell
+Wesselman
+Wesselow
+Wessels
+Wessenberg
+Westbrook
+Westcott
+Wester
+Westfall
+Westgarth
+Westlake
+Westmoreland
+Weston
+Weston-Dawkes
+Westphal
+Westwood
+Wetherbee
+Wetteland
+Wetzel
+Wever
+Weyand
+Whalen
+Whaley
+Whang
+Whatley
+Wheatley
+Wheaton
+Wheeler
+Wheelock
+Whelan
+Whelpdale
+Whetston
+Whetzel
+Whidden
+Whipps
+Whisenhunt
+Whiskin
+Whisler
+Whitaker
+Whited
+Whiteford
+Whitehurst
+Whiteman
+Whiteside
+Whitfield
+Whitfill
+Whitford
+Whiting
+Whitlock
+Whitman
+Whitmore
+Whitney
+Whitsell
+Whitt
+Whittaker
+Whittam
+Whitten
+Whittier
+Whittingham
+Whitton
+Whitty
+Whitwam
+Whitwell
+Whitworth
+Whyte
+Wichers
+Wichman
+Wicht
+Wichterle
+Wickes
+Wickham
+Wickie
+Widdicombe
+Widdis
+Widdowson
+Widener
+Widows
+Widuch
+Wiebe
+Wiederhold
+Wiedman
+Wiegand
+Wieland
+Wienert
+Wiens
+Wiercioch
+Wierzba
+Wiggins
+Wiggs
+Wight
+Wigle
+Wignall
+Wikkerink
+Wiklund
+Wilbur
+Wilby
+Wilcox
+Wilczewski
+Wilde
+Wildeman
+Wilder
+Wilderman
+Wildgen
+Wildman
+Wilemon
+Wilenius
+Wiley
+Wilford
+Wilgosh
+Wilhelm
+Wilhelmson
+Wilhoit
+Wilke
+Wilken
+Wilkerson
+Wilkes
+Wilkie
+Wilkins
+Wilkinson
+Wilko
+Wilks
+Willard
+Willcock
+Willcocks
+Willcox
+Willekes
+Willemsen
+Willenbring
+Willets
+Willett
+Willette
+Willey
+Willhoff
+Williams
+Williamson
+Williford
+Willis
+Willison
+Willmore
+Willmott
+Willoughby
+Willson
+Wilsey
+Wilson
+Wilton
+Wiltz
+Wimberley
+Wimbush
+Wimmer
+Winchester
+Windom
+Windsor
+Winfield
+Wingar
+Wingard
+Wingate
+Wingfield
+Wingo
+Wingrove
+Winicki
+Winje
+Winklemaier
+Winkler
+Winlow
+Winn
+Winningham
+Winnington
+Winnipeg
+Winsborrow
+Winsky
+Winslow
+Winstead
+Winterberg
+Winters
+Wintour
+Wippel
+Wirth
+Wiseman
+Wishewan
+Wisniewski
+Wissinger
+Wissler
+Witchlow
+Witham
+Withrow
+Witkowski
+Witney
+Witt
+Wittich
+Wittik
+Wittman
+Witzel
+Witzman
+Witzmann
+Wobbrock
+Woelffel
+Woessner
+Woinsky
+Wojciechowski
+Wojcik
+Wojdylo
+Wojnar
+Wojtecki
+Wokoma
+Wolczanski
+Wolfe
+Wolfenbarger
+Wolff
+Wolfman
+Wolford
+Wolfs
+Wolfson
+Wolk
+Woll
+Woloshko
+Wolowidnyk
+Wolska
+Wolski
+Wolter
+Womack
+Womble
+Wong
+Woodall
+Woodford
+Woodhall
+Woodley
+Woodlief
+Woodman
+Woodrow
+Woods
+Woodward
+Woodward-Jack
+Woodyer
+Wooff
+Woolery
+Wooley
+Woollam
+Woolley
+Woolwine
+Wooster
+Wooten
+Wootton
+Wormald
+Worobey
+Woroszczuk
+Worpell
+Worrall
+Worsley
+Worthington
+Wortman
+Wozniak
+Wracher
+Wray
+Wrigglesworth
+Wrigley
+Wrobel
+Wroblewski
+Wsadmin
+Wsbackup
+Wu
+Wueppelmann
+Wun
+Wurtz
+Wyant
+Wyatt
+Wyble
+Wycoff
+Wye
+Wykoff
+Wylie
+Wyllie
+Wyman
+Wymard
+Wyndham
+Wynes
+Wyrstiuk
+Wyss
+Wytenburg
+Wyzga-Taplin
+Xavier
+Xayaraj
+Xenos
+Xie
+Xmssupport
+Xpm
+Xpmbld
+Xpmbuilder
+Xu
+Yabe
+Yach
+Yadollahi
+Yaeger
+Yahyapour
+Yakibchuk
+Yakimovich
+Yamada
+Yamamoto
+Yamaoka
+Yamashita
+Yamin
+Yan
+Yanagida
+Yancey
+Yandell
+Yano
+Yanosik
+Yao
+Yarber
+Yarbrough
+Yardy
+Yarnell
+Yarosh
+Yassa
+Yates
+Yau
+Yaung
+Yazdani
+Yazdi
+Yearwood
+Yeck
+Yedema
+Yee
+Yeh
+Yelvington
+Yendall
+Yenilmez
+Yenor
+Yeo
+Yerigan
+Yerneni
+Yeung
+Yim
+Ying
+Yoakum
+Yoe
+Yogeswaran
+Yohe
+Yokeley
+Yong
+Yonk
+York
+Yorke
+Yoshikawa
+Yoshioka
+Yoshiyama
+Yost
+Youel
+Younan
+Younes
+Youngblood
+Younger
+Younglove
+Youngman
+Youngs
+Younkin
+Yount
+Yousefpour
+Yousuf
+Yowell
+Yu
+Yuan
+Yudin
+Yue
+Yuen
+Yuengling
+Yuhanna
+Yuhn
+Yuill
+Yum
+Yumurtaci
+Yun
+Yundt
+Yung
+Yurach
+Yurchuk
+Yuste
+Yvon
+Yzerman
+Zabek
+Zabokrzycki
+Zabransky
+Zaccari
+Zacharias
+Zack
+Zadeh
+Zadorozny
+Zadow
+Zafarano
+Zafarullah
+Zagorsek
+Zagorski
+Zagrodney
+Zaharychuk
+Zahn
+Zaia
+Zaid
+Zaidi
+Zajac
+Zakarow
+Zalameda
+Zaleski
+Zalite
+Zalokar
+Zaloker
+Zalzale
+Zaman
+Zampino
+Zander
+Zanet
+Zanetti
+Zanga
+Zani
+Zantiris
+Zapach
+Zarate
+Zaretsky
+Zargham
+Zarkel
+Zarlenga
+Zatkovic
+Zattiero
+Zatylny
+Zauhar
+Zauner
+Zavadiuk
+Zawadka
+Zaydan
+Zazulak
+Zbib
+Zbuda
+Zee
+Zeggil
+Zegray
+Zeidler
+Zeiger
+Zeigler
+Zeimet
+Zeisler
+Zeitler
+Zelenka
+Zeller
+Zelsmann
+Zemanek
+Zen
+Zenar
+Zeng
+Zenisek
+Zenkevicius
+Zenkner
+Zerriffi
+Zetterlund-Clark
+Zetts
+Zhang
+Zhao
+Zhelka
+Zhong
+Zhou
+Ziai
+Ziebarth
+Zieber
+Ziegler
+Ziehn
+Zielinski
+Ziemba
+Zigrand
+Zilaie
+Zimmer
+Zimmerer
+Zimmerly
+Zimmerman
+Zimmermann
+Zingale
+Zingeler
+Zinkie
+Zinn
+Zirko
+Zisu
+Zitko
+Zito
+Zitzmann
+Zivilik
+Zivkovic
+Zlatin
+Znack
+Zoehner
+Zoellner
+Zoerb
+Zollman
+Zolmer
+Zonner
+Zonoun
+Zoppel
+Zoratti
+Zork
+Zorony
+Zorzi
+Zottola
+Zou
+Zrobok
+Zubans
+Zubricki
+Zuccarelli
+Zug
+Zuk
+Zukas
+Zukosky
+Zukovsky
+Zumhagen
+Zumpf
+Zunuzi
+Zurawlev
+Zureik
+Zurl
+Zvonar
+Zwick
+Zwicker
+Zwierzchowski
+Zybala
+Zylstra
+Zywiel
+cprs
+de Belen
+de Buda
+de CHABERT
+de Elizalde
+de Grace
+de Hoog
+de Salis
+de Wilton
+de Witte
+deMontluzin
+deRosenroll
+deVette
+fpsched
+livinston
+van Leeuwen
+von Ende
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/people_and_groups.template b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/people_and_groups.template
new file mode 100644
index 0000000..0f2c423
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/people_and_groups.template
@@ -0,0 +1,62 @@
+define suffix=dc=example,dc=com
+define maildomain=example.com
+define numusers=10
+define numous=10
+define numgroup=5
+
+branch: [suffix]
+subordinateTemplate: ous:[numous]
+
+template: ous
+subordinateTemplate: People:1
+subordinateTemplate: Groups:1
+rdnAttr: ou
+objectclass: top
+objectclass: organizationalUnit
+ou: Organization_<sequential:1>
+description: This is {ou}
+
+template: People
+rdnAttr: ou
+subordinateTemplate: person:[numusers]
+objectclass: top
+objectclass: organizationalUnit
+ou: People
+
+template: Groups
+subordinateTemplate: groupOfName:[numgroup]
+rdnAttr: ou
+objectclass: top
+objectclass: organizationalUnit
+ou: Groups
+
+template: person
+rdnAttr: uid
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: <first>
+sn: <last>
+cn: {givenName} {sn}
+initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}
+employeeNumber: <sequential:0>
+uid: user.{employeeNumber}
+mail: {uid}@[maildomain]
+userPassword: password
+telephoneNumber: <random:telephone>
+homePhone: <random:telephone>
+pager: <random:telephone>
+mobile: <random:telephone>
+street: <random:numeric:5> <file:streets> Street
+l: <file:cities>
+st: <file:states>
+postalCode: <random:numeric:5>
+postalAddress: {cn}${street}${l}, {st}  {postalCode}
+description: This is the description for {cn}.
+
+template: groupOfName
+rdnAttr: cn
+objectClass: top
+objectClass: groupOfNames
+cn: Group_<sequential:1>
\ No newline at end of file
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/states b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/states
new file mode 100644
index 0000000..961d2c4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/states
@@ -0,0 +1,51 @@
+AL
+AK
+AZ
+AR
+CA
+CO
+CT
+DE
+DC
+FL
+GA
+HI
+ID
+IL
+IN
+IA
+KS
+KY
+LA
+ME
+MD
+MA
+MI
+MN
+MS
+MO
+MT
+NE
+NV
+NH
+NJ
+NM
+NY
+NC
+ND
+OH
+OK
+OR
+PA
+RI
+SC
+SD
+TN
+TX
+UT
+VT
+VA
+WA
+WV
+WI
+WY
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/streets b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/streets
new file mode 100644
index 0000000..be3a345
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/streets
@@ -0,0 +1,74 @@
+Adams
+Ash
+Birch
+Broadway
+Cedar
+Center
+Central
+Cherry
+Chestnut
+Church
+College
+Davis
+Dogwood
+East
+Eighth
+Eleventh
+Elm
+Fifteenth
+Fifth
+First
+Forest
+Fourteenth
+Fourth
+Franklin
+Green
+Hickory
+Highland
+Hill
+Hillcrest
+Jackson
+Jefferson
+Johnson
+Lake
+Lakeview
+Laurel
+Lee
+Lincoln
+Locust
+Madison
+Main
+Maple
+Meadow
+Mill
+Miller
+Ninth
+North
+Oak
+Park
+Pine
+Poplar
+Railroad
+Ridge
+River
+Second
+Seventh
+Sixth
+South
+Spring
+Spruce
+Sunset
+Sycamore
+Taylor
+Tenth
+Third
+Thirteenth
+Twelfth
+Valley
+Walnut
+Washington
+West
+Williams
+Willow
+Wilson
+Woodland
diff --git a/opendj-sdk/opendj-core/src/site/xdoc/index.xml.vm b/opendj-sdk/opendj-core/src/site/xdoc/index.xml.vm
new file mode 100644
index 0000000..cbedee4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/site/xdoc/index.xml.vm
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2013-2015 ForgeRock AS.
+-->
+<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+  <properties>
+    <title>About OpenDJ LDAP SDK</title>
+    <author email="opendj-dev@forgerock.org">${project.organization.name}</author>
+  </properties>
+  <body>
+    <section name="About OpenDJ LDAP SDK">
+      <p>
+        The OpenDJ LDAP SDK provides a set of modern, developer-friendly Java
+        APIs as part of the OpenDJ product suite. The product suite includes the
+        client SDK alongside command-line tools and sample code, a 100% pure
+        Java directory server, and more. You can use OpenDJ SDK to create client
+        applications for use with any server that complies with the,
+        <a href='http://tools.ietf.org/html/rfc4510'>RFC 4510: Lightweight Directory
+        Access Protocol (LDAP): Technical Specification Road Map</a>.
+      </p>
+      <p>
+        The OpenDJ LDAP SDK brings you easy-to-use connection management,
+        connection pooling, load balancing, and all the standard LDAP operations
+        to read and write directory entries. OpenDJ LDAP SDK also lets you build
+        applications with capabilities defined in additional draft and
+        experimental RFCs that are supported by modern LDAP servers.
+      </p>
+    </section>
+    <section name="Documentation for OpenDJ SDK">
+      <p>
+        Javadoc for this module can be found <a href="apidocs/index.html">here</a>.
+        Read the <a href="../doc/dev-guide/index.html">developer guide</a> for
+        a deeper understanding of LDAP application development, as well as a
+        detailed over of LDAP itself.
+      </p>
+    </section>
+    <section name="Get the OpenDJ LDAP SDK">
+      <p>
+        You can start developing your LDAP applications now by obtaining the
+        OpenDJ LDAP SDK using any of the following methods:
+      </p>
+      <subsection name="Maven">
+        <p>
+          By far the simplest method is to develop your application using Maven
+          and add the following settings to your <b>pom.xml</b>:
+        </p>
+        <source>&lt;repositories>
+  &lt;repository>
+    &lt;id>forgerock-staging-repository&lt;/id>
+    &lt;name>ForgeRock Release Repository&lt;/name>
+    &lt;url>${mavenRepoReleases}&lt;/url>
+    &lt;snapshots>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/snapshots>
+  &lt;/repository>
+  &lt;repository>
+    &lt;id>forgerock-snapshots-repository&lt;/id>
+    &lt;name>ForgeRock Snapshot Repository&lt;/name>
+    &lt;url>${mavenRepoSnapshots}&lt;/url>
+    &lt;releases>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/releases>
+  &lt;/repository>
+&lt;/repositories></source>
+        <p>
+          The following dependencies will load both the OpenDJ Core APIs and the
+          OpenDJ Grizzly network transport:
+        </p>
+        <source>&lt;dependencies>
+  &lt;dependency>
+    &lt;groupId>${project.groupId}&lt;/groupId>
+    &lt;artifactId>${project.artifactId}&lt;/artifactId>
+    &lt;version>${project.version}&lt;/version>
+  &lt;/dependency>
+  &lt;dependency>
+    &lt;groupId>${project.groupId}&lt;/groupId>
+    &lt;artifactId>opendj-grizzly&lt;/artifactId>
+    &lt;version>${project.version}&lt;/version>
+  &lt;/dependency>
+&lt;/dependencies></source>
+        <p>
+          In some use-cases, such as developing LDAP unit tests or embedded
+          LDAP applications, the network transport is not required, in which
+          case you can simply declare a dependency on the OpenDJ core APIs:
+        </p>
+        <source>&lt;dependencies>
+  &lt;dependency>
+    &lt;groupId>${project.groupId}&lt;/groupId>
+    &lt;artifactId>${project.artifactId}&lt;/artifactId>
+    &lt;version>${project.version}&lt;/version>
+  &lt;/dependency>
+&lt;/dependencies></source>
+      </subsection>
+      <subsection name="Download">
+        <p>
+          If you are not using Maven then you will need to download a pre-built
+          binary from the ForgeRock Maven repository, along with any compile
+          time <a href="dependencies.html">dependencies</a>:
+        </p>
+        <ul>
+          <li><a href="${mavenRepoReleases}/org/forgerock/opendj/${project.artifactId}">Stable releases</a></li>
+          <li><a href="${mavenRepoSnapshots}/org/forgerock/opendj/${project.artifactId}/${project.version}">Latest development snapshot</a></li>
+        </ul>
+      </subsection>
+      <subsection name="Build">
+        <p>
+          For the DIY enthusiasts you can build it yourself by checking out the
+          latest code using <a href="source-repository.html">Subversion</a>
+          and building it with Maven 3.
+        </p>
+      </subsection>
+    </section>
+    <section name="Getting started">
+      <p>
+        The following example shows how the OpenDJ SDK may be used to
+        connect to a Directory Server, authenticate, and then perform a
+        search. The search results are output as LDIF to the standard output:
+      </p>
+      <source>// Create an LDIF writer which will write the search results to stdout.
+final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+Connection connection = null;
+try
+{
+  // Connect and bind to the server.
+  final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", 1389);
+
+  connection = factory.getConnection();
+  connection.bind(userName, password);
+
+  // Read the entries and output them as LDIF.
+  final ConnectionEntryReader reader = connection.search(baseDN, scope, filter, attributes);
+  while (reader.hasNext())
+  {
+    if (reader.isEntry())
+    {
+      // Got an entry.
+      final SearchResultEntry entry = reader.readEntry();
+      writer.writeComment("Search result entry: " + entry.getName().toString());
+      writer.writeEntry(entry);
+    }
+    else
+    {
+      // Got a continuation reference.
+      final SearchResultReference ref = reader.readReference();
+      writer.writeComment("Search result reference: " + ref.getURIs().toString());
+    }
+  }
+  writer.flush();
+}
+catch (final Exception e)
+{
+  // Handle exceptions...
+  System.err.println(e.getMessage());
+}
+finally
+{
+  if (connection != null)
+  {
+    connection.close();
+  }
+}</source>
+      <p>
+        You can find more examples in the <a href="../opendj-ldap-sdk-examples/index.html">OpenDJ LDAP SDK Examples</a> module.
+      </p>
+    </section>
+  </body>
+</document>
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControlTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControlTestCase.java
new file mode 100644
index 0000000..a1aaea8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityRequestControlTestCase.java
@@ -0,0 +1,58 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.controls;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlsTestCase;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * Tests the account usability request control.
+ */
+@SuppressWarnings("javadoc")
+public class AccountUsabilityRequestControlTestCase extends ControlsTestCase {
+    @Test
+    public void testControl() throws Exception {
+        // Send this control with a search request and see that you get a valid response.
+        final SearchRequest req =
+                Requests.newSearchRequest(DN.valueOf("uid=user.1,ou=people,o=test"),
+                        SearchScope.BASE_OBJECT, Filter.objectClassPresent());
+        final AccountUsabilityRequestControl control =
+                AccountUsabilityRequestControl.newControl(false);
+        req.addControl(control);
+        final Connection con = TestCaseUtils.getInternalConnection();
+        final List<SearchResultEntry> entries = new ArrayList<>();
+        con.search(req, entries);
+        assertTrue(entries.size() > 0);
+        final SearchResultEntry entry = entries.get(0);
+        final Control ctrl = entry.getControls().get(0);
+        assertEquals(ctrl.getOID(), "1.3.6.1.4.1.42.2.27.9.5.8", "expected control response 1.3.6.1.4.1.42.2.27.9.5.8");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControlTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControlTestCase.java
new file mode 100644
index 0000000..ab3ffc4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/ldap/controls/AccountUsabilityResponseControlTestCase.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.controls;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.controls.ControlsTestCase;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the account usability response control.
+ */
+@SuppressWarnings("javadoc")
+public class AccountUsabilityResponseControlTestCase extends ControlsTestCase {
+    @Test
+    public void testInvalidResponseControl() throws Exception {
+        // Don't send the request control and hence there
+        // shouldn't be response.
+        final SearchRequest req =
+                Requests.newSearchRequest(DN.valueOf("uid=user.1,ou=people,o=test"),
+                        SearchScope.BASE_OBJECT, Filter.objectClassPresent());
+        final Connection con = TestCaseUtils.getInternalConnection();
+        final List<SearchResultEntry> entries = new ArrayList<>();
+        con.search(req, entries);
+        assertTrue(entries.size() > 0);
+        final SearchResultEntry entry = entries.get(0);
+        final AccountUsabilityResponseControl aurctrl =
+                entry.getControl(AccountUsabilityResponseControl.DECODER, new DecodeOptions());
+        assertNull(aurctrl);
+    }
+
+    @Test
+    public void testValidResponseControl() throws Exception {
+        // Send this control with a search request and see that you get a valid response.
+        final SearchRequest req =
+                Requests.newSearchRequest(DN.valueOf("uid=user.1,ou=people,o=test"),
+                        SearchScope.BASE_OBJECT, Filter.objectClassPresent());
+        final AccountUsabilityRequestControl control =
+                AccountUsabilityRequestControl.newControl(false);
+        req.addControl(control);
+        final Connection con = TestCaseUtils.getInternalConnection();
+        final List<SearchResultEntry> entries = new ArrayList<>();
+        con.search(req, entries);
+        assertTrue(entries.size() > 0);
+        final SearchResultEntry entry = entries.get(0);
+        final AccountUsabilityResponseControl aurctrl =
+                entry.getControl(AccountUsabilityResponseControl.DECODER, new DecodeOptions());
+        assertFalse(aurctrl.isExpired());
+        assertFalse(aurctrl.isLocked());
+        assertFalse(aurctrl.isInactive());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ASCIICharPropTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ASCIICharPropTestCase.java
new file mode 100644
index 0000000..10b3b80
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ASCIICharPropTestCase.java
@@ -0,0 +1,257 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests various methods of ASCIICharProp class.
+ */
+public class ASCIICharPropTestCase extends UtilTestCase {
+    /**
+     * Invalid Ascii char data provider.
+     *
+     * @return Returns an array of data.
+     */
+    @DataProvider(name = "invalidasciidata")
+    public Object[][] createInValidASCIIData() {
+        return new Object[][] { { '\u200A' } };
+    }
+
+    /**
+     * Valid Ascii char data provider.
+     *
+     * @return Returns an array of data.
+     */
+    @DataProvider(name = "validasciidata")
+    public Object[][] createValidASCIIData() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                (char) 1,
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                false, // is key char
+                false  // is compat key char
+            },
+            {
+                '-',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                '_',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                false, // is key char
+                true   // is compat key char
+            },
+            {
+                '.',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                false, // is key char
+                true   // is compat key char
+            },
+            {
+                '+',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                false, // is key char
+                false  // is compat key char
+            },
+            {
+                '=',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                false, // is letter
+                false, // is digit
+                false, // is key char
+                true  // is compat key char
+            },
+            {
+                'a',
+                false, // uppercase
+                10,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                'A',
+                true,  // uppercase
+                10,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                'f',
+                false, // uppercase
+                15,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                'F',
+                true,  // uppercase
+                15,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                'z',
+                false, // uppercase
+                -1,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                'Z',
+                true,  // uppercase
+                -1,    // hex
+                -1,    // decimal
+                true,  // is letter
+                false, // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                '0',
+                false, // uppercase
+                0,     // hex
+                0,     // decimal
+                false, // is letter
+                true,  // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+            {
+                '9',
+                false, // uppercase
+                9,     // hex
+                9,     // decimal
+                false, // is letter
+                true,  // is digit
+                true,  // is key char
+                true   // is compat key char
+            },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests whether a character is an invalid ascii character or not.
+     *
+     * @param myChar
+     *            The character that needs to be verified.
+     * @throws Exception
+     *             In case of any errors.
+     */
+    @Test(dataProvider = "invalidasciidata")
+    public void testValueOf(final char myChar) throws Exception {
+        assertEquals(ASCIICharProp.valueOf(myChar), null);
+    }
+
+    /**
+     * Tests whether a character is a valid ascii character or not.
+     *
+     * @param myChar
+     *            The character that needs to be verified.
+     * @param isUpper
+     *            Whether it is uppercase
+     * @param hexValue
+     *            The hexadecimal value
+     * @param decimalValue
+     *            The decimal value
+     * @param isLetter
+     *            Whether the character is a letter
+     * @param isDigit
+     *            Whether the character is a digit
+     * @param isKeyChar
+     *            Whether the character is a key char.
+     * @param isCompatKeyChar
+     *            Whether the character is a compat key char.
+     * @throws Exception
+     *             In case of any errors.
+     */
+    @Test(dataProvider = "validasciidata")
+    public void testValueOf(final char myChar, final boolean isUpper, final int hexValue,
+            final int decimalValue, final boolean isLetter, final boolean isDigit,
+            final boolean isKeyChar, final boolean isCompatKeyChar) throws Exception {
+        final ASCIICharProp myProp = ASCIICharProp.valueOf(myChar);
+
+        // check letter.
+        assertEquals(isLetter, myProp.isLetter());
+
+        // Check case.
+        assertEquals(isLetter & isUpper, myProp.isUpperCase());
+        assertEquals(isLetter & !isUpper, myProp.isLowerCase());
+
+        // check digit.
+        assertEquals(isDigit, myProp.isDigit());
+
+        // Check hex.
+        assertEquals(myProp.hexValue(), hexValue);
+
+        // Decimal value.
+        assertEquals(myProp.decimalValue(), decimalValue);
+
+        // Check if it is equal to itself.
+        assertEquals(myProp.charValue(), myChar);
+        assertEquals(myProp.compareTo(ASCIICharProp.valueOf(myChar)), 0);
+
+        // keychar.
+        assertEquals(isKeyChar, myProp.isKeyChar(false));
+        assertEquals(isCompatKeyChar, myProp.isKeyChar(true));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/OperatingSystemTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/OperatingSystemTestCase.java
new file mode 100644
index 0000000..b28e814
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/OperatingSystemTestCase.java
@@ -0,0 +1,133 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This class tests the model functionality.
+ */
+@SuppressWarnings("javadoc")
+public class OperatingSystemTestCase extends UtilTestCase {
+
+    // @formatter:off
+    @DataProvider(name = "allOS")
+    Object[][] createValidArguments() throws Exception {
+        return new Object[][] {
+            { null },
+            { "" },
+            { "AIX" },
+            { "Digital Unix" },
+            { "FreeBSD" },
+            { "HP UX" },
+            { "Irix" },
+            { "Linux" },
+            { "Mac OS" },
+            { "Mac OS X" },
+            { "MPE/iX" },
+            { "Netware 4.11" },
+            { "OS/2" },
+            { "Solaris" },
+            { "Windows 2000" },
+            { "Windows Server 2008" },
+            { "Windows 95" },
+            { "Windows 98" },
+            { "Windows NT" },
+            { "Windows Vista" },
+            { "Windows 7" },
+            { "Windows XP"  },
+        };
+    }
+    // @formatter:on
+
+    @Test(dataProvider = "allOS")
+    public void testOperatingSystems(String value) throws Exception {
+        String orig = System.getProperty("os.name");
+        try {
+            if (value != null) {
+                System.setProperty("os.name", value);
+            } else {
+                System.clearProperty("os.name");
+            }
+            run();
+        } finally {
+            System.setProperty("os.name", orig);
+        }
+    }
+
+    @Test
+    private void run() {
+        final OperatingSystem os = OperatingSystem.getOperatingSystem();
+
+        if (os == OperatingSystem.WINDOWS7) {
+            assertTrue(OperatingSystem.isWindows());
+            assertTrue(OperatingSystem.isWindows7());
+            assertFalse(OperatingSystem.isVista());
+            assertFalse(OperatingSystem.isWindows2008());
+            assertFalse(OperatingSystem.isMacOS());
+            assertFalse(OperatingSystem.isUnix());
+        } else if (os == OperatingSystem.WINDOWS_VISTA) {
+            assertTrue(OperatingSystem.isWindows());
+            assertFalse(OperatingSystem.isWindows7());
+            assertTrue(OperatingSystem.isVista());
+            assertFalse(OperatingSystem.isWindows2008());
+            assertFalse(OperatingSystem.isMacOS());
+            assertFalse(OperatingSystem.isUnix());
+        } else if (os == OperatingSystem.WINDOWS_SERVER_2008) {
+            assertTrue(OperatingSystem.isWindows());
+            assertFalse(OperatingSystem.isWindows7());
+            assertFalse(OperatingSystem.isVista());
+            assertTrue(OperatingSystem.isWindows2008());
+            assertFalse(OperatingSystem.isMacOS());
+            assertFalse(OperatingSystem.isUnix());
+        } else if (os == OperatingSystem.WINDOWS) {
+            assertTrue(OperatingSystem.isWindows());
+            assertFalse(OperatingSystem.isWindows7());
+            assertFalse(OperatingSystem.isVista());
+            assertFalse(OperatingSystem.isWindows2008());
+            assertFalse(OperatingSystem.isMacOS());
+            assertFalse(OperatingSystem.isUnix());
+        } else if (os == OperatingSystem.SOLARIS
+                || os == OperatingSystem.LINUX
+                || os == OperatingSystem.HPUX
+                || os == OperatingSystem.FREEBSD
+                || os == OperatingSystem.AIX) {
+            assertNotWindows();
+            assertFalse(OperatingSystem.isMacOS());
+            assertTrue(OperatingSystem.isUnix());
+        } else if (os == OperatingSystem.MACOSX) {
+            assertNotWindows();
+            assertTrue(OperatingSystem.isMacOS());
+            assertTrue(OperatingSystem.isUnix());
+        } else {
+            assertNotWindows();
+            assertFalse(OperatingSystem.isMacOS());
+            assertFalse(OperatingSystem.isUnix());
+            assertTrue(OperatingSystem.isUnknown());
+        }
+    }
+
+    private void assertNotWindows() {
+        assertFalse(OperatingSystem.isWindows());
+        assertFalse(OperatingSystem.isWindows7());
+        assertFalse(OperatingSystem.isVista());
+        assertFalse(OperatingSystem.isWindows2008());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ReferenceCountedObjectTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ReferenceCountedObjectTestCase.java
new file mode 100644
index 0000000..c2af560
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/ReferenceCountedObjectTestCase.java
@@ -0,0 +1,151 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import org.testng.annotations.Test;
+
+/**
+ * This Test Class tests {@link ReferenceCountedObject}.
+ */
+@SuppressWarnings("javadoc")
+public class ReferenceCountedObjectTestCase extends UtilTestCase {
+
+    private interface Impl {
+        void destroyInstance(Object instance);
+
+        Object newInstance();
+    }
+
+    private final Object object = "Test Object";
+
+    @Test
+    public void testAcquire() throws Exception {
+        final Impl impl = mock(Impl.class);
+        when(impl.newInstance()).thenReturn(object);
+        final ReferenceCountedObject<Object> rco = rco(impl);
+
+        // First acquisition should create new instance.
+        final ReferenceCountedObject<Object>.Reference ref1 = rco.acquire();
+        assertThat(ref1.get()).isSameAs(object);
+        verify(impl).newInstance();
+        verifyNoMoreInteractions(impl);
+
+        // Second acquisition should just bump the ref count.
+        final ReferenceCountedObject<Object>.Reference ref2 = rco.acquire();
+        assertThat(ref2.get()).isSameAs(object);
+        verifyNoMoreInteractions(impl);
+
+        // First dereference should just decrease the ref count.
+        ref1.release();
+        verifyNoMoreInteractions(impl);
+
+        // Second dereference should destroy the instance.
+        ref2.release();
+        verify(impl).destroyInstance(object);
+        verifyNoMoreInteractions(impl);
+    }
+
+    @Test
+    public void testAcquireIfNull() throws Exception {
+        final Object otherObject = "Other object";
+        final Impl impl = mock(Impl.class);
+        when(impl.newInstance()).thenReturn(object);
+        final ReferenceCountedObject<Object> rco = rco(impl);
+        final ReferenceCountedObject<Object>.Reference ref = rco.acquireIfNull(otherObject);
+
+        verify(impl, never()).newInstance();
+        assertThat(ref.get()).isSameAs(otherObject);
+        ref.release();
+        verifyNoMoreInteractions(impl);
+    }
+
+    /**
+     * This test attempts to test that finalization works.
+     */
+    @Test
+    public void testFinalization() {
+        final Impl impl = mock(Impl.class);
+        when(impl.newInstance()).thenReturn(object);
+        final ReferenceCountedObject<Object> rco = rco(impl);
+        final ReferenceCountedObject<Object>.Reference ref = rco.acquire();
+        verify(impl, never()).destroyInstance(object);
+        ref.finalize(); // Simulate GC.
+        verify(impl).destroyInstance(object);
+    }
+
+    /**
+     * Test for issue OPENDJ-1049.
+     */
+    @Test
+    public void testReleaseOnceOnly() {
+        final Impl impl = mock(Impl.class);
+        when(impl.newInstance()).thenReturn(object);
+        final ReferenceCountedObject<Object> rco = rco(impl);
+
+        // Create two references.
+        final ReferenceCountedObject<Object>.Reference ref1 = rco.acquire();
+        final ReferenceCountedObject<Object>.Reference ref2 = rco.acquire();
+
+        /*
+         * Now release first reference multiple times and make sure that the
+         * resource is not destroyed.
+         */
+        ref1.release();
+        ref1.release();  // Redundant release.
+        ref1.finalize(); // Simulate GC.
+        verify(impl, never()).destroyInstance(object);
+
+        /*
+         * Now release second reference which should cause the resource to be
+         * destroyed.
+         */
+        ref2.release();
+        verify(impl).destroyInstance(object);
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testStaleReference() throws Exception {
+        final Impl impl = mock(Impl.class);
+        when(impl.newInstance()).thenReturn(object);
+        final ReferenceCountedObject<Object> rco = rco(impl);
+        final ReferenceCountedObject<Object>.Reference ref = rco.acquire();
+        ref.release();
+        ref.get();
+    }
+
+    private ReferenceCountedObject<Object> rco(final Impl impl) {
+        return new ReferenceCountedObject<Object>() {
+
+            @Override
+            protected void destroyInstance(final Object instance) {
+                impl.destroyInstance(instance);
+            }
+
+            @Override
+            protected Object newInstance() {
+                return impl.newInstance();
+            }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StaticUtilsTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StaticUtilsTestCase.java
new file mode 100644
index 0000000..87bf93c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StaticUtilsTestCase.java
@@ -0,0 +1,332 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static com.forgerock.opendj.util.StaticUtils.*;
+
+import static org.fest.assertions.Assertions.*;
+
+/**
+ * Test {@code StaticUtils}.
+ */
+@SuppressWarnings("javadoc")
+public final class StaticUtilsTestCase extends UtilTestCase {
+    /**
+     * Create data for format(...) tests.
+     *
+     * @return Returns test data.
+     */
+    @DataProvider(name = "createFormatData")
+    public Object[][] createFormatData() {
+        return new Object[][] {
+            // Note that Calendar months run from 0-11,
+            // and that there was no such year as year 0 (1 BC -> 1 AD).
+            { 1, 0, 1, 0, 0, 0, 0, "00010101000000.000Z" },
+            { 9, 0, 1, 0, 0, 0, 0, "00090101000000.000Z" },
+            { 10, 0, 1, 0, 0, 0, 0, "00100101000000.000Z" },
+            { 99, 0, 1, 0, 0, 0, 0, "00990101000000.000Z" },
+            { 100, 0, 1, 0, 0, 0, 0, "01000101000000.000Z" },
+            { 999, 0, 1, 0, 0, 0, 0, "09990101000000.000Z" },
+            { 1000, 0, 1, 0, 0, 0, 0, "10000101000000.000Z" },
+            { 2000, 0, 1, 0, 0, 0, 0, "20000101000000.000Z" },
+            { 2099, 0, 1, 0, 0, 0, 0, "20990101000000.000Z" },
+            { 2000, 8, 1, 0, 0, 0, 0, "20000901000000.000Z" },
+            { 2000, 9, 1, 0, 0, 0, 0, "20001001000000.000Z" },
+            { 2000, 10, 1, 0, 0, 0, 0, "20001101000000.000Z" },
+            { 2000, 11, 1, 0, 0, 0, 0, "20001201000000.000Z" },
+            { 2000, 0, 9, 0, 0, 0, 0, "20000109000000.000Z" },
+            { 2000, 0, 10, 0, 0, 0, 0, "20000110000000.000Z" },
+            { 2000, 0, 19, 0, 0, 0, 0, "20000119000000.000Z" },
+            { 2000, 0, 20, 0, 0, 0, 0, "20000120000000.000Z" },
+            { 2000, 0, 29, 0, 0, 0, 0, "20000129000000.000Z" },
+            { 2000, 0, 30, 0, 0, 0, 0, "20000130000000.000Z" },
+            { 2000, 0, 31, 0, 0, 0, 0, "20000131000000.000Z" },
+            { 2000, 0, 1, 9, 0, 0, 0, "20000101090000.000Z" },
+            { 2000, 0, 1, 10, 0, 0, 0, "20000101100000.000Z" },
+            { 2000, 0, 1, 19, 0, 0, 0, "20000101190000.000Z" },
+            { 2000, 0, 1, 20, 0, 0, 0, "20000101200000.000Z" },
+            { 2000, 0, 1, 23, 0, 0, 0, "20000101230000.000Z" },
+            { 2000, 0, 1, 0, 9, 0, 0, "20000101000900.000Z" },
+            { 2000, 0, 1, 0, 10, 0, 0, "20000101001000.000Z" },
+            { 2000, 0, 1, 0, 59, 0, 0, "20000101005900.000Z" },
+            { 2000, 0, 1, 0, 0, 9, 0, "20000101000009.000Z" },
+            { 2000, 0, 1, 0, 0, 10, 0, "20000101000010.000Z" },
+            { 2000, 0, 1, 0, 0, 59, 0, "20000101000059.000Z" },
+            { 2000, 0, 1, 0, 0, 0, 9, "20000101000000.009Z" },
+            { 2000, 0, 1, 0, 0, 0, 10, "20000101000000.010Z" },
+            { 2000, 0, 1, 0, 0, 0, 99, "20000101000000.099Z" },
+            { 2000, 0, 1, 0, 0, 0, 100, "20000101000000.100Z" },
+            { 2000, 0, 1, 0, 0, 0, 999, "20000101000000.999Z" }, };
+    }
+
+    @DataProvider(name = "dataForToLowerCase")
+    public Object[][] dataForToLowerCase() {
+        // Value, toLowerCase or null if identity
+        return new Object[][] {
+            { "", null },
+            { " ", null },
+            { "   ", null },
+            { "12345", null },
+            { "abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./", null },
+            { "Aabcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./",
+                "aabcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./" },
+            { "abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./A",
+                "abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()_-+={}|[]\\:\";'<>?,./a" },
+            { "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" },
+            { "\u00c7edilla", "\u00e7edilla" }, { "ced\u00cdlla", "ced\u00edlla" }, };
+    }
+
+    /**
+     * Tests {@link StaticUtils#formatAsGeneralizedTime(java.util.Date)}.
+     *
+     * @param yyyy
+     *            The year.
+     * @param months
+     *            The month.
+     * @param dd
+     *            The day.
+     * @param hours
+     *            The hour.
+     * @param mm
+     *            The minute.
+     * @param ss
+     *            The second.
+     * @param ms
+     *            The milli-seconds.
+     * @param expected
+     *            The expected generalized time formatted string.
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test(dataProvider = "createFormatData")
+    public void testFormatDate(final int yyyy, final int months, final int dd, final int hours,
+            final int mm, final int ss, final int ms, final String expected) throws Exception {
+        final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+        calendar.set(yyyy, months, dd, hours, mm, ss);
+        calendar.set(Calendar.MILLISECOND, ms);
+        final Date time = new Date(calendar.getTimeInMillis());
+        final String actual = formatAsGeneralizedTime(time);
+        Assert.assertEquals(actual, expected);
+    }
+
+    /**
+     * Tests {@link StaticUtils#formatAsGeneralizedTime(long)} .
+     *
+     * @param yyyy
+     *            The year.
+     * @param months
+     *            The month.
+     * @param dd
+     *            The day.
+     * @param hours
+     *            The hour.
+     * @param mm
+     *            The minute.
+     * @param ss
+     *            The second.
+     * @param ms
+     *            The milli-seconds.
+     * @param expected
+     *            The expected generalized time formatted string.
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test(dataProvider = "createFormatData")
+    public void testFormatLong(final int yyyy, final int months, final int dd, final int hours,
+            final int mm, final int ss, final int ms, final String expected) throws Exception {
+        final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+        calendar.set(yyyy, months, dd, hours, mm, ss);
+        calendar.set(Calendar.MILLISECOND, ms);
+        final long time = calendar.getTimeInMillis();
+        final String actual = formatAsGeneralizedTime(time);
+        Assert.assertEquals(actual, expected);
+    }
+
+    @Test(dataProvider = "dataForToLowerCase")
+    public void testToLowerCaseString(final String s, final String expected) {
+        final String actual = toLowerCase(s);
+        if (expected != null) {
+            Assert.assertEquals(actual, expected);
+        } else {
+            Assert.assertSame(actual, s);
+        }
+    }
+
+    @Test(dataProvider = "dataForToLowerCase")
+    public void testToLowerCaseStringBuilder(final String s, final String expected) {
+        final StringBuilder builder = new StringBuilder();
+        final String actual = toLowerCase(s, builder).toString();
+        if (expected != null) {
+            Assert.assertEquals(actual, expected);
+        } else {
+            Assert.assertEquals(actual, s);
+        }
+    }
+
+    @DataProvider
+    public Object[][] stackTraceToSingleLineLimitedStackProvider() {
+        final String noMessageTrace = "RuntimeException (StaticUtilsTestCase.java";
+        final String messageTrace = "RuntimeException: message (StaticUtilsTestCase.java";
+        return new Object[][] {
+            { null, "" },
+            { new RuntimeException(),   noMessageTrace },
+            { new RuntimeException(""), noMessageTrace },
+            { new RuntimeException("message"), messageTrace },
+            { new InvocationTargetException(new RuntimeException()),   noMessageTrace },
+            { new InvocationTargetException(new RuntimeException("")), noMessageTrace },
+            { new InvocationTargetException(new RuntimeException("message")), messageTrace },
+            {
+                new RuntimeException(new RuntimeException("message")),
+                "RuntimeException: java.lang.RuntimeException: message (StaticUtilsTestCase.java"
+            },
+            { new RuntimeException("message", new RuntimeException()), messageTrace },
+            { new RuntimeException("message", new RuntimeException("message")), messageTrace },
+        };
+    }
+
+    @Test(dataProvider = "stackTraceToSingleLineLimitedStackProvider")
+    public void testStackTraceToSingleLineLimitedStack1(Throwable t, String expectedStartWith) {
+        final String trace = stackTraceToSingleLineString(t, false);
+        assertThat(trace).startsWith(expectedStartWith);
+        if (t != null) {
+            assertThat(trace).endsWith("...)");
+        }
+    }
+
+    @Test(dataProvider = "getBytesTestData")
+    public void testCharsToBytes(String inputString) throws Exception {
+        Assert.assertEquals(StaticUtils.getBytes(inputString.toCharArray()), inputString.getBytes("UTF-8"));
+    }
+
+    @Test(dataProvider = "byteToHexTestData")
+    public void testByteToASCII(byte b) throws Exception {
+        if (b < 32 || b > 126) {
+            Assert.assertEquals(byteToASCII(b), ' ');
+        } else {
+            Assert.assertEquals(byteToASCII(b), (char) b);
+        }
+    }
+
+    @DataProvider
+    public Object[][] stackTraceToSingleLineFullStackStackProvider() {
+        return new Object[][] {
+            { null, "", "" },
+            { new RuntimeException(),   "java.lang.RuntimeException / StaticUtilsTestCase.java:", "" },
+            { new RuntimeException(""), "java.lang.RuntimeException / StaticUtilsTestCase.java:", "" },
+            {
+                new RuntimeException("message"),
+                "java.lang.RuntimeException: message / StaticUtilsTestCase.java:", "message"
+            },
+            {
+                new InvocationTargetException(new RuntimeException("message")),
+                "java.lang.reflect.InvocationTargetException / StaticUtilsTestCase.java:", "message"
+            },
+            {
+                new RuntimeException(new RuntimeException()),
+                "java.lang.RuntimeException: java.lang.RuntimeException / StaticUtilsTestCase.java:",
+                "java.lang.RuntimeException "
+            },
+            {
+                new RuntimeException(new RuntimeException("message")),
+                "java.lang.RuntimeException: java.lang.RuntimeException: message / StaticUtilsTestCase.java:",
+                "java.lang.RuntimeException: message"
+            },
+            {
+                new RuntimeException("message", new RuntimeException()),
+                "java.lang.RuntimeException: message / StaticUtilsTestCase.java:", "java.lang.RuntimeException "
+            },
+            {
+                new RuntimeException("message", new RuntimeException("message")),
+                "java.lang.RuntimeException: message / StaticUtilsTestCase.java:", "java.lang.RuntimeException: message"
+            },
+        };
+    }
+
+    @Test(dataProvider = "stackTraceToSingleLineFullStackStackProvider")
+    public void testStackTraceToSingleLineFullStack1(Exception throwable, String expectedStartWith,
+            String expectedContains) {
+        String trace = stackTraceToSingleLineString(throwable, true);
+        assertThat(trace).startsWith(expectedStartWith);
+        assertThat(trace).contains(expectedContains);
+        assertThat(trace).doesNotContain("...)");
+    }
+
+    @DataProvider(name = "byteToHexTestData")
+    public Object[][] createByteToHexTestData() {
+        Object[][] data = new Object[256][];
+
+        for (int i = 0; i < 256; i++) {
+            data[i] = new Object[] { new Byte((byte) i) };
+        }
+
+        return data;
+    }
+
+    @DataProvider(name = "getBytesTestData")
+    public Object[][] createGetBytesTestData() {
+        List<String> strings = new LinkedList<>();
+
+        // Some simple strings.
+        strings.add("");
+        strings.add(" ");
+        strings.add("an ascii string");
+
+        // A string containing just UTF-8 1 byte sequences.
+        StringBuilder builder = new StringBuilder();
+        for (char c = '\u0000'; c < '\u0080'; c++) {
+            builder.append(c);
+        }
+        strings.add(builder.toString());
+
+        // A string containing UTF-8 1 and 2 byte sequences.
+        builder = new StringBuilder();
+        for (char c = '\u0000'; c < '\u0100'; c++) {
+            builder.append(c);
+        }
+        strings.add(builder.toString());
+
+        // A string containing UTF-8 1 and 6 byte sequences.
+        builder = new StringBuilder();
+        for (char c = '\u0000'; c < '\u0080'; c++) {
+            builder.append(c);
+        }
+        for (char c = '\uff00'; c != '\u0000'; c++) {
+            builder.append(c);
+        }
+        strings.add(builder.toString());
+
+        // Construct the array.
+        Object[][] data = new Object[strings.size()][];
+        for (int i = 0; i < strings.size(); i++) {
+            data[i] = new Object[] { strings.get(i) };
+        }
+
+        return data;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StringPrepProfileTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StringPrepProfileTestCase.java
new file mode 100644
index 0000000..6632c7c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/StringPrepProfileTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This Test Class tests various matching rules for their compability against
+ * RFC 4517,4518 and 3454.
+ */
+@SuppressWarnings("javadoc")
+public class StringPrepProfileTestCase extends UtilTestCase {
+    /**
+     * Tests the stringprep preparation sans any case folding. This is
+     * applicable to case exact matching rules only.
+     */
+    @Test(dataProvider = "exactRuleData")
+    public void testStringPrepNoCaseFold(final String value1, final String value2,
+            final ConditionResult result) throws Exception {
+        // Take any caseExact matching rule.
+        final MatchingRule rule = Schema.getCoreSchema().getMatchingRule("2.5.13.5");
+        final Assertion assertion = rule.getAssertion(ByteString.valueOfUtf8(value1));
+        final ByteString normalizedValue2 =
+                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value2));
+        final ConditionResult liveResult = assertion.matches(normalizedValue2);
+        assertEquals(result, liveResult);
+    }
+
+    /**
+     * Tests the stringprep preparation with case folding. This is applicable to
+     * case ignore matching rules only.
+     */
+    @Test(dataProvider = "caseFoldRuleData")
+    public void testStringPrepWithCaseFold(final String value1, final String value2,
+            final ConditionResult result) throws Exception {
+        // Take any caseExact matching rule.
+        final MatchingRule rule = Schema.getCoreSchema().getMatchingRule("2.5.13.2");
+        final Assertion assertion = rule.getAssertion(ByteString.valueOfUtf8(value1));
+        final ByteString normalizedValue2 =
+                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value2));
+        final ConditionResult liveResult = assertion.matches(normalizedValue2);
+        assertEquals(result, liveResult);
+    }
+
+    /** Generates data for case exact matching rules. */
+    @DataProvider(name = "exactRuleData")
+    public Object[][] createExactRuleData() {
+        return new Object[][] { { "12345678", "12345678", ConditionResult.TRUE },
+            { "ABC45678", "ABC45678", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.FALSE },
+            { "\u0020foo\u0020bar\u0020\u0020", "foo bar", ConditionResult.TRUE },
+            { "test\u00AD\u200D", "test", ConditionResult.TRUE },
+            { "foo\u000Bbar", "foo\u0020bar", ConditionResult.TRUE },
+            { "foo\u070Fbar", "foobar", ConditionResult.TRUE }, };
+    }
+
+    /** Generates data for case ignore matching rules. */
+    @DataProvider(name = "caseFoldRuleData")
+    public Object[][] createIgnoreRuleData() {
+        return new Object[][] { { "12345678", "12345678", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.TRUE },
+            { "\u0020foo\u0020bar\u0020\u0020", "foo bar", ConditionResult.TRUE },
+            { "test\u00AD\u200D", "test", ConditionResult.TRUE },
+            { "foo\u000Bbar", "foo\u0020bar", ConditionResult.TRUE },
+            { "foo\u070Fbar", "foobar", ConditionResult.TRUE },
+            { "foo\u0149bar", "foo\u02BC\u006Ebar", ConditionResult.TRUE },
+            { "foo\u017Bbar", "foo\u017Cbar", ConditionResult.TRUE },
+            { "foo\u017BBAR", "foo\u017Cbar", ConditionResult.TRUE }, };
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/UtilTestCase.java b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/UtilTestCase.java
new file mode 100644
index 0000000..26c6e94
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/com/forgerock/opendj/util/UtilTestCase.java
@@ -0,0 +1,29 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.util;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all util unit tests should extend. Util represents the
+ * classes found directly under the package org.forgerock.opendj.ldap.util.
+ */
+@Test(groups = { "precommit", "util", "sdk" })
+public abstract class UtilTestCase extends ForgeRockTestCase {
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ByteSequenceReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ByteSequenceReaderTestCase.java
new file mode 100644
index 0000000..495f6c8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ByteSequenceReaderTestCase.java
@@ -0,0 +1,32 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.io;
+
+import org.forgerock.opendj.ldap.ByteSequenceReader;
+import org.forgerock.opendj.ldap.ByteString;
+
+/**
+ * Test class for ASN1ByteSequenceReaderTestCase.
+ */
+public class ASN1ByteSequenceReaderTestCase extends ASN1ReaderTestCase {
+    @Override
+    protected ASN1Reader getReader(final byte[] b, final int maxElementSize) {
+        final ByteSequenceReader reader = ByteString.wrap(b).asReader();
+        return ASN1.getReader(reader, maxElementSize);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
new file mode 100644
index 0000000..b8c0687
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
@@ -0,0 +1,30 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.ByteArrayInputStream;
+
+/**
+ * Test class for ASN1InputStreamReader.
+ */
+public class ASN1InputStreamReaderTestCase extends ASN1ReaderTestCase {
+    @Override
+    protected ASN1Reader getReader(final byte[] b, final int maxElementSize) {
+        final ByteArrayInputStream inStream = new ByteArrayInputStream(b);
+        return new ASN1InputStreamReader(inStream, maxElementSize);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1OutputStreamWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1OutputStreamWriterTestCase.java
new file mode 100644
index 0000000..7821b67
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1OutputStreamWriterTestCase.java
@@ -0,0 +1,45 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+
+/**
+ * Test class for ASN1OutputStreamWriter.
+ */
+public class ASN1OutputStreamWriterTestCase extends ASN1WriterTestCase {
+    private final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+    private final ASN1Writer writer = new ASN1OutputStreamWriter(outStream, 1);
+
+    @Override
+    protected byte[] getEncodedBytes() {
+        return outStream.toByteArray();
+    }
+
+    @Override
+    protected ASN1Reader getReader(final byte[] encodedBytes) {
+        final ByteArrayInputStream inStream = new ByteArrayInputStream(encodedBytes);
+        return new ASN1InputStreamReader(inStream, 0);
+    }
+
+    @Override
+    protected ASN1Writer getWriter() {
+        outStream.reset();
+        return writer;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ReaderTestCase.java
new file mode 100644
index 0000000..cee1bc8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1ReaderTestCase.java
@@ -0,0 +1,789 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.io;
+
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract base class for all ASN1Reader test cases.
+ */
+@Test(groups = { "precommit", "asn1", "sdk" })
+@SuppressWarnings("javadoc")
+public abstract class ASN1ReaderTestCase extends ForgeRockTestCase {
+
+    /**
+     * Retrieves the set of byte array values that may be used for testing.
+     *
+     * @return The set of byte array values that may be used for testing.
+     */
+    @DataProvider(name = "byteValues")
+    public Object[][] getByteValues() {
+        final Object[][] array = new Object[256][1];
+        for (int i = 0; i < 256; i++) {
+            array[i] = new Object[] { new byte[] { (byte) (i & 0xFF) } };
+        }
+
+        return array;
+    }
+
+    /**
+     * Create byte arrays with encoded ASN.1 elements to test decoding them as
+     * octet strings.
+     *
+     * @return A list of byte arrays with encoded ASN.1 elements that can be
+     *         decoded as octet strings.
+     */
+    @DataProvider(name = "elementArrays")
+    public Object[][] getElementArrays() {
+        return new Object[][] { new Object[] { new byte[] { 0x04, 0x00 } },
+            new Object[] { new byte[] { (byte) 0x50, 0x00 } },
+            new Object[] { new byte[] { 0x04, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F } },
+            new Object[] { new byte[] { 0x01, 0x01, 0x00 } },
+            new Object[] { new byte[] { 0x01, 0x01, (byte) 0xFF } },
+            new Object[] { new byte[] { 0x0A, 0x01, 0x00 } },
+            new Object[] { new byte[] { 0x0A, 0x01, 0x01 } },
+            new Object[] { new byte[] { 0x0A, 0x01, 0x7F } },
+            new Object[] { new byte[] { 0x0A, 0x01, (byte) 0x80 } },
+            new Object[] { new byte[] { 0x0A, 0x01, (byte) 0xFF } },
+            new Object[] { new byte[] { 0x0A, 0x02, 0x01, 0x00 } },
+            new Object[] { new byte[] { 0x02, 0x01, 0x00 } },
+            new Object[] { new byte[] { 0x02, 0x01, 0x01 } },
+            new Object[] { new byte[] { 0x02, 0x01, 0x7F } },
+            new Object[] { new byte[] { 0x02, 0x02, 0x00, (byte) 0x80 } },
+            new Object[] { new byte[] { 0x02, 0x02, 0x00, (byte) 0xFF } },
+            new Object[] { new byte[] { 0x02, 0x02, 0x01, 0x00 } },
+            new Object[] { new byte[] { 0x05, 0x00 } }, new Object[] { new byte[] { 0x30, 0x00 } },
+            new Object[] { new byte[] { 0x31, 0x00 } },
+            new Object[] { new byte[] { 0x05, (byte) 0x81, 0x00 } },
+            new Object[] { new byte[] { 0x05, (byte) 0x82, 0x00, 0x00 } },
+            new Object[] { new byte[] { 0x05, (byte) 0x83, 0x00, 0x00, 0x00 } },
+            new Object[] { new byte[] { 0x05, (byte) 0x84, 0x00, 0x00, 0x00, 0x00 } }, };
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with an array with a zero length that takes multiple bytes to
+     * encode.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecodeExtendedZeroLengthArrayAsNull() throws Exception {
+        final byte[] b = new byte[] { 0x05, (byte) 0x81, 0x00 };
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with an array that has less bytes than indicated by the length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLengthMismatchArrayAsBoolean() throws Exception {
+        final byte[] b = { 0x01, 0x01 };
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>readEnumerated</CODE> method that takes a byte array with
+     * a length mismatch.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLengthMismatchArrayAsEnumerated() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x81, 0x01 };
+        getReader(b, 0).readEnumerated();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsInteger</CODE> method that takes a byte array
+     * with a length mismatch.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLengthMismatchArrayAsInteger() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x81, 0x01 };
+        getReader(b, 0).readInteger();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetString</CODE> method that takes a byte array
+     * using an array whose actual length doesn't match with the decoded length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLengthMismatchArrayAsOctetString() throws Exception {
+        final byte[] b = { 0x04, 0x02, 0x00 };
+        getReader(b, 0).readOctetString();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with an array that takes too many bytes to expressthe length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsBoolean() throws Exception {
+        final byte[] b = { 0x01, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 };
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>readEnumerated</CODE> method that takes a byte array with
+     * a long length array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsEnumerated() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 };
+        getReader(b, 0).readEnumerated();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsInteger</CODE> method that takes a byte array
+     * with a long length array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsInteger() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 };
+        getReader(b, 0).readInteger();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with an array with a long length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsNull() throws Exception {
+        final byte[] b = new byte[] { 0x05, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x00 };
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetString</CODE> method that takes a byte array
+     * using an array that indicates it takes more than four bytes to encode the
+     * length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsOctetString() throws Exception {
+        final byte[] b = { 0x04, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x00 };
+        getReader(b, 0).readOctetString();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsSequence</CODE> method that takes a byte array
+     * argument with an array that takes too many bytes to encode the length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongLengthArrayAsSequence() throws Exception {
+        final byte[] b = { 0x30, (byte) 0x85, 0x00, 0x00, 0x00, 0x00, 0x00 };
+        getReader(b, 0).readStartSequence();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with an array that has an invalid number of bytes in the value.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeLongValueArrayAsBoolean() throws Exception {
+        final byte[] b = { 0x01, 0x02, 0x00, 0x00 };
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with an array with a nonzero length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeNonZeroLengthArrayAsNull() throws Exception {
+        final byte[] b = new byte[] { 0x05, 0x01, 0x00 };
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>readOctetString</CODE> method when the max element size
+     * is exceeded.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeOctetStringExceedMaxSize() throws Exception {
+        final byte[] b = new byte[] { 0x04, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F };
+        getReader(b, 3).readOctetString();
+    }
+
+    /**
+     * Tests the <CODE>readOctetString</CODE> method when the max element size
+     * is exceeded.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeSequenceExceedMaxSize() throws Exception {
+        final byte[] b = new byte[] { 0x30, 0x07, 0x04, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F };
+        getReader(b, 3).readOctetString();
+    }
+
+    /**
+     * Tests to make sure trailing components are ignored if not used.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecodeSequenceIncompleteRead() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        final byte[] b =
+                new byte[] { 0x30, 0x06, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        reader.readEndSequence();
+        assertFalse(reader.readBoolean());
+    }
+
+    /**
+     * Tests to make sure a premature EOF while reading a sub sequence can be
+     * detected.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeSequencePrematureEof() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        final byte[] b = new byte[] { 0x30, 0x09, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        while (reader.hasNextElement()) {
+            reader.readBoolean();
+        }
+        reader.readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsBoolean() throws Exception {
+        final byte[] b = new byte[1];
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>readEnumerated</CODE> method that takes a byte array with
+     * a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsEnumerated() throws Exception {
+        final byte[] b = new byte[0];
+        getReader(b, 0).readEnumerated();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsInteger</CODE> method that takes a byte array
+     * with a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsInteger() throws Exception {
+        final byte[] b = new byte[0];
+        getReader(b, 0).readInteger();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsNull() throws Exception {
+        final byte[] b = new byte[1];
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetString</CODE> method that takes a byte array
+     * using a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsOctetString() throws Exception {
+        final byte[] b = new byte[1];
+        getReader(b, 0).readOctetString();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsSequence</CODE> method that takes a byte array
+     * argument with a short array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortArrayAsSequence() throws Exception {
+        final byte[] b = new byte[1];
+        getReader(b, 0).readStartSequence();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with an array that has an invalid number of bytes in the value.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeShortValueArrayAsBoolean() throws Exception {
+        final byte[] b = { 0x01, 0x00, 0x00, 0x00 };
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with an array that doesn't contain a full length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsBoolean() throws Exception {
+        final byte[] b = { 0x01, (byte) 0x82, 0x00 };
+        getReader(b, 0).readBoolean();
+    }
+
+    /**
+     * Tests the <CODE>readEnumerated</CODE> method that takes a byte array with
+     * a truncated length array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsEnumerated() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x82, 0x00 };
+        getReader(b, 0).readEnumerated();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsInteger</CODE> method that takes a byte array
+     * with a truncated length array.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsInteger() throws Exception {
+        final byte[] b = { 0x02, (byte) 0x82, 0x00 };
+        getReader(b, 0).readInteger();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with an array with a truncated length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsNull() throws Exception {
+        final byte[] b = new byte[] { 0x05, (byte) 0x82, 0x00 };
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetString</CODE> method that takes a byte array
+     * using an array that doesn't fully contain the length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsOctetString() throws Exception {
+        final byte[] b = { 0x04, (byte) 0x82, 0x00 };
+        getReader(b, 0).readOctetString();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsSequence</CODE> method that takes a byte array
+     * argument with an array that doesn't fully describe the length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testDecodeTruncatedLengthArrayAsSequence() throws Exception {
+        final byte[] b = { 0x30, (byte) 0x82, 0x00 };
+        getReader(b, 0).readStartSequence();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with valid arrays.
+     *
+     * @param b
+     *            The byte array to use for the element values.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "byteValues")
+    public void testDecodeValidArrayAsBoolean(final byte[] b) throws Exception {
+        // First, test with the standard Boolean type.
+        final byte[] elementArray = new byte[] { 0x01, 0x01, b[0] };
+        assertEquals(getReader(elementArray, 0).readBoolean(), (b[0] != 0x00));
+
+        // Next, test with a nonstandard Boolean type.
+        elementArray[0] = 0x50;
+        assertEquals(getReader(elementArray, 0).readBoolean(), (b[0] != 0x00));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetString</CODE> method that takes a byte array
+     * using a valid array.
+     *
+     * @param b
+     *            The byte array to decode.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "elementArrays")
+    public void testDecodeValidArrayAsOctetString(final byte[] b) throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        bsb.appendBERLength(b.length);
+        bsb.appendBytes(b);
+
+        assertEquals(getReader(bsb.toByteArray(), 0).readOctetString(), ByteString.wrap(b));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetStringAsString</CODE> method that takes a
+     * byte array using a valid array.
+     *
+     * @param b
+     *            The byte array to decode.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "elementArrays")
+    public void testDecodeValidArrayAsOctetStringAsString(final byte[] b) throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        bsb.appendBERLength(b.length);
+        bsb.appendBytes(b);
+
+        assertEquals(getReader(bsb.toByteArray(), 0).readOctetStringAsString(), new String(b,
+                "UTF-8"));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetStringAsString</CODE> method that takes a
+     * byte array using a valid array.
+     *
+     * @param b
+     *            The byte array to decode.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "elementArrays")
+    public void testDecodeValidArrayAsOctetStringAsStringCharSet(final byte[] b) throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        bsb.appendBERLength(b.length);
+        bsb.appendBytes(b);
+
+        assertEquals(getReader(bsb.toByteArray(), 0).readOctetStringAsString(), new String(b,
+                "UTF-8"));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsOctetStringBuilder</CODE> method that takes a
+     * byte array using a valid array.
+     *
+     * @param b
+     *            The byte array to decode.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "elementArrays")
+    public void testDecodeValidArrayAsOctetStringBuilder(final byte[] b) throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        bsb.appendBERLength(b.length);
+        bsb.appendBytes(b);
+
+        final ByteStringBuilder bsb2 = new ByteStringBuilder();
+        getReader(bsb.toByteArray(), 0).readOctetString(bsb2);
+        assertEquals(bsb2.toByteString(), ByteString.wrap(b));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsSequence</CODE> method that takes a byte array
+     * argument with valid arrays.
+     *
+     * @param encodedElements
+     *            Byte arrays that may be used as valid values for encoded
+     *            elements.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "elementArrays")
+    public void testDecodeValidArrayAsSequence(final byte[] encodedElements) throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(ASN1.UNIVERSAL_SEQUENCE_TYPE);
+        bsb.appendBERLength(encodedElements.length + 2);
+        bsb.appendByte(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        bsb.appendBERLength(encodedElements.length);
+        bsb.appendBytes(encodedElements);
+
+        final ASN1Reader reader = getReader(bsb.toByteArray(), 0);
+        assertEquals(reader.peekLength(), encodedElements.length + 2);
+        reader.readStartSequence();
+        assertEquals(reader.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        assertEquals(reader.peekLength(), encodedElements.length);
+        reader.readOctetString().equals(ByteString.wrap(encodedElements));
+        reader.readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>decodeAsBoolean</CODE> method that takes a byte array
+     * argument with valid arrays using extended lengths.
+     *
+     * @param b
+     *            The byte array to use for the element values.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(dataProvider = "byteValues")
+    public void testDecodeValidExtendedArrayAsBoolean(final byte[] b) throws Exception {
+        // First, test with the standard Boolean type.
+        final byte[] elementArray = new byte[] { 0x01, (byte) 0x81, 0x01, b[0] };
+        assertEquals(getReader(elementArray, 0).readBoolean(), (b[0] != 0x00));
+
+        // Next, test with a nonstandard Boolean type.
+        elementArray[0] = 0x50;
+        assertEquals(getReader(elementArray, 0).readBoolean(), (b[0] != 0x00));
+    }
+
+    /**
+     * Tests the <CODE>decodeAsNull</CODE> method that takes a byte array
+     * argument with an array with a zero length.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecodeZeroLengthArrayAsNull() throws Exception {
+        final byte[] b = new byte[] { 0x05, 0x00 };
+        getReader(b, 0).readNull();
+    }
+
+    /**
+     * Tests the <CODE>elementAvailable</CODE> method.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testElementAvailable() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        byte[] b = new byte[] { 0x30, 0x06, 0x02, 0x01, 0x00, 0x02 };
+        ASN1Reader reader = getReader(b, 0);
+        assertFalse(reader.elementAvailable());
+
+        b = new byte[] { 0x30, 0x03, 0x02, 0x01, 0x00 };
+        reader = getReader(b, 0);
+        assertTrue(reader.elementAvailable());
+        reader.readStartSequence();
+        assertTrue(reader.elementAvailable());
+        reader.readInteger();
+        assertFalse(reader.elementAvailable());
+    }
+
+    /**
+     * Tests the <CODE>hasNextElement</CODE> method.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testHasNextElement() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        byte[] b = new byte[] { 0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x00, 0x03 };
+        ASN1Reader reader = getReader(b, 0);
+        assertTrue(reader.hasNextElement());
+        reader.readStartSequence();
+        assertTrue(reader.hasNextElement());
+        reader.readInteger();
+        assertTrue(reader.hasNextElement());
+
+        b = new byte[] { 0x30, 0x03, 0x02, 0x01, 0x00 };
+        reader = getReader(b, 0);
+        assertTrue(reader.hasNextElement());
+        reader.readStartSequence();
+        assertTrue(reader.hasNextElement());
+        reader.readInteger();
+        assertFalse(reader.hasNextElement());
+    }
+
+    /**
+     * Tests the <CODE>readEndSequence</CODE> method without first calling
+     * readStartSequence.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { IllegalStateException.class, IOException.class })
+    public void testReadEndSequenceNoStartSequence() throws Exception {
+        final byte[] b = { 0x30, 0x01, 0x00 };
+        getReader(b, 0).readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>skipElement</CODE> method.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSkipElement() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        final byte[] b =
+                new byte[] { 0x30, 0x09, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        reader.readInteger();
+        reader.skipElement();
+        assertEquals(reader.readInteger(), 2);
+        reader.readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>skipElement</CODE> method.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testSkipElementIncompleteRead() throws Exception {
+        // An ASN.1 sequence of booleans missing one boolean element at the end
+        final byte[] b = new byte[] { 0x30, 0x09, 0x01, 0x01, 0x00, 0x01, 0x02 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        reader.readBoolean();
+        reader.skipElement();
+        reader.readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>skipElement</CODE> method providing a specific type.
+     */
+    @Test
+    public void testSkipElementWithType() throws Exception {
+        final byte[] b =
+                new byte[] { 0x30, 0x09, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        reader.skipElement(ASN1.UNIVERSAL_INTEGER_TYPE);
+        reader.skipElement(ASN1.UNIVERSAL_INTEGER_TYPE);
+        assertEquals(reader.readInteger(), 2);
+        reader.readEndSequence();
+    }
+
+    /**
+     * Tests the <CODE>skipElement</CODE> method providing a wrong type.
+     */
+    @Test(expectedExceptions = { DecodeException.class, IOException.class })
+    public void testSkipElementWithWrongType() throws Exception {
+        final byte[] b =
+                new byte[] { 0x30, 0x09, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02 };
+        final ASN1Reader reader = getReader(b, 0);
+        reader.readStartSequence();
+        reader.readInteger();
+        reader.skipElement(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        assertEquals(reader.readInteger(), 2);
+        reader.readEndSequence();
+    }
+
+    /**
+     * Gets the reader to be use for the unit tests.
+     *
+     * @param b
+     *            The array of bytes to be read.
+     * @param maxElementSize
+     *            The max element size.
+     * @return The reader to be use for the unit tests.
+     * @throws IOException
+     *             In an unexpected IO exception occurred.
+     */
+    protected abstract ASN1Reader getReader(byte[] b, int maxElementSize) throws IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1WriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1WriterTestCase.java
new file mode 100644
index 0000000..b68dc77
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1WriterTestCase.java
@@ -0,0 +1,595 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+import static org.testng.Assert.*;
+
+/**
+ * An abstract base class for all ASN1Writer test cases.
+ */
+@Test(groups = { "precommit", "asn1", "sdk" })
+@SuppressWarnings("javadoc")
+public abstract class ASN1WriterTestCase extends ForgeRockTestCase {
+
+    /**
+     * Create an array with a selection of the valid single-byte types. We don't
+     * support multi-byte types, so this should be a comprehensive data set.
+     */
+    private final byte[] testTypes = { 0x00, 0x7f, (byte) 0x80, (byte) 0xff };
+
+    /**
+     * Create byte arrays to use for element values.
+     *
+     * @return A list of byte arrays that can be used as element values.
+     */
+    @DataProvider(name = "binaryValues")
+    public Object[][] getBinaryValues() {
+        // NOTE -- Don't make these arrays too big since they consume memory.
+        return new Object[][] { new Object[] { new byte[0x00] }, // The
+                                                                 // zero-byte
+            // value
+            new Object[] { new byte[0x01] }, // The single-byte value
+            new Object[] { new byte[0x7F] }, // The largest 1-byte length
+                                             // encoding
+            new Object[] { new byte[0x80] }, // The smallest 2-byte length
+                                             // encoding
+            new Object[] { new byte[0xFF] }, // The largest 2-byte length
+                                             // encoding
+            new Object[] { new byte[0x0100] }, // The smallest 3-byte length
+            // encoding
+            new Object[] { new byte[0xFFFF] }, // The largest 3-byte length
+                                               // encoding
+            new Object[] { new byte[0x010000] } // The smallest 4-byte length
+        // encoding
+        };
+    }
+
+    /**
+     * Retrieves the set of boolean values that may be used for testing.
+     *
+     * @return The set of boolean values that may be used for testing.
+     */
+    @DataProvider(name = "booleanValues")
+    public Object[][] getBooleanValues() {
+        return new Object[][] { new Object[] { false }, new Object[] { true } };
+    }
+
+    /**
+     * Retrieves the set of int values that should be used for testing.
+     *
+     * @return The set of int values that should be used for testing.
+     */
+    @DataProvider(name = "intValues")
+    public Object[][] getIntValues() {
+        return new Object[][] { new Object[] { 0x00000000, 1 }, new Object[] { 0x00000001, 1 },
+            new Object[] { 0x0000000F, 1 }, new Object[] { 0x00000010, 1 },
+            new Object[] { 0x0000007F, 1 }, new Object[] { 0x00000080, 2 },
+            new Object[] { 0x000000FF, 2 }, new Object[] { 0x00000100, 2 },
+            new Object[] { 0x00000FFF, 2 }, new Object[] { 0x00001000, 2 },
+            new Object[] { 0x0000FFFF, 3 }, new Object[] { 0x00010000, 3 },
+            new Object[] { 0x000FFFFF, 3 }, new Object[] { 0x00100000, 3 },
+            new Object[] { 0x00FFFFFF, 4 }, new Object[] { 0x01000000, 4 },
+            new Object[] { 0x0FFFFFFF, 4 }, new Object[] { 0x10000000, 4 },
+            new Object[] { 0x7FFFFFFF, 4 }, new Object[] { -0x00000001, 1 },
+            new Object[] { -0x0000000F, 1 }, new Object[] { -0x00000010, 1 },
+            new Object[] { -0x0000007F, 1 }, new Object[] { -0x00000080, 1 },
+            new Object[] { -0x000000FF, 2 }, new Object[] { -0x00000100, 2 },
+            new Object[] { -0x00000FFF, 2 }, new Object[] { -0x00001000, 2 },
+            new Object[] { -0x0000FFFF, 3 }, new Object[] { -0x00010000, 3 },
+            new Object[] { -0x000FFFFF, 3 }, new Object[] { -0x00100000, 3 },
+            new Object[] { -0x00FFFFFF, 4 }, new Object[] { -0x01000000, 4 },
+            new Object[] { -0x0FFFFFFF, 4 }, new Object[] { -0x10000000, 4 },
+            new Object[] { -0x7FFFFFFF, 4 }, new Object[] { 0x80000000, 4 } };
+    }
+
+    /**
+     * Retrieves the set of long values that should be used for testing.
+     *
+     * @return The set of long values that should be used for testing.
+     */
+    @DataProvider(name = "longValues")
+    public Object[][] getLongValues() {
+        return new Object[][] { new Object[] { 0x0000000000000000L, 1 },
+            new Object[] { 0x0000000000000001L, 1 }, new Object[] { 0x000000000000007FL, 1 },
+            new Object[] { 0x0000000000000080L, 2 }, new Object[] { 0x00000000000000FFL, 2 },
+            new Object[] { 0x0000000000000100L, 2 }, new Object[] { 0x000000000000FFFFL, 3 },
+            new Object[] { 0x0000000000010000L, 3 }, new Object[] { 0x0000000000FFFFFFL, 4 },
+            new Object[] { 0x0000000001000000L, 4 }, new Object[] { 0x00000000FFFFFFFFL, 5 },
+            new Object[] { 0x0000000100000000L, 5 }, new Object[] { 0x000000FFFFFFFFFFL, 6 },
+            new Object[] { 0x0000010000000000L, 6 }, new Object[] { 0x0000FFFFFFFFFFFFL, 7 },
+            new Object[] { 0x0001000000000000L, 7 }, new Object[] { 0x00FFFFFFFFFFFFFFL, 8 },
+            new Object[] { 0x0100000000000000L, 8 }, new Object[] { 0x7FFFFFFFFFFFFFFFL, 8 },
+            new Object[] { -0x0000000000000001L, 1 }, new Object[] { -0x000000000000007FL, 1 },
+            new Object[] { -0x0000000000000080L, 1 }, new Object[] { -0x00000000000000FFL, 2 },
+            new Object[] { -0x0000000000000100L, 2 }, new Object[] { -0x000000000000FFFFL, 3 },
+            new Object[] { -0x0000000000010000L, 3 }, new Object[] { -0x0000000000FFFFFFL, 4 },
+            new Object[] { -0x0000000001000000L, 4 }, new Object[] { -0x00000000FFFFFFFFL, 5 },
+            new Object[] { -0x0000000100000000L, 5 }, new Object[] { -0x000000FFFFFFFFFFL, 6 },
+            new Object[] { -0x0000010000000000L, 6 }, new Object[] { -0x0000FFFFFFFFFFFFL, 7 },
+            new Object[] { -0x0001000000000000L, 7 }, new Object[] { -0x00FFFFFFFFFFFFFFL, 8 },
+            new Object[] { -0x0100000000000000L, 8 }, new Object[] { -0x7FFFFFFFFFFFFFFFL, 8 },
+            new Object[] { 0x8000000000000000L, 8 } };
+    }
+
+    /**
+     * Create strings to use for element values.
+     *
+     * @return A list of strings that can be used as element values.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @DataProvider(name = "stringValues")
+    public Object[][] getStringValues() throws Exception {
+        return new Object[][] { new Object[] { null }, new Object[] { "" },
+            new Object[] { "\u0000" }, new Object[] { "\t" }, new Object[] { "\n" },
+            new Object[] { "\r\n" }, new Object[] { " " }, new Object[] { "a" },
+            new Object[] { "Test1\tTest2\tTest3" }, new Object[] { "Test1\nTest2\nTest3" },
+            new Object[] { "Test1\r\nTest2\r\nTest3" },
+            new Object[] { "The Quick Brown Fox Jumps Over The Lazy Dog" },
+            new Object[] { "\u00BFD\u00F3nde est\u00E1 el ba\u00F1o?" } };
+    }
+
+    /**
+     * Tests the <CODE>write/readBoolean</CODE> methods.
+     *
+     * @param b
+     *            The boolean value to use in the test.
+     */
+    @Test(dataProvider = "booleanValues")
+    public void testEncodeDecodeBoolean(final boolean b) throws Exception {
+        getWriter().writeBoolean(b);
+
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), 1);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_BOOLEAN_TYPE);
+        assertEquals(r.readBoolean(), b);
+    }
+
+    /**
+     * Tests the <CODE>write/readBoolean</CODE> methods.
+     *
+     * @param b
+     *            The boolean value to use in the test.
+     */
+    @Test(dataProvider = "booleanValues")
+    public void testEncodeDecodeBooleanType(final boolean b) throws Exception {
+        for (final byte type : testTypes) {
+            getWriter().writeBoolean(type, b);
+
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), 1);
+            assertEquals(r.peekType(), type);
+            assertEquals(r.readBoolean(), b);
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readInteger</CODE> methods with Java ints.
+     *
+     * @param i
+     *            The integer value to use for the test.
+     */
+    @Test(dataProvider = "intValues")
+    public void testEncodeDecodeEnuerated(final int i, final int length) throws Exception {
+        getWriter().writeEnumerated(i);
+
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_ENUMERATED_TYPE);
+        assertEquals(r.readInteger(), i);
+    }
+
+    /**
+     * Tests the <CODE>write/readInteger</CODE> methods with Java ints.
+     *
+     * @param i
+     *            The integer value to use for the test.
+     */
+    @Test(dataProvider = "intValues")
+    public void testEncodeDecodeInteger(final int i, final int length) throws Exception {
+        getWriter().writeInteger(i);
+
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_INTEGER_TYPE);
+        assertEquals(r.readInteger(), i);
+    }
+
+    /**
+     * Tests the <CODE>write/readInteger</CODE> methods with Java longs.
+     *
+     * @param l
+     *            The long value to use for the test.
+     */
+    @Test(dataProvider = "longValues")
+    public void testEncodeDecodeInteger(final long l, final int length) throws Exception {
+        getWriter().writeInteger(l);
+
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_INTEGER_TYPE);
+        assertEquals(r.readInteger(), l);
+    }
+
+    /**
+     * Tests the <CODE>write/readInteger</CODE> methods with Java ints.
+     *
+     * @param i
+     *            The integer value to use for the test.
+     */
+    @Test(dataProvider = "intValues")
+    public void testEncodeDecodeIntegerType(final int i, final int length) throws Exception {
+        for (final byte type : testTypes) {
+            getWriter().writeInteger(type, i);
+
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), length);
+            assertEquals(r.peekType(), type);
+            assertEquals(r.readInteger(), i);
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readInteger</CODE> methods wiht JavaLongs.
+     *
+     * @param l
+     *            The long value to use for the test.
+     */
+    @Test(dataProvider = "longValues")
+    public void testEncodeDecodeIntegerType(final long l, final int length) throws Exception {
+        for (final byte type : testTypes) {
+            getWriter().writeInteger(type, l);
+
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), length);
+            assertEquals(r.peekType(), type);
+            assertEquals(r.readInteger(), l);
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readNull</CODE> methods.
+     */
+    @Test
+    public void testEncodeDecodeNull() throws Exception {
+        getWriter().writeNull();
+
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), 0);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_NULL_TYPE);
+        r.readNull();
+    }
+
+    /**
+     * Tests the <CODE>write/readNull</CODE> methods.
+     */
+    @Test
+    public void testEncodeDecodeNullType() throws Exception {
+        for (final byte type : testTypes) {
+            getWriter().writeNull(type);
+
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), 0);
+            assertEquals(r.peekType(), type);
+            r.readNull();
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readOctetString</CODE> methods.
+     */
+    @Test(dataProvider = "binaryValues")
+    public void testEncodeDecodeOctetString(final byte[] b) throws Exception {
+        final ByteString bs = ByteString.wrap(b);
+
+        getWriter().writeOctetString(bs);
+
+        ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), b.length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        assertTrue(bs.equals(r.readOctetString()));
+
+        getWriter().writeOctetString(b, 0, b.length);
+
+        r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), b.length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        assertTrue(bs.equals(r.readOctetString()));
+    }
+
+    /**
+     * Tests the <CODE>write/readOctetString</CODE> methods.
+     */
+    @Test(dataProvider = "stringValues")
+    public void testEncodeDecodeOctetString(final String s) throws Exception {
+        getWriter().writeOctetString(s);
+
+        final String expected = s != null ? s : "";
+        final ASN1Reader r = getReader(getEncodedBytes());
+        assertEquals(r.peekLength(), StaticUtils.getBytes(expected).length);
+        assertEquals(r.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+        assertEquals(r.readOctetStringAsString(), expected);
+    }
+
+    /**
+     * Tests the <CODE>write/readOctetString</CODE> methods.
+     */
+    @Test
+    public void testEncodeDecodeOctetStringOffLen() throws Exception {
+        final byte[] b = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
+
+        for (int i = 0; i < 5; i += 2) {
+            final byte[] bsb = new byte[3];
+            System.arraycopy(b, i, bsb, 0, 3);
+            final ByteString bs = ByteString.wrap(bsb);
+            getWriter().writeOctetString(b, i, 3);
+
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), 3);
+            assertEquals(r.peekType(), ASN1.UNIVERSAL_OCTET_STRING_TYPE);
+            assertTrue(bs.equals(r.readOctetString()));
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readOctetString</CODE> methods.
+     */
+    @Test(dataProvider = "binaryValues")
+    public void testEncodeDecodeOctetStringType(final byte[] b) throws Exception {
+        final ByteString bs = ByteString.wrap(b);
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+
+        for (final byte type : testTypes) {
+            bsb.clear();
+            getWriter().writeOctetString(type, bs);
+
+            ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), b.length);
+            assertEquals(r.peekType(), type);
+            r.readOctetString(bsb);
+            assertTrue(bs.equals(bsb));
+
+            bsb.clear();
+            getWriter().writeOctetString(type, b, 0, b.length);
+
+            r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), b.length);
+            assertEquals(r.peekType(), type);
+            r.readOctetString(bsb);
+            assertTrue(bs.equals(bsb));
+        }
+    }
+
+    /**
+     * Tests the <CODE>write/readOctetString</CODE> methods.
+     */
+    @Test(dataProvider = "stringValues")
+    public void testEncodeDecodeOctetStringType(final String s) throws Exception {
+        for (final byte type : testTypes) {
+            getWriter().writeOctetString(type, s);
+
+            final String expected = s != null ? s : "";
+            final ASN1Reader r = getReader(getEncodedBytes());
+            assertEquals(r.peekLength(), StaticUtils.getBytes(expected).length);
+            assertEquals(r.peekType(), type);
+            assertEquals(r.readOctetStringAsString(), expected);
+        }
+    }
+
+    @Test
+    public void testEncodeDecodeSequence() throws Exception {
+        final ASN1Writer writer = getWriter();
+
+        writer.writeStartSequence();
+
+        writer.writeBoolean(true);
+        writer.writeBoolean(false);
+        writer.writeInteger(0);
+        writer.writeInteger(10L);
+        writer.writeNull();
+        writer.writeOctetString("test value");
+        writer.writeOctetString("skip value");
+
+        writer.writeStartSequence();
+        writer.writeOctetString("nested sequence");
+        writer.writeEndSequence();
+
+        writer.writeStartSet();
+        writer.writeOctetString("nested set");
+        writer.writeEndSet();
+
+        writer.writeEndSequence();
+
+        final ASN1Reader reader = getReader(getEncodedBytes());
+        assertEquals(reader.peekType(), ASN1.UNIVERSAL_SEQUENCE_TYPE);
+        assertEquals(reader.peekLength(), 71);
+
+        assertTrue(reader.hasNextElement());
+        reader.readStartSequence();
+        assertTrue(reader.hasNextElement());
+
+        assertEquals(true, reader.readBoolean());
+        assertEquals(false, reader.readBoolean());
+        assertEquals(0, reader.readInteger());
+        assertEquals(10, reader.readInteger());
+        reader.readNull();
+        assertEquals("test value", reader.readOctetStringAsString());
+        reader.skipElement();
+
+        assertEquals(reader.peekLength(), 17);
+        assertEquals(reader.peekType(), ASN1.UNIVERSAL_SEQUENCE_TYPE);
+        reader.readStartSequence();
+        assertEquals("nested sequence", reader.readOctetStringAsString());
+        reader.readEndSequence();
+
+        assertEquals(reader.peekLength(), 12);
+        assertEquals(reader.peekType(), ASN1.UNIVERSAL_SET_TYPE);
+        reader.readStartSequence();
+        assertEquals("nested set", reader.readOctetStringAsString());
+        reader.readEndSequence();
+
+        assertFalse(reader.hasNextElement());
+        reader.readEndSequence();
+        assertFalse(reader.elementAvailable());
+    }
+
+    /**
+     * Tests that negative integers are encoded according to ASN.1 BER
+     * specification.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testNegativeIntEncoding() throws Exception {
+        // Some negative integers of interest
+        // to test specific ranges/boundaries.
+        getWriter().writeInteger(-1);
+        byte[] value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+
+        getWriter().writeInteger(-2);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFE);
+
+        getWriter().writeInteger(-127);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x81);
+
+        getWriter().writeInteger(-128);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+
+        getWriter().writeInteger(-255);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x01);
+
+        getWriter().writeInteger(-256);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+
+        getWriter().writeInteger(-65535);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x01);
+
+        getWriter().writeInteger(-65536);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+
+        getWriter().writeInteger(-2147483647);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+        assertEquals(value[5], (byte) 0x01);
+
+        getWriter().writeInteger(-2147483648);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+        assertEquals(value[5], (byte) 0x00);
+    }
+
+    /**
+     * Tests that negative integers are encoded according to ASN.1 BER
+     * specification.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testNegativeLongEncoding() throws Exception {
+        // Some negative integers of interest
+        // to test specific ranges/boundaries.
+        getWriter().writeInteger(-1L);
+        byte[] value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+
+        getWriter().writeInteger(-2L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFE);
+
+        getWriter().writeInteger(-127L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x81);
+
+        getWriter().writeInteger(-128L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+
+        getWriter().writeInteger(-255L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x01);
+
+        getWriter().writeInteger(-256L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+
+        getWriter().writeInteger(-65535L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x01);
+
+        getWriter().writeInteger(-65536L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0xFF);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+
+        getWriter().writeInteger(-2147483647L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+        assertEquals(value[5], (byte) 0x01);
+
+        getWriter().writeInteger(-2147483648L);
+        value = getEncodedBytes();
+        assertEquals(value[2], (byte) 0x80);
+        assertEquals(value[3], (byte) 0x00);
+        assertEquals(value[4], (byte) 0x00);
+        assertEquals(value[5], (byte) 0x00);
+    }
+
+    protected abstract byte[] getEncodedBytes() throws IOException, DecodeException;
+
+    protected abstract ASN1Reader getReader(byte[] encodedBytes) throws DecodeException,
+            IOException;
+
+    protected abstract ASN1Writer getWriter() throws IOException;
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java
new file mode 100644
index 0000000..5ba17fe
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java
@@ -0,0 +1,510 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.io;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CancelExtendedRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.Options;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Support class for testing {@code LDAPWriter} and {@code LDAPReader} classes
+ * in a specific transport provider.
+ * <p>
+ * Exercices a write and a read for all LDAP messages supported by the
+ * {@code LDAPMessageHandler} class.
+ * <p>
+ * A specific transport provider should provide a test case by :
+ * <ul>
+ * <li>Extending this class</li>
+ * <li>Implementing the 3 abstract methods {@code getLDAPReader()},
+ * {@code getLDAPReader()} and {@code transferFromWriterToReader()}</li>
+ * </ul>
+ */
+@SuppressWarnings("javadoc")
+public abstract class LDAPReaderWriterTestCase extends SdkTestCase {
+
+    /** Message ID is used in all tests. */
+    private static final int MESSAGE_ID = 0;
+
+    /** DN used is several tests. */
+    private static final String TEST_DN = "cn=test";
+
+    interface LDAPWrite {
+        void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException;
+    }
+
+    @DataProvider
+    protected Object[][] messagesFactories() {
+        return new Object[][] { abandonRequest(), addRequest(), addResult(), abandonRequest(),
+            bindRequest(), bindResult(), compareRequest(), compareResult(), deleteRequest(),
+            deleteResult(), extendedRequest(), extendedResult(), intermediateResponse(),
+            modifyDNRequest(), modifyDNResult(), modifyRequest(), modifyResult(), searchRequest(),
+            searchResult(), searchResultEntry(), searchResultReference(), unbindRequest(),
+            unrecognizedMessage() };
+    }
+
+    Object[] abandonRequest() {
+        final int requestID = 1;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeAbandonRequest(MESSAGE_ID, Requests.newAbandonRequest(requestID));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void abandonRequest(int messageID, AbandonRequest request)
+                    throws DecodeException, IOException {
+                assertThat(request.getRequestID()).isEqualTo(requestID);
+            }
+        } };
+    }
+
+    Object[] addRequest() {
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeAddRequest(MESSAGE_ID, Requests.newAddRequest(TEST_DN));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void addRequest(int messageID, AddRequest request) throws DecodeException,
+                    IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+            }
+        } };
+    }
+
+    Object[] addResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeAddResult(MESSAGE_ID, Responses.newResult(resultCode).setMatchedDN(
+                        TEST_DN));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void addResult(int messageID, Result result) throws DecodeException, IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+                assertThat(result.getMatchedDN()).isEqualTo(TEST_DN);
+            }
+        } };
+    }
+
+    Object[] bindRequest() {
+        final int version = 1;
+        final byte type = 0x01;
+        final byte[] value = new byte[] { 0x01, 0x02 };
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeBindRequest(MESSAGE_ID, version, Requests.newGenericBindRequest(
+                        TEST_DN, type, value));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void bindRequest(final int messageID, final int version,
+                    final GenericBindRequest request) throws DecodeException, IOException {
+                assertThat(request.getAuthenticationType()).isEqualTo(type);
+                assertThat(request.getAuthenticationValue()).isEqualTo(value);
+                assertThat(request.getName()).isEqualTo(TEST_DN);
+            }
+        } };
+    }
+
+    Object[] bindResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeBindResult(MESSAGE_ID, Responses.newBindResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void bindResult(final int messageID, final BindResult result)
+                    throws DecodeException, IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] compareRequest() {
+        final String description = "cn";
+        final String value = "test";
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeCompareRequest(MESSAGE_ID, Requests.newCompareRequest(TEST_DN,
+                        description, value));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void compareRequest(final int messageID, final CompareRequest request)
+                    throws DecodeException, IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+                assertThat(request.getAttributeDescription().toString()).isEqualTo(description);
+                assertThat(request.getAssertionValue().toString()).isEqualTo(value);
+            }
+        } };
+    }
+
+    Object[] compareResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeCompareResult(MESSAGE_ID, Responses.newCompareResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void compareResult(int messageID, CompareResult result) throws DecodeException,
+                    IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] deleteRequest() {
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeDeleteRequest(MESSAGE_ID, Requests.newDeleteRequest(TEST_DN));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void deleteRequest(int messageID, DeleteRequest request) throws DecodeException,
+                    IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+            }
+        } };
+    }
+
+    Object[] deleteResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeDeleteResult(MESSAGE_ID, Responses.newResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void deleteResult(int messageID, Result result) throws DecodeException,
+                    IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] extendedRequest() {
+        final int requestID = 1;
+        final String oidCancel = CancelExtendedRequest.OID;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeExtendedRequest(MESSAGE_ID, Requests
+                        .newCancelExtendedRequest(requestID));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public <R extends ExtendedResult> void extendedRequest(int messageID,
+                    ExtendedRequest<R> request) throws DecodeException, IOException {
+                CancelExtendedRequest cancelRequest =
+                        CancelExtendedRequest.DECODER.decodeExtendedRequest(request,
+                            Options.defaultOptions().get(LDAP_DECODE_OPTIONS));
+                assertThat(cancelRequest.getOID()).isEqualTo(oidCancel);
+                assertThat(cancelRequest.getRequestID()).isEqualTo(requestID);
+            }
+        } };
+    }
+
+    Object[] extendedResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        final String oidCancel = CancelExtendedRequest.OID;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeExtendedResult(MESSAGE_ID, Responses.newGenericExtendedResult(
+                        resultCode).setOID(oidCancel));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void extendedResult(int messageID, ExtendedResult result)
+                    throws DecodeException, IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+                assertThat(result.getOID()).isEqualTo(oidCancel);
+            }
+        } };
+    }
+
+    Object[] intermediateResponse() {
+        final String oid = "1.2.3";
+        final String responseValue = "value";
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeIntermediateResponse(MESSAGE_ID, Responses
+                        .newGenericIntermediateResponse(oid, responseValue));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void intermediateResponse(int messageID, IntermediateResponse response)
+                    throws DecodeException, IOException {
+                assertThat(response.getOID()).isEqualTo(oid);
+                assertThat(response.getValue()).isEqualTo(ByteString.valueOfUtf8(responseValue));
+            }
+        } };
+    }
+
+    Object[] modifyDNRequest() {
+        final String newRDN = "cn=test2";
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeModifyDNRequest(MESSAGE_ID, Requests
+                        .newModifyDNRequest(TEST_DN, newRDN));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void modifyDNRequest(int messageID, ModifyDNRequest request)
+                    throws DecodeException, IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+                assertThat(request.getNewRDN().toString()).isEqualTo(newRDN);
+            }
+        } };
+    }
+
+    Object[] modifyDNResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeModifyDNResult(MESSAGE_ID, Responses.newResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void modifyDNResult(int messageID, Result result) throws DecodeException,
+                    IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] modifyRequest() {
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeModifyRequest(MESSAGE_ID, Requests.newModifyRequest(TEST_DN));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void modifyRequest(int messageID, ModifyRequest request) throws DecodeException,
+                    IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+            }
+        } };
+    }
+
+    Object[] modifyResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeModifyResult(MESSAGE_ID, Responses.newResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void modifyResult(int messageID, Result result) throws DecodeException,
+                    IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] searchRequest() {
+        final SearchScope scope = SearchScope.BASE_OBJECT;
+        final String filter = "(&(objectClass=person)(objectClass=user))";
+        final String attribute = "cn";
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeSearchRequest(MESSAGE_ID, Requests.newSearchRequest(TEST_DN, scope,
+                        filter, attribute));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void searchRequest(int messageID, SearchRequest request) throws DecodeException,
+                    IOException {
+                assertThat(request.getName().toString()).isEqualTo(TEST_DN);
+                assertThat(request.getScope()).isEqualTo(scope);
+                assertThat(request.getFilter().toString()).isEqualTo(filter);
+                assertThat(request.getAttributes()).containsExactly(attribute);
+            }
+        } };
+    }
+
+    Object[] searchResult() {
+        final ResultCode resultCode = ResultCode.SUCCESS;
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeSearchResult(MESSAGE_ID, Responses.newResult(resultCode));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void searchResult(int messageID, Result result) throws DecodeException,
+                    IOException {
+                assertThat(result.getResultCode()).isEqualTo(resultCode);
+            }
+        } };
+    }
+
+    Object[] searchResultEntry() {
+        final Entry entry =
+                new LinkedHashMapEntry("dn: cn=test", "objectClass: top", "objectClass: test");
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeSearchResultEntry(1, Responses.newSearchResultEntry(entry));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void searchResultEntry(int messageID, SearchResultEntry resultEntry)
+                    throws DecodeException, IOException {
+                assertThat(resultEntry).isEqualTo(entry);
+            }
+        } };
+    }
+
+    Object[] searchResultReference() {
+        final String uri = "ldap://ldap.example.com/cn=test??sub?(sn=Jensen)";
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeSearchResultReference(MESSAGE_ID, Responses
+                        .newSearchResultReference(uri));
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void searchResultReference(int messageID, SearchResultReference reference)
+                    throws DecodeException, IOException {
+                assertThat(reference.getURIs()).containsExactly(uri);
+            }
+        } };
+    }
+
+    Object[] unbindRequest() {
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeUnbindRequest(MESSAGE_ID, Requests.newUnbindRequest());
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void unbindRequest(int messageID, UnbindRequest request) throws DecodeException,
+                    IOException {
+                assertThat(request).isNotNull();
+            }
+        } };
+    }
+
+    Object[] unrecognizedMessage() {
+        final byte messageTag = 0x01;
+        final ByteString messageBytes = ByteString.valueOfUtf8("message");
+        return new Object[] { new LDAPWrite() {
+            @Override
+            public void perform(LDAPWriter<? extends ASN1Writer> writer) throws IOException {
+                writer.writeUnrecognizedMessage(MESSAGE_ID, messageTag, messageBytes);
+            }
+        }, new AbstractLDAPMessageHandler() {
+            @Override
+            public void unrecognizedMessage(int messageID, byte tag, ByteString message)
+                    throws DecodeException, IOException {
+                assertThat(messageID).isEqualTo(MESSAGE_ID);
+                assertThat(tag).isEqualTo(messageTag);
+                assertThat(message).isEqualTo(messageBytes);
+            }
+        } };
+    }
+
+    /**
+     * Test that a LDAP message written by LDAPWriter is read correctly using
+     * LDAPReader.
+     *
+     * @param writing
+     *            write instruction to perform
+     * @param messageHandler
+     *            handler of message read, containing assertion(s) to check that
+     *            message is as expected
+     * @throws Exception
+     */
+    @Test(dataProvider = "messagesFactories")
+    public void testWriteReadMessage(LDAPWrite writing, LDAPMessageHandler messageHandler)
+            throws Exception {
+        LDAPWriter<? extends ASN1Writer> writer = getLDAPWriter();
+        writing.perform(writer);
+        LDAPReader<? extends ASN1Reader> reader = getLDAPReader();
+        transferFromWriterToReader(writer, reader);
+        reader.readMessage(messageHandler);
+    }
+
+    /**
+     * Returns a writer specific to the transport module.
+     */
+    protected abstract LDAPWriter<? extends ASN1Writer> getLDAPWriter();
+
+    /**
+     * Returns a reader specific to the transport module.
+     */
+    protected abstract LDAPReader<? extends ASN1Reader> getLDAPReader();
+
+    /**
+     * Transfer raw data from writer to the reader.
+     */
+    protected abstract void transferFromWriterToReader(LDAPWriter<? extends ASN1Writer> writer,
+            LDAPReader<? extends ASN1Reader> reader);
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AVATestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AVATestCase.java
new file mode 100644
index 0000000..b7ac087
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AVATestCase.java
@@ -0,0 +1,83 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Portions copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** This class defines a set of tests for the {@link AVA} class. */
+@SuppressWarnings("javadoc")
+public class AVATestCase extends SdkTestCase {
+
+    @DataProvider
+    private Object[][] valueOfDataProvider() {
+        AttributeType cnAttrType = Schema.getCoreSchema().getAttributeType("commonName");
+        return new Object[][] {
+            { "CN=value", cnAttrType, "CN", "value" },
+            { "commonname=value", cnAttrType, "commonname", "value" },
+            { "2.5.4.3=#76616C7565", cnAttrType, "2.5.4.3", "value" },
+        };
+    }
+
+    @Test(dataProvider = "valueOfDataProvider")
+    public void valueOf(String avaString, AttributeType expectedAttrType, String expectedAttrName,
+            String expectedValue) throws Exception {
+        AVA ava = AVA.valueOf(avaString);
+        assertThat(ava.getAttributeType()).isEqualTo(expectedAttrType);
+        assertThat(ava.getAttributeName()).isEqualTo(expectedAttrName);
+        assertThat(ava.getAttributeValue()).isEqualTo(ByteString.valueOfUtf8(expectedValue));
+        assertThat(ava.toString()).isEqualTo(avaString);
+    }
+
+    @Test
+    public void hexEncodingDoesNotLoseInformation() throws Exception {
+        final String avaString = "2.5.4.3=#76616C7565";
+        final String roundtrippedValue = AVA.valueOf(avaString).toString();
+        assertThat(AVA.valueOf(roundtrippedValue).toString()).isEqualTo(avaString);
+    }
+
+    @DataProvider
+    public Object[][] toStringShouldStripOutIllegalWhitespaceDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            { " dc = hello  world ", "dc=hello  world" },
+            { " dc =\\  hello  world\\  ", "dc=\\  hello  world\\ " },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toStringShouldStripOutIllegalWhitespaceDataProvider")
+    public void toStringShouldStripOutIllegalWhitespace(String withWhiteSpace, String withoutWhiteSpace) {
+        assertThat(AVA.valueOf(withWhiteSpace).toString()).isEqualTo(withoutWhiteSpace);
+        assertThat(AVA.valueOf(withWhiteSpace).toNormalizedByteString(new ByteStringBuilder()))
+                .isEqualTo(AVA.valueOf(withoutWhiteSpace).toNormalizedByteString(new ByteStringBuilder()));
+    }
+
+    @Test
+    public void avaConstructedWithValueContainingLeadingAndTrailingSpacesShouldBeEscaped() {
+        AVA ava = new AVA("dc", " hello  world ");
+        assertThat(ava.toString()).isEqualTo("dc=\\ hello  world\\ ");
+    }
+
+    @Test
+    public void valueOfDecodesTrailingEscapedChars() {
+        assertThat(AVA.valueOf("dc=\\41\\42\\43").toString()).isEqualTo("dc=ABC");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
new file mode 100644
index 0000000..f62706a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
@@ -0,0 +1,462 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.ResultHandler;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.opendj.ldap.requests.Requests.*;
+import static org.forgerock.opendj.ldap.responses.Responses.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit test for AbstractAsynchronousConnection. The tests verify that all
+ * synchronous operation methods delegate to the equivalent asynchronous method.
+ */
+@SuppressWarnings("javadoc")
+public class AbstractAsynchronousConnectionTestCase extends SdkTestCase {
+    public final class MockConnection extends AbstractAsynchronousConnection {
+        private final ResultCode resultCode;
+        private final SearchResultEntry[] entries;
+
+        public MockConnection(ResultCode resultCode, SearchResultEntry...entries) {
+            this.resultCode = resultCode;
+            this.entries = entries;
+        }
+
+        @Override
+        public LdapPromise<Void> abandonAsync(AbandonRequest request) {
+            if (!resultCode.isExceptional()) {
+                return newSuccessfulLdapPromise((Void) null);
+            } else {
+                return newFailedLdapPromise(newLdapException(resultCode));
+            }
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(AddRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newResult(resultCode));
+        }
+
+        @Override
+        public void addConnectionEventListener(ConnectionEventListener listener) {
+            // Do nothing.
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(BindRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newBindResult(resultCode));
+        }
+
+        @Override
+        public void close(UnbindRequest request, String reason) {
+            // Do nothing.
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(CompareRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newCompareResult(resultCode));
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(DeleteRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newResult(resultCode));
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(request.getResultDecoder().newExtendedErrorResult(resultCode, "", ""));
+        }
+
+        @Override
+        public boolean isClosed() {
+            return false;
+        }
+
+        @Override
+        public boolean isValid() {
+            return true;
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(ModifyRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newResult(resultCode));
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
+                IntermediateResponseHandler intermediateResponseHandler) {
+            return getPromiseFromResultCode(newResult(resultCode));
+        }
+
+        @Override
+        public void removeConnectionEventListener(ConnectionEventListener listener) {
+            // Do nothing.
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(SearchRequest request,
+                IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) {
+            for (SearchResultEntry entry : entries) {
+                entryHandler.handleEntry(entry);
+            }
+
+            return getPromiseFromResultCode(newResult(resultCode));
+        }
+
+        private <T extends Result> LdapPromise<T> getPromiseFromResultCode(T correctResult) {
+            if (resultCode.isExceptional()) {
+                return newFailedLdapPromise(newLdapException(resultCode));
+            } else {
+                return newSuccessfulLdapPromise(correctResult);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "MockConnection";
+        }
+    }
+
+    @Test
+    public void testAddRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final AddRequest addRequest = newAddRequest("cn=test");
+        assertThat(mockConnection.add(addRequest).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testAddRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final AddRequest addRequest = newAddRequest("cn=test");
+        try {
+            mockConnection.add(addRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testBindRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final BindRequest bindRequest = newSimpleBindRequest();
+        assertThat(mockConnection.bind(bindRequest).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testBindRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final BindRequest bindRequest = newSimpleBindRequest();
+        try {
+            mockConnection.bind(bindRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testCompareRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final CompareRequest compareRequest = newCompareRequest("cn=test", "cn", "test");
+        assertThat(mockConnection.compare(compareRequest).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testCompareRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final CompareRequest compareRequest = newCompareRequest("cn=test", "cn", "test");
+        try {
+            mockConnection.compare(compareRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testDeleteRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final DeleteRequest deleteRequest = newDeleteRequest("cn=test");
+        assertThat(mockConnection.delete(deleteRequest).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final DeleteRequest deleteRequest = newDeleteRequest("cn=test");
+        try {
+            mockConnection.delete(deleteRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testExtendedRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final GenericExtendedRequest extendedRequest = newGenericExtendedRequest("test");
+        assertThat(mockConnection.extendedRequest(extendedRequest).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testExtendedRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final GenericExtendedRequest extendedRequest = newGenericExtendedRequest("test");
+        try {
+            mockConnection.extendedRequest(extendedRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testModifyRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final ModifyRequest modifyRequest = newModifyRequest("cn=test");
+        assertThat(mockConnection.modify(modifyRequest).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testModifyRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final ModifyRequest modifyRequest = newModifyRequest("cn=test");
+        try {
+            mockConnection.modify(modifyRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testModifyDNRequestSuccess() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final ModifyDNRequest modifyDNRequest = newModifyDNRequest("cn=test", "cn=newrdn");
+        assertThat(mockConnection.modifyDN(modifyDNRequest).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+    }
+
+    @Test
+    public void testModifyDNRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final ModifyDNRequest modifyDNRequest = newModifyDNRequest("cn=test", "cn=newrdn");
+        try {
+            mockConnection.modifyDN(modifyDNRequest);
+            fail();
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testSearchRequestSuccess() throws Exception {
+        final SearchResultEntry entry = newSearchResultEntry("cn=test");
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
+        final SearchRequest searchRequest =
+                newSearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        List<SearchResultEntry> entries = new LinkedList<>();
+        assertThat(mockConnection.search(searchRequest, entries).getResultCode()).isEqualTo(
+                ResultCode.SUCCESS);
+        assertThat(entries.size()).isEqualTo(1);
+        assertThat(entries.iterator().next()).isSameAs(entry);
+    }
+
+    @Test
+    public void testSearchRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final SearchRequest searchRequest =
+                newSearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        List<SearchResultEntry> entries = new LinkedList<>();
+        try {
+            mockConnection.search(searchRequest, entries);
+            failWasExpected(LdapException.class);
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+            assertThat(entries.isEmpty());
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchRequestSuccess() throws Exception {
+        final SearchResultEntry entry = newSearchResultEntry("cn=test");
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        assertThat(mockConnection.searchSingleEntry(request)).isEqualTo(entry);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testSingleEntrySearchAsyncRequestSuccess() throws Exception {
+        final SearchResultEntry entry = newSearchResultEntry("cn=test");
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        ResultHandler<SearchResultEntry> resultHandler = mock(ResultHandler.class);
+        SearchResultEntry resultEntry = mockConnection.searchSingleEntryAsync(request)
+                                                      .thenOnResult(resultHandler).get();
+        assertThat(resultEntry).isEqualTo(entry);
+        verify(resultHandler).handleResult(any(SearchResultEntry.class));
+    }
+
+    @Test
+    public void testSingleEntrySearchRequestNoEntryReturned() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(EntryNotFoundException.class);
+        } catch (EntryNotFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED);
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchRequestMultipleEntriesToReturn() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
+                newSearchResultEntry("cn=test"));
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchRequestMultipleEntriesReturnedByServer() throws Exception {
+        // could happen if server does not enforce size limit
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, newSearchResultEntry("cn=test"),
+                newSearchResultEntry("cn=test,ou=org"));
+        final SearchRequest request = newSingleEntrySearchRequest("cn=test", SearchScope.WHOLE_SUBTREE,
+                "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testSingleEntrySearchAsyncRequestMultipleEntriesToReturn() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
+                newSearchResultEntry("cn=test"));
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
+
+        try {
+            mockConnection.searchSingleEntryAsync(request).thenOnException(exceptionHandler).getOrThrow();
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+            verify(exceptionHandler).handleException(any(LdapException.class));
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchAsyncRequestMultipleEntriesReturnedByServer() throws Exception {
+        // could happen if server does not enfore size limit
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, newSearchResultEntry("cn=test"),
+                newSearchResultEntry("cn=test,ou=org"));
+        final SearchRequest request = newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT,
+                "(objectClass=*)");
+        @SuppressWarnings("unchecked")
+        ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
+        try {
+            mockConnection.searchSingleEntryAsync(request).thenOnException(exceptionHandler).getOrThrow();
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+            verify(exceptionHandler).handleException(any(LdapException.class));
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(LdapException.class);
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test
+    public void testSingleEntrySearchAsyncRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final SearchRequest request =
+                newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        @SuppressWarnings("unchecked")
+        ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
+        try {
+            mockConnection.searchSingleEntryAsync(request).thenOnException(exceptionHandler).getOrThrow();
+            failWasExpected(LdapException.class);
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+            verify(exceptionHandler).handleException(any(LdapException.class));
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AddressMaskTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AddressMaskTestCase.java
new file mode 100644
index 0000000..7dffda5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AddressMaskTestCase.java
@@ -0,0 +1,242 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class AddressMaskTestCase extends SdkTestCase {
+
+    /** These are all valid rules -- should all pass. */
+    @DataProvider(name = "validRules")
+    public Object[][] validData() {
+        return new Object[][] { { "129.34.55.67" }, { "129.*.78.55" }, { ".central.sun.com" },
+            { "foo.central.sun.com" }, { "foo.*.sun.*" }, { "128.*.*.*" }, { "129.45.23.67/22" },
+            { "128.33.23.21/32" }, { "*.*.*.*" }, { "129.45.67.34/0" }, { "foo.com" }, { "foo" } };
+    }
+
+    @DataProvider(name = "invalidRules")
+    public Object[][] invalidData() {
+        return new Object[][] { { "129.*.900.67" }, { "129.67" }, { "   " },
+            { "129.56.78.90/2000" }, { "677.777.AG.BC" }, { "/34" }, { "234.12.12.*/31" },
+            { "234.12.12.90/" }, { "129.34.56.78/-100" }, { "129" }, { "129.34.-90.67" },
+            { "129.**.56.67" }, { "foo bar.com" }, { "12foo.example.com" }, { "123.45." },
+            { ".central.sun day.com" }, { "129.34.45.45/4/3/" } };
+    }
+
+    @DataProvider(name = "toStringRule")
+    public Object[][] toStringData() {
+        return new Object[][] { { "129.35.45.66/12" } };
+    }
+
+    @Test(dataProvider = "validRules")
+    public void testValidDecode(String mask) {
+        AddressMask.valueOf(mask);
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class,
+            dataProvider = "invalidRules")
+    public void testInvalidDecode(String mask) throws Exception {
+        AddressMask.valueOf(mask);
+    }
+
+    @DataProvider(name = "matchRules")
+    public Object[][] ruleMatchData() {
+        // @Checkstyle:off
+        return new Object[][] { {
+            //Rules
+            new String[] { "129.56.*.22", //1
+                "*.domain.com", //2
+                "foo.example.com", //3
+                "126.67.89.90", //4
+                "90.89.78.67/30", //5
+                ".test.com", //6
+                "128.153.147.32/21", //7
+                "128.153.146.32/26", //8
+                "90.89.78.67/26" }, //9
+            //Addresses
+            new String[] { "128.153.147.45", //rule 7
+                "128.153.146.60", //rule 8
+                "148.45.45.46", //host
+                "129.56.78.22", //rule 1
+                "148.45.45.47", //host
+                "148.45.45.48", //host
+                "90.89.78.65" }, //rule 5
+            //Hostnames
+            new String[] { "some.host.name", //addr
+                "some.host.name", //addr
+                "foo.example.com", //rule 3
+                "some.host.name", //addr
+                "foo.test.com", //rule 6
+                "foo.domain.com", //rule 2
+                "some.host.name" //addr
+            } } };
+        // @Checkstyle:on
+    }
+
+    @DataProvider(name = "noMatchRules")
+    public Object[][] ruleNoMatchData() {
+        // @Checkstyle:off
+        return new Object[][] { {
+            // Rule to not match
+            new String[] { "129.56.*.22", //1
+                "*.domain.com", //2
+                "foo.example.com", //3
+                "126.67.89.90", //4
+                "90.89.78.67/30", //5
+                ".test.com", //6
+                "128.153.147.32/21", //7
+                "128.153.146.32/26", //8
+                "90.89.78.67/26" }, //9
+            //Addresses
+            new String[] { "128.153.140.45", "128.153.143.255", "148.45.45.46", "126.56.78.22",
+                "148.45.45.47", "148.45.45.48", "90.89.78.128", "148.45.45.49" },
+            //Hostnames
+            new String[] { "some.host.name", "some.host.name", "foo.examplee.com",
+                "some.host.name", "foo.ttest.com", "foo.domain.comm", "some.host.name", "f.e.c",
+                "foo.domain.cm" } } };
+        // @Checkstyle:on
+    }
+
+    @DataProvider(name = "matchWCRules")
+    public Object[][] ruleMatchWCData() {
+        // @Checkstyle:off
+        return new Object[][] { {
+            //Rules
+            new String[] { "*.*.*", "*.*.*.*" },
+            //Addresses
+            new String[] { "129.34.45.12", "129.34.45.13" },
+            //Hostnames
+            new String[] { "some.host.name", "some.host.name" } } };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "matchRules")
+    public void testMatch(String[] rules, String[] addrs, String[] hostNames) throws Exception {
+        assertTrue(match(rules, addrs, hostNames));
+    }
+
+    @Test(dataProvider = "matchWCRules")
+    public void testWildCardMatch(String[] rules, String[] addrs, String[] hostNames)
+            throws Exception {
+        assertTrue(match(rules, addrs, hostNames));
+    }
+
+    @Test(dataProvider = "noMatchRules")
+    public void testNoMatch(String[] rules, String[] addrs, String[] hostNames) throws Exception {
+        assertFalse(match(rules, addrs, hostNames));
+    }
+
+    @Test(dataProvider = "toStringRule")
+    public void testToString(String rule) {
+        AddressMask m = AddressMask.valueOf(rule);
+        assertEquals(rule, m.toString());
+    }
+
+    @Test
+    public void testNullMatch() {
+        AddressMask m = AddressMask.valueOf("*.*.*.*");
+        assertFalse(AddressMask.matchesAny(Arrays.asList(m), null));
+    }
+
+    private boolean match(String[] rules, String[] addrs, String[] hostNames)
+            throws UnknownHostException {
+        int i = 0;
+        Collection<AddressMask> m = new ArrayList<>(rules.length);
+        for (i = 0; i < rules.length; i++) {
+            m.add(AddressMask.valueOf(rules[i]));
+        }
+        for (int j = 0; j < addrs.length; j++) {
+            InetAddress addr =
+                    InetAddress.getByAddress(hostNames[j], InetAddress.getByName(addrs[j])
+                            .getAddress());
+            if (!AddressMask.matchesAny(m, addr)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /*
+     * IPV6 data and tests.
+     */
+
+    /** Invalid IPv6 expressions. */
+    @DataProvider(name = "invalid6Rules")
+    public Object[][] inValid6Data() {
+        return new Object[][] { { "2001:feca:ba23:cd1f:dcb1:1010:9234:4088///124" },
+            { "2001:feca:ba23:cd1f:dcb1:1010:9234:4088?124" },
+            { "2001:fecz:ba23:cd1f:dcb1:1010:9234:4088/124" },
+            { "2001:fecd:ba23:cd1ff:dcb1:1010:9234:4088/46" }, { "0:0:0:0:0:ffff:101..45.75.219" },
+            { "0:0:0:0:0:0:101.45.75.700" }, { "1080::8:800:200C:417A/500" },
+            { "1080::8:800:*:417A/66" }, { "2001:fecd:ba23:cd1ff:dcb1:1010:202.45.66.20" }, };
+    }
+
+    /** Valid IPv6 expressions. */
+    @DataProvider(name = "valid6Rules")
+    public Object[][] valid6Data() {
+        return new Object[][] { { "2001:fecd:ba23:cd1f:dcb1:1010:9234:4088/124" },
+            { "2001:fecd:ba23:cd1f:dcb1:1010:9234:4088" },
+            { "[2001:fecd:ba23:cd1f:dcb1:1010:9234:4088]/45" }, { "::/128" }, { "::1/128" },
+            { "::" }, { "0:0:0:0:0:ffff:101.45.75.219" }, { "1080::8:800:200C:417A" },
+            { "0:0:0:0:0:0:101.45.75.219" }, { "::101.45.75.219" } };
+    }
+
+    @DataProvider(name = "match6Rules")
+    public Object[][] ruleMatch6Data() {
+        // @Checkstyle:off
+        return new Object[][] { {
+            //IPV6 Rules
+            new String[] { "[12ab:0:0:cd30::]/60", "::ffff:72.56.78.9", "::", "42ab:0:0:dd30::" },
+            //IPv6 Addresses
+            new String[] { "12ab:0:0:cd3f:0000:0000:23DC:DC30", "72.56.78.9", "::",
+                "42ab:0000:0000:dd30:0000:0000:0000:0000" },
+            //ignored Hostnames
+            new String[] { "ignored.host.name", "ignored.host.name", "ignored.host.name",
+                "ignored.host.name" } } };
+        // @Checkstyle:on
+    }
+
+    @Test(dataProvider = "valid6Rules")
+    public void testValid6Decode(String mask) {
+        AddressMask.valueOf(mask);
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class,
+            dataProvider = "invalid6Rules")
+    public void testInvalid6Decode(String mask) {
+        AddressMask.valueOf(mask);
+    }
+
+    @Test(dataProvider = "match6Rules")
+    public void testMatch6(String[] rules, String[] addrs, String[] hostNames)
+            throws UnknownHostException {
+        assertTrue(match(rules, addrs, hostNames));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java
new file mode 100644
index 0000000..a2c061c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeDescriptionTestCase.java
@@ -0,0 +1,544 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.assertj.core.api.Assertions;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test {@link AttributeDescription}. */
+@SuppressWarnings("javadoc")
+public final class AttributeDescriptionTestCase extends SdkTestCase {
+
+    @DataProvider
+    public Object[][] dataForCompareCoreSchema() {
+        // @formatter:off
+        return new Object[][] {
+            // AD1, AD2, compare result, isSubtype, isSuperType
+            { "cn", "cn", 0, true, true },
+            { "cn", "commonName", 0, true, true },
+            { " cn", "commonName ", 0, true, true },
+            { "commonName", "cn", 0, true, true },
+            { "commonName", "commonName", 0, true, true },
+            { "cn", "objectClass", 1, false, false },
+            { "objectClass", "cn", -1, false, false },
+            { "name", "cn", 1, false, true },
+            { "cn", "name", -1, true, false },
+            { "name;foo", "cn", 1, false, false },
+            { "cn;foo", "name", -1, true, false },
+            { "name", "cn;foo", 1, false, true },
+            { "cn", "name;foo", -1, false, false },
+        };
+        // @formatter:on
+    }
+
+    @DataProvider
+    public Object[][] dataForCompareNoSchema() {
+        // @formatter:off
+        return new Object[][] {
+            // AD1, AD2, compare result, isSubtype, isSuperType
+            { "cn", "cn", 0, true, true },
+            { "cn", "CN", 0, true, true },
+            { "CN", "cn", 0, true, true },
+            { "CN", "CN", 0, true, true },
+            { "cn", "commonName", -1, false, false },
+            { "commonName", "cn", 1, false, false },
+            { "commonName", "commonName", 0, true, true },
+            { "cn", "cn;foo", -1, false, true },
+            { "cn;foo", "cn", 1, true, false },
+            { "cn;foo", "cn;foo", 0, true, true },
+            { "CN;FOO", "cn;foo", 0, true, true },
+            { "cn;foo", "CN;FOO", 0, true, true },
+            { "CN;FOO", "CN;FOO", 0, true, true },
+            { "cn;foo", "cn;bar", 1, false, false },
+            { "cn;bar", "cn;foo", -1, false, false },
+
+            { "cn;xxx;yyy", "cn", 1, true, false },
+            { "cn;xxx;yyy", "cn;yyy", 1, true, false },
+            { "cn;xxx;yyy", "cn;xxx", 1, true, false },
+            { "cn;xxx;yyy", "cn;xxx;yyy", 0, true, true },
+            { "cn;xxx;yyy", "cn;yyy;xxx", 0, true, true },
+
+            { "cn", "cn;xxx;yyy", -1, false, true },
+            { "cn;yyy", "cn;xxx;yyy", -1, false, true },
+            { "cn;xxx", "cn;xxx;yyy", -1, false, true },
+            { "cn;xxx;yyy", "cn;xxx;yyy", 0, true, true },
+            { "cn;yyy;xxx", "cn;xxx;yyy", 0, true, true },
+        };
+        // @formatter:on
+    }
+
+    @DataProvider
+    public Object[][] dataForValueOfCoreSchema() {
+        // @formatter:off
+        return new Object[][] {
+            // Value, type, isObjectClass
+            { "cn", "cn", false },
+            { "CN", "cn", false },
+            { "commonName", "cn", false },
+            { "objectclass", "objectClass", true },
+        };
+        // @formatter:on
+    }
+
+    @DataProvider
+    public Object[][] dataForValueOfInvalidAttributeDescriptions() {
+        // @formatter:off
+        return new Object[][] {
+            { "" },
+            { " " },
+            { ";" },
+            { " ; " },
+            { "0cn" },
+            { "cn+" },
+            { "cn;foo+bar" },
+            { "cn;foo;foo+bar" },
+            { ";foo" },
+            { "cn;" },
+            { "cn;;foo" },
+            { "cn; ;foo" },
+            { "cn;foo;" },
+            { "cn;foo; " },
+            { "cn;foo;;bar" },
+            { "cn;foo; ;bar" },
+            { "cn;foo;bar;;" },
+            { "1a" },
+            { "1.a" },
+            { "1-" },
+            { "1.1a" },
+            { "1.1.a" },
+        };
+        // @formatter:on
+    }
+
+    @DataProvider
+    public Object[][] dataForValueOfNoSchema() {
+        // @formatter:off
+        return new Object[][] {
+            // Value, type, options, containsOptions("foo")
+            { "cn", "cn", new String[0], false },
+            { " cn ", "cn", new String[0], false },
+            { "  cn  ", "cn", new String[0], false },
+            { "CN", "CN", new String[0], false },
+            { "1", "1", new String[0], false },
+            { "1.2", "1.2", new String[0], false },
+            { "1.2.3", "1.2.3", new String[0], false },
+            { "111.222.333", "111.222.333", new String[0], false },
+            { "objectClass", "objectClass", new String[0], false },
+            { "cn;foo", "cn", new String[] { "foo" }, true },
+            { "cn;FOO", "cn", new String[] { "FOO" }, true },
+            { "cn;bar", "cn", new String[] { "bar" }, false },
+            { "cn;BAR", "cn", new String[] { "BAR" }, false },
+            { "cn;foo;bar", "cn", new String[] { "foo", "bar" }, true },
+            { "cn;FOO;bar", "cn", new String[] { "FOO", "bar" }, true },
+            { "cn;foo;BAR", "cn", new String[] { "foo", "BAR" }, true },
+            { "cn;FOO;BAR", "cn", new String[] { "FOO", "BAR" }, true },
+            { "cn;bar;FOO", "cn", new String[] { "bar", "FOO" }, true },
+            { "cn;BAR;foo", "cn", new String[] { "BAR", "foo" }, true },
+            { "cn;bar;FOO", "cn", new String[] { "bar", "FOO" }, true },
+            { "cn;BAR;FOO", "cn", new String[] { "BAR", "FOO" }, true },
+            { " cn;BAR;FOO ", "cn", new String[] { "BAR", "FOO" }, true },
+            { "  cn;BAR;FOO  ", "cn", new String[] { "BAR", "FOO" }, true },
+            { "  CN;BAR;FOO  ", "CN", new String[] { "BAR", "FOO" }, true },
+            { "cn;xxx;yyy;zzz", "cn", new String[] { "xxx", "yyy", "zzz" }, false },
+            { "cn;zzz;YYY;xxx", "cn", new String[] { "zzz", "YYY", "xxx" }, false },
+            { "CN;zzz;YYY;xxx", "CN", new String[] { "zzz", "YYY", "xxx" }, false },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "dataForCompareCoreSchema")
+    public void testCompareCoreSchema(final String ad1, final String ad2, final int compare,
+            final boolean isSubType, final boolean isSuperType) {
+        Schema schema = Schema.getCoreSchema();
+        compare0(ad1, ad2, compare, isSubType, isSuperType, schema);
+    }
+
+    @Test(dataProvider = "dataForCompareNoSchema")
+    public void testCompareNoSchema(final String ad1, final String ad2, final int compare,
+            final boolean isSubType, final boolean isSuperType) {
+        Schema schema = Schema.getEmptySchema();
+        compare0(ad1, ad2, compare, isSubType, isSuperType, schema);
+    }
+
+    private void compare0(final String ad1, final String ad2, final int compare,
+            final boolean isSubType, final boolean isSuperType, final Schema schema) {
+        final AttributeDescription attributeDescription1 = AttributeDescription.valueOf(ad1, schema);
+        final AttributeDescription attributeDescription2 = AttributeDescription.valueOf(ad2, schema);
+
+        // Identity.
+        assertEquals(attributeDescription1, attributeDescription1);
+        assertEquals(attributeDescription1.compareTo(attributeDescription1), 0);
+        assertTrue(attributeDescription1.isSubTypeOf(attributeDescription1));
+        assertTrue(attributeDescription1.isSuperTypeOf(attributeDescription1));
+
+        if (compare == 0) {
+            assertEquals(attributeDescription1, attributeDescription2);
+            assertEquals(attributeDescription2, attributeDescription1);
+            assertEquals(attributeDescription1.compareTo(attributeDescription2), 0);
+            assertEquals(attributeDescription2.compareTo(attributeDescription1), 0);
+
+            assertTrue(attributeDescription1.isSubTypeOf(attributeDescription2));
+            assertTrue(attributeDescription1.isSuperTypeOf(attributeDescription2));
+            assertTrue(attributeDescription2.isSubTypeOf(attributeDescription1));
+            assertTrue(attributeDescription2.isSuperTypeOf(attributeDescription1));
+        } else {
+            assertFalse(attributeDescription1.equals(attributeDescription2));
+            assertFalse(attributeDescription2.equals(attributeDescription1));
+
+            if (compare < 0) {
+                assertTrue(attributeDescription1.compareTo(attributeDescription2) < 0);
+                assertTrue(attributeDescription2.compareTo(attributeDescription1) > 0);
+            } else {
+                assertTrue(attributeDescription1.compareTo(attributeDescription2) > 0);
+                assertTrue(attributeDescription2.compareTo(attributeDescription1) < 0);
+            }
+
+            assertEquals(attributeDescription1.isSubTypeOf(attributeDescription2), isSubType);
+            assertEquals(attributeDescription1.isSuperTypeOf(attributeDescription2), isSuperType);
+        }
+    }
+
+    @Test(dataProvider = "dataForValueOfCoreSchema")
+    public void testValueOfCoreSchema(final String ad, final String at, final boolean isObjectClass) {
+        final AttributeDescription attributeDescription =
+                AttributeDescription.valueOf(ad, Schema.getCoreSchema());
+
+        assertEquals(attributeDescription.toString(), ad);
+        assertEquals(attributeDescription.getNameOrOID(), ad);
+        assertEquals(attributeDescription.getAttributeType().getNameOrOID(), at);
+        assertEquals(attributeDescription.isObjectClass(), isObjectClass);
+
+        assertFalse(attributeDescription.hasOptions());
+        assertFalse(attributeDescription.hasOption("dummy"));
+
+        Assertions.assertThat(attributeDescription.getOptions()).isEmpty();
+    }
+
+    /** FIXME: none of these pass! The valueOf method is far too lenient. */
+    @Test(dataProvider = "dataForValueOfInvalidAttributeDescriptions",
+            expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfInvalidAttributeDescriptions(final String ad) {
+        AttributeDescription.valueOf(ad, Schema.getEmptySchema());
+    }
+
+    @Test(dataProvider = "dataForValueOfNoSchema")
+    public void testValueOfNoSchema(final String ad, final String at, final String[] options,
+            final boolean containsFoo) {
+        final AttributeDescription attributeDescription =
+                AttributeDescription.valueOf(ad, Schema.getEmptySchema());
+
+        assertEquals(attributeDescription.toString(), ad);
+        assertEquals(attributeDescription.getAttributeType().getNameOrOID(), at);
+        assertFalse(attributeDescription.isObjectClass());
+
+        assertOptions(attributeDescription, options);
+        assertFalse(attributeDescription.hasOption("dummy"));
+        if (containsFoo) {
+            assertTrue(attributeDescription.hasOption("foo"));
+            assertTrue(attributeDescription.hasOption("FOO"));
+            assertTrue(attributeDescription.hasOption("FoO"));
+        } else {
+            assertFalse(attributeDescription.hasOption("foo"));
+            assertFalse(attributeDescription.hasOption("FOO"));
+            assertFalse(attributeDescription.hasOption("FoO"));
+        }
+    }
+
+    private void assertOptions(final AttributeDescription attributeDescription, final String... options) {
+        assertEquals(attributeDescription.hasOptions(), options.length != 0);
+        for (final String option : options) {
+            assertTrue(attributeDescription.hasOption(option));
+        }
+
+        Assertions.assertThat(attributeDescription.getOptions()).containsExactly(options);
+    }
+
+    @Test
+    public void testWithOptionAddFirstOption() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn");
+        AttributeDescription ad2 = ad1.withOption("test");
+        assertOptions(ad2, "test");
+        assertFalse(ad2.hasOption("dummy"));
+        assertEquals(ad2.toString(), "cn;test");
+    }
+
+    @Test
+    public void testWithOptionAddExistingFirstOption() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test");
+        AttributeDescription ad2 = ad1.withOption("test");
+        assertSame(ad1, ad2);
+    }
+
+    @Test
+    public void testWithOptionAddSecondOption() {
+        testWithOptionAddSecondOption("test1", "test2");
+    }
+
+    @Test
+    public void testWithOptionAddSecondOption2() {
+        testWithOptionAddSecondOption("test2", "test1");
+    }
+
+    private void testWithOptionAddSecondOption(String option1, String option2) {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;" + option1);
+        AttributeDescription ad2 = ad1.withOption(option2);
+        assertOptions(ad2, option1, option2);
+        assertFalse(ad2.hasOption("dummy"));
+        assertEquals(ad2.toString(), toAttributeDescriptionString("cn", option1, option2));
+    }
+
+    @Test
+    public void testWithOptionAddExistingSecondOption() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withOption("test1");
+        AttributeDescription ad3 = ad1.withOption("test2");
+        assertSame(ad1, ad2);
+        assertSame(ad1, ad3);
+    }
+
+    @Test
+    public void testWithOptionAddMultipleOptions() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withOption("test4").withOption("test3");
+        assertOptions(ad2, "test1", "test2", "test4", "test3");
+        assertFalse(ad2.hasOption("dummy"));
+        assertEquals(ad2.toString(), "cn;test1;test2;test4;test3");
+    }
+
+    @Test
+    public void testWithOptionAddMultipleOptions2() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withOption("test0");
+        assertOptions(ad2, "test1", "test2", "test0");
+        assertFalse(ad2.hasOption("dummy"));
+        assertEquals(ad2.toString(), "cn;test1;test2;test0");
+    }
+
+    @Test
+    public void testWithoutOptionEmpty() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn");
+        AttributeDescription ad2 = ad1.withoutOption("test");
+        assertSame(ad1, ad2);
+    }
+
+    @Test
+    public void testWithoutOptionFirstOption() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test");
+        AttributeDescription ad2 = ad1.withoutOption("test");
+        assertOptions(ad2);
+        assertFalse(ad2.hasOption("test"));
+        assertEquals(ad2.toString(), "cn");
+    }
+
+    @Test
+    public void testWithoutOptionFirstOptionMissing() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test");
+        AttributeDescription ad2 = ad1.withoutOption("dummy");
+        assertSame(ad1, ad2);
+    }
+
+    @Test
+    public void testWithoutOptionSecondOption1() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withoutOption("test1");
+        assertOptions(ad2, "test2");
+        assertFalse(ad2.hasOption("test1"));
+        assertEquals(ad2.toString(), "cn;test2");
+    }
+
+    @Test
+    public void testWithoutOptionSecondOption2() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withoutOption("test2");
+        assertOptions(ad2, "test1");
+        assertFalse(ad2.hasOption("test2"));
+        assertEquals(ad2.toString(), "cn;test1");
+    }
+
+    @Test
+    public void testWithoutOptionSecondOptionMissing() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2");
+        AttributeDescription ad2 = ad1.withoutOption("dummy");
+        assertSame(ad1, ad2);
+    }
+
+    @Test
+    public void testWithoutOptionThirdOption1() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
+        AttributeDescription ad2 = ad1.withoutOption("test1");
+        assertOptions(ad2, "test2", "test3");
+        assertFalse(ad2.hasOption("test1"));
+        assertEquals(ad2.toString(), "cn;test2;test3");
+    }
+
+    @Test
+    public void testWithoutOptionThirdOption2() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
+        AttributeDescription ad2 = ad1.withoutOption("test2");
+        assertOptions(ad2, "test1", "test3");
+        assertFalse(ad2.hasOption("test2"));
+        assertEquals(ad2.toString(), "cn;test1;test3");
+    }
+
+    @Test
+    public void testWithoutOptionThirdOption3() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
+        AttributeDescription ad2 = ad1.withoutOption("test3");
+        assertOptions(ad2, "test1", "test2");
+        assertFalse(ad2.hasOption("test3"));
+        assertEquals(ad2.toString(), "cn;test1;test2");
+    }
+
+    @Test
+    public void testWithoutOptionThirdOptionMissing() {
+        AttributeDescription ad1 = AttributeDescription.valueOf("cn;test1;test2;test3");
+        AttributeDescription ad2 = ad1.withoutOption("dummy");
+        assertSame(ad1, ad2);
+    }
+
+    @Test
+    public void testCreateAttributeType() {
+        Schema schema = Schema.getCoreSchema();
+        AttributeType attributeType = schema.getAttributeType("cn");
+        String name = attributeType.getNameOrOID();
+
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(attributeType),
+            name, attributeType);
+    }
+
+    @Test
+    public void testCreateAttributeNameAndType() {
+        Schema schema = Schema.getCoreSchema();
+        String name = "CN";
+        AttributeType attributeType = schema.getAttributeType(name);
+
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(name, attributeType),
+            name, attributeType);
+    }
+
+    @Test
+    public void testCreateAttributeTypeAndOption() {
+        Schema schema = Schema.getCoreSchema();
+        AttributeType attributeType = schema.getAttributeType("cn");
+        String name = attributeType.getNameOrOID();
+
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(attributeType, "option"),
+            name, attributeType, "option");
+    }
+
+    @Test
+    public void testCreateAttributeNameTypeAndOption() {
+        Schema schema = Schema.getCoreSchema();
+        String name = "CN";
+        AttributeType attributeType = schema.getAttributeType(name);
+
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(name, attributeType, "option"),
+            name, attributeType, "option");
+    }
+
+    @Test
+    public void testCreateAttributeTypeAndOptionsArray() {
+        Schema schema = Schema.getCoreSchema();
+        AttributeType attributeType = schema.getAttributeType("cn");
+        String name = attributeType.getNameOrOID();
+
+        String[] options = { "option1", "option2" };
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(attributeType, options),
+            name, attributeType, options);
+    }
+
+    @Test
+    public void testCreateAttributeNameTypeAndOptionsArray() {
+        Schema schema = Schema.getCoreSchema();
+        String name = "CN";
+        AttributeType attributeType = schema.getAttributeType(name);
+
+        String[] options = { "option1", "option2" };
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(name, attributeType, options),
+            name, attributeType, options);
+    }
+
+    @Test
+    public void testCreateAttributeTypeAndOptionsCollection() {
+        Schema schema = Schema.getCoreSchema();
+        AttributeType attributeType = schema.getAttributeType("cn");
+        String name = attributeType.getNameOrOID();
+
+        String[] options = { "option1", "option2" };
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(attributeType, Arrays.asList(options)),
+            name, attributeType, options);
+    }
+
+    @Test
+    public void testCreateAttributeNameTypeAndNoOptionsCollection() {
+        testCreateAttributeNameTypeAndOptionsCollection();
+    }
+
+    @Test
+    public void testCreateAttributeNameTypeAndOneOptionCollection() {
+        testCreateAttributeNameTypeAndOptionsCollection("option");
+    }
+
+    @Test
+    public void testCreateAttributeNameTypeAndTwoOptionsCollection() {
+        testCreateAttributeNameTypeAndOptionsCollection("option1", "option2");
+    }
+
+    private void testCreateAttributeNameTypeAndOptionsCollection(String... options) {
+        Schema schema = Schema.getCoreSchema();
+        String name = "CN";
+        AttributeType attributeType = schema.getAttributeType(name);
+
+        assertAttributeDescriptionCreate(
+            AttributeDescription.create(name, attributeType, Arrays.asList(options)),
+            name, attributeType, options);
+    }
+
+    private void assertAttributeDescriptionCreate(AttributeDescription attrDesc,
+            String name, AttributeType attributeType, String... options) {
+        assertEquals(attrDesc.getAttributeType(), attributeType);
+        assertEquals(attrDesc.getNameOrOID(), name);
+        assertOptions(attrDesc, options);
+        assertEquals(attrDesc.toString(), toAttributeDescriptionString(name, options));
+    }
+
+    private String toAttributeDescriptionString(String name, String... options) {
+        StringBuilder sb = new StringBuilder(name);
+        for (String option : options) {
+            sb.append(";").append(option);
+        }
+        return sb.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java
new file mode 100644
index 0000000..56da512
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java
@@ -0,0 +1,352 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.NoSuchElementException;
+
+import org.fest.util.Collections;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.Test;
+
+/**
+ * Test {@code AttributeParser}.
+ */
+@SuppressWarnings("javadoc")
+public final class AttributeParserTestCase extends SdkTestCase {
+
+    @Test
+    public void testAsBooleanTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: true");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: false");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isFalse();
+    }
+
+    @Test
+    public void testAsBooleanTrueDefaultFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: true");
+        assertThat(e.parseAttribute("enabled").asBoolean(false)).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanFalseDefaultTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: false");
+        assertThat(e.parseAttribute("enabled").asBoolean(true)).isFalse();
+    }
+
+    @Test
+    public void testAsBooleanMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isNull();
+    }
+
+    @Test
+    public void testAsBooleanMissingDefaultTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean(true)).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanMissingDefaultFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean(false)).isFalse();
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsBooleanMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("enabled").requireValue().asBoolean();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsBooleanInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: xxx");
+        e.parseAttribute("enabled").asBoolean();
+    }
+
+    @Test
+    public void testAsInteger99() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asInteger()).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsInteger99Default100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asInteger(100)).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsIntegerMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asInteger()).isNull();
+    }
+
+    @Test
+    public void testAsIntegerMissingDefault100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asInteger(100)).isEqualTo(100);
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsIntegerMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("age").requireValue().asInteger();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsIntegerInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: xxx");
+        e.parseAttribute("age").asInteger();
+    }
+
+    @Test
+    public void testAsLong99() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asLong()).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsLong99Default100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asLong(100)).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsLongMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asLong()).isNull();
+    }
+
+    @Test
+    public void testAsLongMissingDefault100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asLong(100)).isEqualTo(100);
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsLongMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("age").requireValue().asLong();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsLongInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: xxx");
+        e.parseAttribute("age").asLong();
+    }
+
+    @Test
+    public void testAsDN() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: cn=manager");
+        assertThat((Object) e.parseAttribute("manager").asDN()).isEqualTo(DN.valueOf("cn=manager"));
+    }
+
+    @Test
+    public void testAsDNDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: cn=manager");
+        assertThat((Object) e.parseAttribute("manager").asDN("cn=boss")).isEqualTo(
+                DN.valueOf("cn=manager"));
+    }
+
+    @Test
+    public void testAsDNMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("manager").asDN()).isNull();
+    }
+
+    @Test
+    public void testAsDNMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat((Object) e.parseAttribute("manager").asDN(DN.valueOf("cn=boss"))).isEqualTo(
+                DN.valueOf("cn=boss"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsDNMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("manager").requireValue().asDN();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsDNInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: xxx");
+        e.parseAttribute("manager").asDN();
+    }
+
+    @Test
+    public void testAsAttributeDescription() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asAttributeDescription()).isEqualTo(
+                AttributeDescription.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsAttributeDescriptionDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asAttributeDescription("sn")).isEqualTo(
+                AttributeDescription.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsAttributeDescriptionMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asAttributeDescription()).isNull();
+    }
+
+    @Test
+    public void testAsAttributeDescriptionMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(
+                e.parseAttribute("type").asAttributeDescription(AttributeDescription.valueOf("sn")))
+                .isEqualTo(AttributeDescription.valueOf("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsAttributeDescriptionMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asAttributeDescription();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsAttributeDescriptionInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: ;x");
+        e.parseAttribute("type").asAttributeDescription();
+    }
+
+    @Test
+    public void testAsString() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asString()).isEqualTo(String.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsStringDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asString("sn")).isEqualTo(String.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsStringMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asString()).isNull();
+    }
+
+    @Test
+    public void testAsStringMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asString(String.valueOf("sn"))).isEqualTo(
+                String.valueOf("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsStringMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asString();
+    }
+
+    @Test
+    public void testAsByteString() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asByteString()).isEqualTo(ByteString.valueOfUtf8("cn"));
+    }
+
+    @Test
+    public void testAsByteStringDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asByteString(ByteString.valueOfUtf8("sn"))).isEqualTo(
+                ByteString.valueOfUtf8("cn"));
+    }
+
+    @Test
+    public void testAsByteStringMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asByteString()).isNull();
+    }
+
+    @Test
+    public void testAsByteStringMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asByteString(ByteString.valueOfUtf8("sn"))).isEqualTo(
+                ByteString.valueOfUtf8("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsByteStringMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asByteString();
+    }
+
+    /**
+     * Smoke test for set of methods: use one type only since the code is common
+     * and we've already tested the parsing.
+     */
+    @Test
+    public void testAsSetOfDN() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: cn=member2", "member: cn=member3");
+        assertThat(e.parseAttribute("member").asSetOfDN()).isEqualTo(
+                Collections.set(DN.valueOf("cn=member1"), DN.valueOf("cn=member2"), DN
+                        .valueOf("cn=member3")));
+    }
+
+    @Test
+    public void testAsSetOfDNDefault() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: cn=member2", "member: cn=member3");
+        assertThat(e.parseAttribute("member").asSetOfDN("cn=dummy1", "cn=dummy2")).isEqualTo(
+                Collections.set(DN.valueOf("cn=member1"), DN.valueOf("cn=member2"), DN
+                        .valueOf("cn=member3")));
+    }
+
+    @Test
+    public void testAsSetOfDNMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        assertThat(e.parseAttribute("member").asSetOfDN()).isEqualTo(
+                java.util.Collections.emptySet());
+    }
+
+    @Test
+    public void testAsSetOfDNMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        assertThat(e.parseAttribute("member").asSetOfDN("cn=dummy1", "cn=dummy2")).isEqualTo(
+                Collections.set(DN.valueOf("cn=dummy1"), DN.valueOf("cn=dummy2")));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsSetOfDNMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        e.parseAttribute("member").requireValue().asSetOfDN();
+    }
+
+    @Test(expectedExceptions = { LocalizedIllegalArgumentException.class })
+    public void testAsSetOfDNInvalid() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: xxxx");
+        e.parseAttribute("member").asSetOfDN();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributesTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributesTestCase.java
new file mode 100644
index 0000000..d31dce9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/AttributesTestCase.java
@@ -0,0 +1,114 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.testng.Assert.assertTrue;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the Attributes class.
+ */
+@SuppressWarnings("javadoc")
+public class AttributesTestCase extends SdkTestCase {
+    /**
+     * Data provider for attribute descriptions.
+     *
+     * @return
+     */
+    @DataProvider(name = "dataForAttributeDescriptions")
+    public Object[][] dataForAttributeDescriptions() {
+        // Value, type, options, containsOptions("foo")
+        return new Object[][] { { "cn" }, { "CN" }, { "objectClass" }, { "cn;foo" }, { "cn;FOO" },
+            { "cn;bar" }, { "cn;BAR" }, { "cn;foo;bar" }, { "cn;FOO;bar" }, };
+    }
+
+    /** Data provider for old and new attributes. */
+    @DataProvider(name = "dataForAttributeRename")
+    public Object[][] dataForAttributeRename() {
+        return new Object[][] { { "cn", "cn", true }, { "CN", "cn", true },
+            { "objectClass", "cn", false }, { "cn;foo", "cn", true } };
+    }
+
+    /**
+     * Tests the attribute renaming method.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "dataForAttributeRename")
+    public void testAttributeRename(final String attr, final String desc, final boolean valid)
+            throws Exception {
+        final AttributeDescription desc1 =
+                AttributeDescription.valueOf(attr, Schema.getCoreSchema());
+        final AttributeDescription desc2 =
+                AttributeDescription.valueOf(desc, Schema.getCoreSchema());
+        final Attribute attr1 = Attributes.emptyAttribute(desc1);
+        try {
+            Attributes.renameAttribute(attr1, desc2);
+        } catch (final Exception e) {
+            if (valid) {
+                // shouldn't have come here.
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * Tests the empty attribute method.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "dataForAttributeDescriptions")
+    public void testEmptyAttribute(final String attrDesc) throws Exception {
+        final AttributeDescription desc =
+                AttributeDescription.valueOf(attrDesc, Schema.getCoreSchema());
+        final Attribute attr = Attributes.emptyAttribute(desc);
+        assertTrue(attr.isEmpty());
+    }
+
+    /**
+     * Tests the unmodifiable attribute method.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "dataForAttributeDescriptions",
+            expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAttribute(final String attrDesc) throws Exception {
+        final AttributeDescription desc =
+                AttributeDescription.valueOf(attrDesc, Schema.getCoreSchema());
+        final Attribute attr = Attributes.emptyAttribute(desc);
+        attr.add("test"); // should go through.
+        // Make it unmodifiable.
+        final Attribute attr1 = Attributes.unmodifiableAttribute(attr);
+        attr1.add("test");
+    }
+
+    /**
+     * Tests the unmodifiable entry method.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableEntry() throws Exception {
+        final Entry entry = new LinkedHashMapEntry("cn=test");
+        // add a value.
+        entry.clearAttributes();
+        final Entry entry1 = Entries.unmodifiableEntry(entry);
+        entry1.clearAttributes();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceReaderTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceReaderTest.java
new file mode 100644
index 0000000..2b60f18
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceReaderTest.java
@@ -0,0 +1,369 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test class for ByteSequenceReader.
+ */
+@SuppressWarnings("javadoc")
+public class ByteSequenceReaderTest extends SdkTestCase {
+
+    private static final byte[] EIGHT_BYTES =
+        new byte[]{ b(0x01), b(0x02), b(0x03), b(0x04),
+                    b(0x05), b(0x06), b(0x07), b(0x08) };
+
+    private static byte b(int i) {
+        return (byte) i;
+    }
+
+    @DataProvider(name = "readerProvider")
+    public Object[][] byteSequenceReaderProvider() {
+        return new Object[][] {
+            { ByteString.wrap(EIGHT_BYTES).asReader(), EIGHT_BYTES },
+            { new ByteStringBuilder().appendBytes(EIGHT_BYTES).asReader(), EIGHT_BYTES },
+            { new ByteStringBuilder().appendBytes(EIGHT_BYTES).subSequence(0, 8).asReader(), EIGHT_BYTES }
+        };
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadByte(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        for (byte b : ba) {
+            Assert.assertEquals(reader.readByte(), b);
+        }
+
+        // The next read should result in IOB exception.
+        reader.readByte();
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadBytes(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            byte[] readArray = new byte[length];
+            reader.readBytes(readArray);
+            byte[] subArray = new byte[length];
+            System.arraycopy(ba, ba.length - remaining, subArray, 0, length);
+            Assert.assertTrue(Arrays.equals(readArray, subArray));
+
+            remaining -= length;
+        }
+
+        // Any more reads should result in IOB exception.
+        reader.readBytes(new byte[1]);
+    }
+
+    @Test(dataProvider = "readerProvider")
+    public void testReadBytesWithOffset(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+        byte[] readArray = new byte[ba.length];
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            reader.readBytes(readArray, ba.length - remaining, length);
+
+            remaining -= length;
+        }
+
+        Assert.assertTrue(Arrays.equals(readArray, ba));
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testNegativeOffsetReadBytes(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        byte[] array = new byte[ba.length];
+        reader.readBytes(array, -1, ba.length);
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testNegativeLengthReadBytes(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        byte[] array = new byte[ba.length];
+        reader.readBytes(array, 0, -1);
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testBadOffLenReadBytes(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        byte[] array = new byte[ba.length];
+        reader.readBytes(array, 3, ba.length);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadBERLength() {
+        ByteSequenceReader reader = ByteString.wrap(new byte[]{
+            b(0x00), b(0x01), b(0x0F), b(0x10),
+            b(0x7F),
+
+            b(0x81), b(0xFF),
+
+            b(0x82), b(0x01), b(0x00),
+            b(0x82), b(0x0F), b(0xFF), b(0x82), b(0x10),
+            b(0x00), b(0x82), b(0xFF), b(0xFF),
+
+            b(0x83), b(0x01), b(0x00), b(0x00),
+            b(0x83), b(0x0F), b(0xFF), b(0xFF),
+            b(0x83), b(0x10), b(0x00), b(0x00),
+            b(0x83), b(0xFF), b(0xFF), b(0xFF),
+
+            b(0x84), b(0x01), b(0x00), b(0x00), b(0x00),
+            b(0x84), b(0x0F), b(0xFF), b(0xFF), b(0xFF),
+            b(0x84), b(0x10), b(0x00), b(0x00), b(0x00),
+            b(0x84), b(0xFF), b(0xFF), b(0xFF), b(0xFF),
+
+            b(0x84), b(0x10), b(0x00)
+        }).asReader();
+
+        int[] expectedLength = new int[]{
+            0x00000000, 0x00000001, 0x0000000F, 0x00000010,
+            0x0000007F,
+
+            0x000000FF,
+
+            0x00000100, 0x00000FFF, 0x00001000, 0x0000FFFF,
+
+            0x00010000, 0x000FFFFF, 0x00100000, 0x00FFFFFF,
+
+            0x01000000, 0x0FFFFFFF, 0x10000000, 0xFFFFFFFF
+        };
+
+        for (int length : expectedLength) {
+            Assert.assertEquals(reader.readBERLength(), length);
+        }
+
+        // Last one is incomplete and should throw error
+        reader.readBERLength();
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testOversizedReadBERLength() {
+        ByteSequenceReader reader = ByteString.wrap(new byte[]{
+            b(0x85), b(0x10), b(0x00), b(0x00), b(0x00), b(0x00)
+        }).asReader();
+
+        // Shouldn't be able to reader over a 4 byte length.
+        reader.readBERLength();
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testUndersizedReadBERLength() {
+        ByteSequenceReader reader = ByteString.empty().asReader();
+
+        // Shouldn't be able to reader over a 4 byte length.
+        reader.readBERLength();
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadByteSequence(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            ByteSequence readSequence = reader.readByteSequence(length);
+            byte[] subArray = new byte[length];
+            System.arraycopy(ba, ba.length - remaining, subArray, 0, length);
+            Assert.assertTrue(Arrays.equals(readSequence.toByteArray(), subArray));
+
+            remaining -= length;
+        }
+
+        // Any more reads should result in IOB exception.
+        reader.readByteSequence(1);
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadByteString(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            ByteString readSequence = reader.readByteString(length);
+            byte[] subArray = new byte[length];
+            System.arraycopy(ba, ba.length - remaining, subArray, 0, length);
+            Assert.assertTrue(Arrays.equals(readSequence.toByteArray(), subArray));
+
+            remaining -= length;
+        }
+
+        // Any more reads should result in IOB exception.
+        reader.readByteString(1);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadShort() {
+        ByteSequenceReader reader = ByteString.wrap(new byte[]{
+            b(0x80), b(0x00), b(0x7F), b(0xFF), b(0xFF)
+        }).asReader();
+
+        Assert.assertEquals(reader.readShort(), Short.MIN_VALUE);
+        Assert.assertEquals(reader.readShort(), Short.MAX_VALUE);
+
+        // Any more reads should result in IOB exception.
+        reader.readShort();
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadInt() {
+        ByteSequenceReader reader = ByteString.wrap(new byte[]{
+            b(0x80), b(0x00), b(0x00), b(0x00), b(0x7F),
+            b(0xFF), b(0xFF), b(0xFF), b(0xFF) }).asReader();
+
+        Assert.assertEquals(reader.readInt(), Integer.MIN_VALUE);
+        Assert.assertEquals(reader.readInt(), Integer.MAX_VALUE);
+
+        // Any more reads should result in IOB exception.
+        reader.readInt();
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadLong() {
+        ByteSequenceReader reader = ByteString.wrap(new byte[]{
+            b(0x80), b(0x00), b(0x00), b(0x00), b(0x00),
+            b(0x00), b(0x00), b(0x00), b(0x7F), b(0xFF),
+            b(0xFF), b(0xFF), b(0xFF), b(0xFF), b(0xFF),
+            b(0xFF), b(0xFF) }).asReader();
+
+        Assert.assertEquals(reader.readLong(), Long.MIN_VALUE);
+        Assert.assertEquals(reader.readLong(), Long.MAX_VALUE);
+
+        // Any more reads should result in IOB exception.
+        reader.readLong();
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testReadString(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            String readString = reader.readStringUtf8(length);
+            String subString = new String(ba, ba.length - remaining, length);
+            Assert.assertTrue(readString.equals(subString));
+
+            remaining -= length;
+        }
+
+        // Any more reads should result in IOB exception.
+        reader.readStringUtf8(1);
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testSetPosition(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+
+        for (int i = 0; i < ba.length; i++) {
+            reader.position(i);
+            String readString = reader.readStringUtf8(ba.length - i);
+            String subString = new String(ba, i, ba.length - i);
+            Assert.assertTrue(readString.equals(subString));
+        }
+
+        // Setting an invalid position should result in IOB exception.
+        reader.position(ba.length + 1);
+    }
+
+    @Test(dataProvider = "readerProvider")
+    public void testRemaining(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+
+        for (int i = 0; i < ba.length; i++) {
+            reader.position(i);
+            Assert.assertEquals(reader.remaining(), ba.length - i);
+        }
+    }
+
+    @Test(dataProvider = "readerProvider")
+    public void testRewind(ByteSequenceReader reader, byte[] ba) {
+        reader.position(ba.length - 1);
+        reader.rewind();
+        Assert.assertEquals(reader.position(), 0);
+    }
+
+    @Test(dataProvider = "readerProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testSkip(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+        int remaining = ba.length;
+
+        while (remaining > 0) {
+            int length = remaining / 2;
+            if (length == 0) {
+                length = remaining % 2;
+            }
+
+            reader.skip(length);
+            remaining -= length;
+
+            Assert.assertEquals(reader.position(), ba.length - remaining);
+        }
+
+        // Any more skips should result in IOB exception.
+        reader.skip(1);
+    }
+
+    @Test(dataProvider = "readerProvider")
+    public void testPeek(ByteSequenceReader reader, byte[] ba) {
+        reader.rewind();
+
+        int length = ba.length;
+        int pos = 0;
+        for (int i = 0; i < length; i++) {
+            for (int j = 0; j < length - i; j++) {
+                if (j == 0) {
+                    Assert.assertEquals(reader.peek(), ba[pos]);
+                }
+                Assert.assertEquals(reader.peek(j), ba[pos + j]);
+            }
+            pos++;
+            if (pos < length) {
+                reader.skip(1);
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceTestCase.java
new file mode 100644
index 0000000..e761742
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteSequenceTestCase.java
@@ -0,0 +1,160 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+import javax.xml.bind.DatatypeConverter;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Abstract test case for the ByteSequence interface.
+ */
+@SuppressWarnings("javadoc")
+public abstract class ByteSequenceTestCase extends SdkTestCase {
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testByteAt(final ByteSequence bs, final byte[] ba) {
+        for (int i = 0; i < ba.length; i++) {
+            Assert.assertEquals(bs.byteAt(i), ba[i]);
+        }
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testByteAtBadIndex1(final ByteSequence bs, final byte[] ba) {
+        bs.byteAt(ba.length);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testByteAtBadIndex2(final ByteSequence bs, final byte[] ba) {
+        bs.byteAt(-1);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testCopyTo(final ByteSequence bs, final byte[] ba) {
+        final byte[] newBa = new byte[ba.length];
+        bs.copyTo(newBa);
+        Assert.assertTrue(Arrays.equals(newBa, ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testCopyToByteSequenceBuilder(final ByteSequence bs, final byte[] ba) {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        bs.copyTo(builder);
+        Assert.assertTrue(Arrays.equals(builder.toByteArray(), ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testCopyToOutputStream(final ByteSequence bs, final byte[] ba) throws Exception {
+        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        bs.copyTo(stream);
+        Assert.assertTrue(Arrays.equals(stream.toByteArray(), ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testCopyToWithBadOffset(final ByteSequence bs, final byte[] ba) {
+        bs.copyTo(new byte[0], -1);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testCopyToWithOffset(final ByteSequence bs, final byte[] ba) {
+        for (int i = 0; i < ba.length * 2; i++) {
+            final byte[] newBa = new byte[ba.length * 2];
+            bs.copyTo(newBa, i);
+
+            final byte[] resultBa = new byte[ba.length * 2];
+            System.arraycopy(ba, 0, resultBa, i, Math.min(ba.length, ba.length * 2 - i));
+            Assert.assertTrue(Arrays.equals(newBa, resultBa));
+        }
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testEquals(final ByteSequence bs, final byte[] ba) throws Exception {
+        Assert.assertTrue(bs.equals(ByteString.wrap(ba)));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testLength(final ByteSequence bs, final byte[] ba) throws Exception {
+        Assert.assertEquals(bs.length(), ba.length);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testSubSequence(final ByteSequence bs, final byte[] ba) {
+        ByteSequence bsSub = bs.subSequence(0, bs.length() / 2);
+        byte[] baSub = new byte[ba.length / 2];
+        System.arraycopy(ba, 0, baSub, 0, baSub.length);
+        Assert.assertTrue(Arrays.equals(bsSub.toByteArray(), baSub));
+
+        bsSub = bs.subSequence(ba.length / 4, (bs.length() / 4) * 3);
+        baSub = new byte[(bs.length() / 4) * 3 - ba.length / 4];
+        System.arraycopy(ba, ba.length / 4, baSub, 0, baSub.length);
+        Assert.assertTrue(Arrays.equals(bsSub.toByteArray(), baSub));
+
+        bsSub = bs.subSequence(ba.length / 2, bs.length());
+        baSub = new byte[bs.length() - ba.length / 2];
+        System.arraycopy(ba, ba.length / 2, baSub, 0, baSub.length);
+        Assert.assertTrue(Arrays.equals(bsSub.toByteArray(), baSub));
+
+        bsSub = bs.subSequence(0, bs.length());
+        Assert.assertTrue(Arrays.equals(bsSub.toByteArray(), ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testSubSequenceBadStartEnd1(final ByteSequence bs, final byte[] ba) {
+        bs.subSequence(-1, bs.length());
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testSubSequenceBadStartEnd2(final ByteSequence bs, final byte[] ba) {
+        bs.subSequence(0, bs.length() + 1);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider",
+            expectedExceptions = IndexOutOfBoundsException.class)
+    public void testSubSequenceBadStartEnd3(final ByteSequence bs, final byte[] ba) {
+        bs.subSequence(-1, bs.length() + 1);
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testToByteArray(final ByteSequence bs, final byte[] ba) {
+        Assert.assertTrue(Arrays.equals(bs.toByteArray(), ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testToByteSequence(final ByteSequence bs, final byte[] ba) {
+        Assert.assertTrue(Arrays.equals(bs.toByteString().toByteArray(), ba));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testToString(final ByteSequence bs, final byte[] ba) throws Exception {
+        String str = new String(ba, "UTF-8");
+        Assert.assertTrue(bs.toString().equals(str));
+    }
+
+    @Test(dataProvider = "byteSequenceProvider")
+    public void testToBase64String(final ByteSequence bs, final byte[] ba) throws Exception {
+        final String base64 = bs.toBase64String();
+        Assert.assertEquals(base64, DatatypeConverter.printBase64Binary(ba));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringBuilderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringBuilderTestCase.java
new file mode 100644
index 0000000..b3134d9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringBuilderTestCase.java
@@ -0,0 +1,444 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterOutputStream;
+
+import org.fest.assertions.Assertions;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test case for ByteStringBuilder.
+ */
+@SuppressWarnings("javadoc")
+@Test(groups = "unit")
+public class ByteStringBuilderTestCase extends ByteSequenceTestCase {
+
+    private static byte b(int i) {
+        return (byte) i;
+    }
+
+    private static final byte[] EIGHT_BYTES = new byte[] { b(0x01), b(0x02), b(0x03),
+        b(0x04), b(0x05), b(0x06), b(0x07), b(0x08) };
+
+    /**
+     * ByteSequence data provider.
+     *
+     * @return The array of ByteStrings and the bytes it should contain.
+     */
+    @DataProvider(name = "byteSequenceProvider")
+    public Object[][] byteSequenceProvider() throws Exception {
+        final Object[][] builders = byteStringBuilderProvider();
+        final Object[][] addlSequences = new Object[builders.length + 1][];
+        System.arraycopy(builders, 0, addlSequences, 0, builders.length);
+        addlSequences[builders.length] =
+                new Object[] { new ByteStringBuilder().appendBytes(EIGHT_BYTES).subSequence(2, 6),
+                    new byte[] { b(0x03), b(0x04), b(0x05), b(0x06) } };
+
+        return addlSequences;
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testCannotAppendCompactNegativeValues() {
+        ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendCompactUnsigned(-1);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testCannotAppendCompact57BitsValues() {
+        new ByteStringBuilder().appendCompactUnsigned(0x100000000000000L);
+    }
+
+    @Test(dataProvider = "unsignedLongValues")
+    public void testCanAppendCompactPositiveValue(long value) {
+        assertThat(new ByteStringBuilder().appendCompactUnsigned(value).asReader().readCompactUnsignedLong())
+            .isEqualTo(value);
+    }
+
+    @DataProvider
+    public Object[][] unsignedLongValues() throws Exception {
+        return new Object[][] {
+            { 0 },
+            { 0x80L }, { 0x81L },
+            { 0x4000L }, { 0x4001L },
+            { 0x200000L }, { 0x200001L },
+            { 0x10000000L }, { 0x10000001L },
+            { 0x800000000L }, { 0x800000001L },
+            { 0x40000000000L }, { 0x40000000001L },
+            { 0x2000000000000L }, { 0x2000000000001L },
+            { 0x00FFFFFFFFFFFFFFL }
+        };
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadByteBufferLength1() {
+        new ByteStringBuilder().appendBytes(ByteBuffer.wrap(new byte[5]), -1);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadByteBufferLength2() {
+        new ByteStringBuilder().appendBytes(ByteBuffer.wrap(new byte[5]), 6);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadByteSequenceReaderLength1() {
+        new ByteStringBuilder().appendBytes(ByteString.wrap(new byte[5]).asReader(), -1);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadByteSequenceReaderLength2() {
+        new ByteStringBuilder().appendBytes(ByteString.wrap(new byte[5]).asReader(), 6);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadInputStreamLength() throws Exception {
+        final ByteArrayInputStream stream = new ByteArrayInputStream(new byte[5]);
+        new ByteStringBuilder().appendBytes(stream, -1);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadLength1() {
+        new ByteStringBuilder().appendBytes(new byte[5], 0, 6);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadLength2() {
+        new ByteStringBuilder().appendBytes(new byte[5], 0, -1);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadOffset1() {
+        new ByteStringBuilder().appendBytes(new byte[5], -1, 3);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testAppendBadOffset2() {
+        new ByteStringBuilder().appendBytes(new byte[5], 6, 0);
+    }
+
+    @Test
+    public void testAppendInputStream() throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        final ByteArrayInputStream stream = new ByteArrayInputStream(new byte[5]);
+        Assert.assertEquals(bsb.appendBytes(stream, 10), 5);
+    }
+
+    @Test
+    public void testAppendDataInputWithNonEmptyBuilder() throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendByte(0);
+        final DataInput stream = new DataInputStream(new ByteArrayInputStream(new byte[5]));
+        bsb.appendBytes(stream, 5);
+        Assert.assertEquals(bsb.length(), 6);
+    }
+
+    @Test(dataProvider = "builderProvider", expectedExceptions = IndexOutOfBoundsException.class)
+    public void testClear(final ByteStringBuilder bs, final byte[] ba) {
+        bs.clear();
+        Assert.assertEquals(bs.length(), 0);
+        bs.byteAt(0);
+    }
+
+    @DataProvider
+    public Object[][] clearAndTruncateProvider() throws Exception {
+        return new Object[][] {
+            { builder(0), 42, 42 },
+            { builder(42), 42, 42 },
+            { builder(43), 42, 42 },
+        };
+    }
+
+    private ByteStringBuilder builder(int length) {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        for (int i = 0; i < length; i++) {
+            builder.appendInt(42);
+        }
+        return builder;
+    }
+
+    @Test(dataProvider = "clearAndTruncateProvider")
+    public void testClearAndTruncate(ByteStringBuilder bs, int thresholdCapacity, int newCapacity) {
+        bs.clearAndTruncate(thresholdCapacity, newCapacity);
+        Assertions.assertThat(bs.length()).isEqualTo(0);
+        Assertions.assertThat(bs.capacity()).isLessThanOrEqualTo(thresholdCapacity);
+        Assertions.assertThat(bs.capacity()).isLessThanOrEqualTo(newCapacity);
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void clearAndTruncateThrowsWithNegativeNewCapacity() {
+        new ByteStringBuilder().clearAndTruncate(42, -1);
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void clearAndTruncateThrowsWithNewCapacityAboveThreshold() {
+        new ByteStringBuilder().clearAndTruncate(42, 42 + 1);
+    }
+
+    @Test
+    public void testEnsureAdditionalCapacity() {
+        final ByteStringBuilder bsb = new ByteStringBuilder(8);
+        Assert.assertEquals(bsb.getBackingArray().length, 8);
+        bsb.ensureAdditionalCapacity(43);
+        bsb.ensureAdditionalCapacity(2);
+        Assert.assertTrue(bsb.getBackingArray().length >= 43);
+    }
+
+    @Test(dataProvider = "builderProvider")
+    public void testGetBackingArray(final ByteStringBuilder bs, final byte[] ba) {
+        final byte[] trimmedArray = new byte[bs.length()];
+        System.arraycopy(bs.getBackingArray(), 0, trimmedArray, 0, bs.length());
+        Assert.assertTrue(Arrays.equals(trimmedArray, ba));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testInvalidCapacity() {
+        new ByteStringBuilder(-1);
+    }
+
+    @Test
+    public void testTrimToSize() {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        bsb.appendBytes(EIGHT_BYTES);
+        Assert.assertTrue(bsb.getBackingArray().length > 8);
+        bsb.trimToSize();
+        Assert.assertEquals(bsb.getBackingArray().length, 8);
+    }
+
+    @Test
+    public void testAsOutputStream() throws Exception {
+        final ByteStringBuilder bsb = new ByteStringBuilder();
+        try (final OutputStream os = bsb.asOutputStream()) {
+            os.write(b(0x01));
+            os.write(2);
+            os.write(new byte[] { 2, 3, 4, 5 }, 1, 2);
+        }
+        Assert.assertEquals(bsb.length(), 4);
+        Assert.assertEquals(bsb.toByteArray(), new byte[] { 1, 2, 3, 4 });
+    }
+
+    @Test
+    public void testAsOutputStreamCompress() throws Exception {
+        final ByteString data = ByteString.wrap(new byte[4000]);
+        final ByteStringBuilder compressedData = new ByteStringBuilder();
+        try (final OutputStream compressor = new DeflaterOutputStream(compressedData.asOutputStream())) {
+            data.copyTo(compressor);
+        }
+        Assert.assertTrue(compressedData.length() > 0 && compressedData.length() < 4000);
+
+        final ByteStringBuilder decompressedData = new ByteStringBuilder();
+        try (final OutputStream decompressor = new InflaterOutputStream(decompressedData.asOutputStream())) {
+            compressedData.copyTo(decompressor);
+        }
+        Assert.assertEquals(decompressedData.toByteString(), data);
+    }
+
+    @DataProvider(name = "builderProvider")
+    private Object[][] byteStringBuilderProvider() throws Exception {
+        final ByteBuffer testBuffer = ByteBuffer.wrap(EIGHT_BYTES);
+        final ByteString testByteString = ByteString.wrap(EIGHT_BYTES);
+        final ByteSequenceReader testByteReader = testByteString.asReader();
+        final InputStream testStream = new ByteArrayInputStream(EIGHT_BYTES);
+        final ByteStringBuilder testBuilderFromStream = new ByteStringBuilder(8);
+        testBuilderFromStream.appendBytes(testStream, 8);
+
+        return new Object[][] {
+            { new ByteStringBuilder().appendByte(0x00).appendByte(0x01),
+                new byte[] { b(0x00), b(0x01) } },
+            { new ByteStringBuilder(5)
+                      .appendBytes(new byte[] { b(0x01), b(0x02), b(0x03), b(0x04) })
+                      .appendBytes(new byte[] { b(0x05), b(0x06), b(0x07), b(0x08) }),
+                EIGHT_BYTES },
+            { new ByteStringBuilder(3).appendBytes(EIGHT_BYTES, 0, 3).appendBytes(EIGHT_BYTES, 3, 5),
+                EIGHT_BYTES },
+            { new ByteStringBuilder().appendBytes(testBuffer, 3).appendBytes(testBuffer, 5), EIGHT_BYTES },
+            { new ByteStringBuilder(2).appendBytes(testByteString), EIGHT_BYTES },
+            { new ByteStringBuilder().appendBytes(testByteReader, 5).appendBytes(testByteReader, 3),
+                EIGHT_BYTES },
+            { testBuilderFromStream, EIGHT_BYTES },
+            { new ByteStringBuilder().appendShort(Short.MIN_VALUE).appendShort(Short.MAX_VALUE),
+                new byte[] { b(0x80), b(0x00), b(0x7F), b(0xFF) } },
+            {
+                new ByteStringBuilder(5).appendInt(Integer.MIN_VALUE).appendInt(Integer.MAX_VALUE),
+                new byte[] { b(0x80), b(0x00), b(0x00), b(0x00), b(0x7F),
+                    b(0xFF), b(0xFF), b(0xFF) } },
+            {
+                new ByteStringBuilder().appendLong(Long.MIN_VALUE).appendLong(Long.MAX_VALUE),
+                new byte[] { b(0x80), b(0x00), b(0x00), b(0x00), b(0x00),
+                    b(0x00), b(0x00), b(0x00), b(0x7F), b(0xFF), b(0xFF),
+                    b(0xFF), b(0xFF), b(0xFF), b(0xFF), b(0xFF) } },
+            { new ByteStringBuilder(11).appendUtf8("this is a").appendUtf8(" test"),
+                "this is a test".getBytes("UTF-8") },
+            { new ByteStringBuilder().appendObject("this is a").appendObject(" test"),
+                "this is a test".getBytes("UTF-8") },
+            {
+                new ByteStringBuilder().appendUtf8("this is a".toCharArray()).appendUtf8(
+                        " test".toCharArray()), "this is a test".getBytes("UTF-8") },
+            {
+                new ByteStringBuilder().appendObject("this is a".toCharArray()).appendObject(
+                        " test".toCharArray()), "this is a test".getBytes("UTF-8") },
+            {
+                new ByteStringBuilder().appendObject(EIGHT_BYTES).appendObject(EIGHT_BYTES),
+                new byte[] { b(0x01), b(0x02), b(0x03), b(0x04), b(0x05),
+                    b(0x06), b(0x07), b(0x08), b(0x01), b(0x02), b(0x03),
+                    b(0x04), b(0x05), b(0x06), b(0x07), b(0x08) } },
+            {
+                new ByteStringBuilder().appendBERLength(0x00000000).appendBERLength(0x00000001)
+                        .appendBERLength(0x0000000F).appendBERLength(0x00000010).appendBERLength(
+                                0x0000007F).appendBERLength(0x000000FF).appendBERLength(0x00000100)
+                        .appendBERLength(0x00000FFF).appendBERLength(0x00001000).appendBERLength(
+                                0x0000FFFF).appendBERLength(0x00010000).appendBERLength(0x000FFFFF)
+                        .appendBERLength(0x00100000).appendBERLength(0x00FFFFFF).appendBERLength(
+                                0x01000000).appendBERLength(0x0FFFFFFF).appendBERLength(0x10000000)
+                        .appendBERLength(0xFFFFFFFF),
+                new byte[] { b(0x00), b(0x01), b(0x0F), b(0x10), b(0x7F),
+                    b(0x81), b(0xFF), b(0x82), b(0x01), b(0x00), b(0x82),
+                    b(0x0F), b(0xFF), b(0x82), b(0x10), b(0x00), b(0x82),
+                    b(0xFF), b(0xFF), b(0x83), b(0x01), b(0x00), b(0x00),
+                    b(0x83), b(0x0F), b(0xFF), b(0xFF), b(0x83), b(0x10),
+                    b(0x00), b(0x00), b(0x83), b(0xFF), b(0xFF), b(0xFF),
+                    b(0x84), b(0x01), b(0x00), b(0x00), b(0x00), b(0x84),
+                    b(0x0F), b(0xFF), b(0xFF), b(0xFF), b(0x84), b(0x10),
+                    b(0x00), b(0x00), b(0x00), b(0x84), b(0xFF), b(0xFF),
+                    b(0xFF), b(0xFF) } }, };
+    }
+
+    @Test
+    public void testCopyCtor() {
+        final ByteStringBuilder builder = new ByteStringBuilder(400);
+        builder.appendUtf8("this is a ByteString");
+        final ByteString orig = builder.toByteString();
+        final ByteString copy = new ByteStringBuilder(orig).toByteString();
+        Assert.assertEquals(copy, orig);
+        Assert.assertEquals(copy.length(), builder.length());
+    }
+
+    @Test
+    public void testSetByte() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8("this is a ByteString");
+        builder.setByte(2, b('a'));
+        builder.setByte(3, b('t'));
+        Assert.assertEquals(builder.toByteString().toString(), "that is a ByteString");
+    }
+
+    @Test(expectedExceptions = { IndexOutOfBoundsException.class })
+    public void testSetByteAtInvalidLowerIndex() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.setByte(-1, b(0));
+    }
+
+    @Test(expectedExceptions = { IndexOutOfBoundsException.class })
+    public void testSetByteAtInvalidUpperIndex() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.setByte(builder.length(), b(0));
+    }
+
+    @Test
+    public void testSetLength() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8("this is a ByteString");
+        builder.setLength(builder.length() - 16);
+        Assert.assertEquals(builder.toString(), "this");
+        builder.setLength(builder.length() + 1);
+        Assert.assertEquals(builder.toString(), "this\u0000");
+    }
+
+    @Test(expectedExceptions = { IndexOutOfBoundsException.class })
+    public void testSetInvalidLength() {
+        new ByteStringBuilder().setLength(-1);
+    }
+
+    @Test
+    public void testAppendNullCharArray() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8((char[]) null);
+        Assert.assertTrue(builder.isEmpty());
+    }
+
+    @Test
+    public void testAppendNullString() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8((String) null);
+        Assert.assertTrue(builder.isEmpty());
+    }
+
+    @Test
+    public void testAppendNonAsciiCharArray() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8(new char[] { 'œ', 'Œ' });
+        Assert.assertEquals(builder.toString(), "œŒ");
+    }
+
+    @Test
+    public void testAppendNonAsciiString() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        builder.appendUtf8("œŒ");
+        Assert.assertEquals(builder.toString(), "œŒ");
+    }
+
+    @Test
+    public void testByteStringBuilderCompareTo() {
+        final ByteString orig = ByteString.valueOfUtf8("this is a ByteString");
+        final ByteStringBuilder builder = new ByteStringBuilder(orig);
+        Assert.assertEquals(builder.compareTo(builder), 0);
+        Assert.assertEquals(builder.compareTo(orig), 0);
+        Assert.assertEquals(orig.compareTo(builder), 0);
+    }
+
+    @Test
+    public void testSubSequenceCompareTo() {
+        final ByteString orig = ByteString.valueOfUtf8("this is a ByteString");
+        final ByteStringBuilder builder = new ByteStringBuilder(orig);
+        final ByteSequence subSequence = builder.subSequence(0, 4);
+        Assert.assertEquals(subSequence.compareTo(subSequence), 0);
+        Assert.assertTrue(subSequence.compareTo(orig) < 0);
+        Assert.assertTrue(orig.compareTo(subSequence) > 0);
+    }
+
+    @Test
+    public void testSubSequenceEqualsAndHashCode() {
+        final ByteString orig = ByteString.valueOfUtf8("this is a ByteString");
+        final ByteStringBuilder builder = new ByteStringBuilder(orig);
+        final ByteSequence subSequence = builder.subSequence(0, builder.length());
+        final ByteSequence subSequence2 = builder.subSequence(0, builder.length());
+        Assert.assertTrue(subSequence.hashCode() != 0);
+        Assert.assertTrue(subSequence.equals(subSequence));
+        Assert.assertTrue(subSequence.equals(subSequence2));
+        Assert.assertTrue(subSequence.equals(builder));
+        Assert.assertFalse(subSequence.equals(null));
+    }
+
+    @Test
+    public void testSubSequenceIsEmpty() {
+        final ByteStringBuilder builder = new ByteStringBuilder();
+        Assert.assertTrue(builder.subSequence(0, builder.length()).isEmpty());
+        builder.appendUtf8("This is a ByteString");
+        Assert.assertFalse(builder.subSequence(0, builder.length()).isEmpty());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java
new file mode 100644
index 0000000..5fe9304
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ByteStringTestCase.java
@@ -0,0 +1,363 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.util.Arrays;
+
+import javax.xml.bind.DatatypeConverter;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+
+/**
+ * This class defines a set of tests for the ByteString class.
+ */
+@SuppressWarnings("javadoc")
+public class ByteStringTestCase extends ByteSequenceTestCase {
+    /**
+     * ByteString data provider.
+     *
+     * @return The array of ByteStrings and the bytes it should contain.
+     */
+    @DataProvider(name = "byteSequenceProvider")
+    public Object[][] byteSequenceProvider() throws Exception {
+        byte[] testBytes =
+                new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05,
+                    (byte) 0x06, (byte) 0x07, (byte) 0x08 };
+
+        return new Object[][] {
+            { ByteString.empty(), new byte[0] },
+            { ByteString.valueOfBase64("AAA="), new byte[] { 0x00, 0x00 }},
+            { ByteString.valueOfBase64("AAAA"), new byte[] { 0x00, 0x00, 0x00 }},
+            { ByteString.valueOfBase64("AAAAAA=="), new byte[] { 0x00, 0x00, 0x00, 0x00 }},
+            { ByteString.valueOfInt(1),
+                new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 } },
+            { ByteString.valueOfInt(Integer.MAX_VALUE),
+                new byte[] { (byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF } },
+            { ByteString.valueOfInt(Integer.MIN_VALUE),
+                new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 } },
+            {
+                ByteString.valueOfLong(Long.MAX_VALUE),
+                new byte[] { (byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
+                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF } },
+            {
+                ByteString.valueOfLong(Long.MIN_VALUE),
+                new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                    (byte) 0x00, (byte) 0x00, (byte) 0x00 } },
+            { ByteString.valueOfUtf8("cn=testvalue"), "cn=testvalue".getBytes("UTF-8") },
+            { ByteString.valueOfObject("cn=testvalue"), "cn=testvalue".getBytes("UTF-8") },
+            { ByteString.valueOfUtf8("cn=testvalue".toCharArray()), "cn=testvalue".getBytes("UTF-8") },
+            { ByteString.valueOfObject("cn=testvalue".toCharArray()),
+                "cn=testvalue".getBytes("UTF-8") },
+            { ByteString.valueOfBytes(new byte[0]), new byte[0] },
+            { ByteString.valueOfBytes(testBytes), testBytes },
+            { ByteString.valueOfObject(testBytes), testBytes },
+            { ByteString.valueOfObject(ByteString.valueOfUtf8("cn=testvalue")),
+                "cn=testvalue".getBytes("UTF-8") },
+            { ByteString.wrap(new byte[0]), new byte[0] },
+            {
+                ByteString.wrap(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08 }),
+                new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05,
+                    (byte) 0x06, (byte) 0x07, (byte) 0x08 } },
+            {
+                ByteString.wrap(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x10 },
+                        0, 8),
+                new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05,
+                    (byte) 0x06, (byte) 0x07, (byte) 0x08 } },
+            {
+                ByteString.wrap(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x10 },
+                        1, 8),
+                new byte[] { (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06,
+                    (byte) 0x07, (byte) 0x08, (byte) 0x09 } },
+            {
+                ByteString.wrap(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x10 },
+                        2, 8),
+                new byte[] { (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+                    (byte) 0x08, (byte) 0x09, (byte) 0x10 } },
+            {
+                ByteString.wrap(new byte[] { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04,
+                    (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08 }, 3, 0), new byte[0] }, };
+    }
+
+    @DataProvider(name = "byteStringIntegerProvider")
+    public Object[][] byteStringIntegerProvider() {
+        return new Object[][] { { ByteString.valueOfInt(0), 0 }, { ByteString.valueOfInt(1), 1 },
+            { ByteString.valueOfInt(Integer.MAX_VALUE), Integer.MAX_VALUE },
+            { ByteString.valueOfInt(Integer.MIN_VALUE), Integer.MIN_VALUE }, };
+    }
+
+    @DataProvider(name = "byteStringLongProvider")
+    public Object[][] byteStringLongProvider() {
+        return new Object[][] { { ByteString.valueOfLong(0L), 0L }, { ByteString.valueOfLong(1L), 1L },
+            { ByteString.valueOfLong(Long.MAX_VALUE), Long.MAX_VALUE },
+            { ByteString.valueOfLong(Long.MIN_VALUE), Long.MIN_VALUE } };
+    }
+
+    @DataProvider(name = "byteStringCharArrayProvider")
+    public Object[][] byteStringCharArrayProvider() {
+        return new Object[][] { { "" }, { "1" }, { "1234567890" } };
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testInvalidWrapLength() {
+        ByteString.wrap(new byte[] { (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03 }, 2, 8);
+    }
+
+    @Test(dataProvider = "byteStringIntegerProvider")
+    public void testToInteger(final ByteString bs, final int i) {
+        Assert.assertEquals(bs.toInt(), i);
+    }
+
+    @Test(dataProvider = "byteStringLongProvider")
+    public void testToLong(final ByteString bs, final long l) {
+        Assert.assertEquals(bs.toLong(), l);
+    }
+
+    @Test(dataProvider = "byteStringCharArrayProvider")
+    public void testFromStringToCharArray(final String s) {
+        ByteString bs = ByteString.valueOfUtf8(s);
+        Assert.assertTrue(Arrays.equals(bs.toCharArray(), s.toCharArray()));
+    }
+
+    @Test(dataProvider = "byteStringCharArrayProvider")
+    public void testFromCharArrayToCharArray(final String s) {
+        final char[] chars = s.toCharArray();
+        ByteString bs = ByteString.valueOfUtf8(chars);
+        Assert.assertTrue(Arrays.equals(bs.toCharArray(), chars));
+    }
+
+    @Test(dataProvider = "byteStringCharArrayProvider")
+    public void testValueOfCharArray(final String s) {
+        ByteString bs = ByteString.valueOfUtf8(s.toCharArray());
+        Assert.assertEquals(bs.toString(), s);
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testUndersizedToInteger() {
+        ByteString.wrap(new byte[] { (byte) 0x00, (byte) 0x01 }).toInt();
+    }
+
+    @Test(expectedExceptions = IndexOutOfBoundsException.class)
+    public void testUndersizedToLong() {
+        ByteString.wrap(new byte[] { (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03 }).toLong();
+    }
+
+    /**
+     * Base 64 invalid test data provider.
+     *
+     * @return Returns an array of invalid encoded base64 data.
+     */
+    @DataProvider(name = "invalidBase64Data")
+    public Object[][] createInvalidBase64Data() {
+        // FIXME: fix cases ==== and ==x=
+
+        return new Object[][] { { "=" }, { "==" }, { "===" }, { "A" }, { "AA" }, { "AAA" },
+            { "AA`=" }, { "AA~=" }, { "AA!=" }, { "AA@=" }, { "AA#=" }, { "AA$=" }, { "AA%=" },
+            { "AA^=" }, { "AA*=" }, { "AA(=" }, { "AA)=" }, { "AA_=" }, { "AA-=" }, { "AA{=" },
+            { "AA}=" }, { "AA|=" }, { "AA[=" }, { "AA]=" }, { "AA\\=" }, { "AA;=" }, { "AA'=" },
+            { "AA\"=" }, { "AA:=" }, { "AA,=" }, { "AA.=" }, { "AA<=" }, { "AA>=" }, { "AA?=" },
+            { "AA;=" } };
+    }
+
+    /**
+     * Base 64 valid test data provider.
+     *
+     * @return Returns an array of decoded and valid encoded base64 data.
+     */
+    @DataProvider(name = "validBase64Data")
+    public Object[][] createValidBase64Data() {
+        return new Object[][] {
+            { "", "" },
+            { "00", "AA==" },
+            { "01", "AQ==" },
+            { "02", "Ag==" },
+            { "03", "Aw==" },
+            { "04", "BA==" },
+            { "05", "BQ==" },
+            { "06", "Bg==" },
+            { "07", "Bw==" },
+            { "0000", "AAA=" },
+            { "000000", "AAAA" },
+            { "00000000", "AAAAAA==" },
+            {
+                "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f"
+                        + "202122232425262728292a2b2c2d2e2f" + "303132333435363738393a3b3c3d3e3f"
+                        + "404142434445464748494a4b4c4d4e4f" + "505152535455565758595a5b5c5d5e5f"
+                        + "606162636465666768696a6b6c6d6e6f" + "707172737475767778797a7b7c7d7e7f"
+                        + "808182838485868788898a8b8c8d8e8f" + "909192939495969798999a9b9c9d9e9f"
+                        + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf" + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
+                        + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
+                        + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+                "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v"
+                        + "MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f"
+                        + "YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P"
+                        + "kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/"
+                        + "wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v"
+                        + "8PHy8/T19vf4+fr7/P3+/w==" }, };
+    }
+
+    @DataProvider
+    public Object[][] comparatorTest() throws Exception {
+        final Object[][] array = byteSequenceProvider();
+        int len = array.length;
+        final Object[][] result = new Object[len + 4][];
+        for (int i = 0; i < array.length; i++) {
+            final Object[] original = array[i];
+            final Object[] copy = Arrays.copyOf(original, original.length + 1);
+            copy[original.length] = 0;
+            result[i] = copy;
+        }
+        result[len++] = new Object[] { ByteString.empty(), new byte[0], 0 };
+        result[len++] = new Object[] { ByteString.empty(), "bla".getBytes("UTF8"), -3 };
+        result[len++] = new Object[] { ByteString.valueOfUtf8("bla"), new byte[0], 3 };
+        result[len++] = new Object[] { ByteString.valueOfUtf8("bla"), "bla".getBytes("UTF8"), 0 };
+        return result;
+    }
+
+    @Test(dataProvider = "invalidBase64Data",
+            expectedExceptions = { LocalizedIllegalArgumentException.class })
+    public void testValueOfBase64ThrowsLIAE(final String invalidBase64) throws Exception {
+        ByteString.valueOfBase64(invalidBase64);
+    }
+
+    @Test(dataProvider = "validBase64Data")
+    public void testValueOfBase64(final String hexData, final String encodedData) throws Exception {
+        final byte[] data = DatatypeConverter.parseHexBinary(hexData);
+        final byte[] decodedData = ByteString.valueOfBase64(encodedData).toByteArray();
+        Assert.assertEquals(decodedData, data);
+    }
+
+    @Test
+    public void testToHex() throws Exception {
+        ByteString byteString = new ByteStringBuilder().appendUtf8("org=example").toByteString();
+        assertThat(byteString.toHexString()).isEqualTo("6F72673D6578616D706C65");
+
+        assertThat(ByteString.empty().toHexString()).isEqualTo("");
+    }
+
+    @Test
+    public void testToPercentHex() throws Exception {
+        ByteString byteString = new ByteStringBuilder().appendUtf8("org=example").toByteString();
+        assertThat(byteString.toPercentHexString())
+            .isEqualTo("%6F%72%67%3D%65%78%61%6D%70%6C%65");
+    }
+
+    @Test
+    public void testCopyToCharBuffer() throws Exception {
+        String value = "org=example";
+        ByteString byteString = new ByteStringBuilder().appendUtf8(value).toByteString();
+        CharBuffer buffer = CharBuffer.allocate(value.length());
+        final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
+
+        boolean isCopied = byteString.copyTo(buffer, decoder);
+        buffer.flip();
+
+        assertThat(isCopied).isTrue();
+        assertThat(buffer.toString()).isEqualTo(value);
+    }
+
+    @Test
+    public void testCopyToCharBufferFailure() throws Exception {
+        // Non valid UTF-8 byte sequence
+        ByteString byteString = new ByteStringBuilder().appendByte(0x80).toByteString();
+        CharBuffer buffer = CharBuffer.allocate(1);
+        final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
+
+        boolean isCopied = byteString.copyTo(buffer, decoder);
+        buffer.flip();
+
+        assertThat(isCopied).isFalse();
+    }
+
+    @Test
+    public void testCopyToByteBuffer() throws Exception {
+        String value = "org=example";
+        ByteString byteString = new ByteStringBuilder().appendUtf8(value).toByteString();
+        ByteBuffer buffer = ByteBuffer.allocate(value.length());
+
+        byteString.copyTo(buffer);
+        buffer.flip();
+
+        assertSameByteContent(buffer, byteString);
+    }
+
+    private void assertSameByteContent(ByteBuffer buffer, ByteString byteString) {
+        for (byte b : byteString.toByteArray()) {
+            assertThat(buffer.get()).isEqualTo(b);
+        }
+    }
+
+    @Test
+    public void testToHexPlusAsciiString() throws Exception {
+        final String eol = System.getProperty("line.separator");
+        ByteString byteString = new ByteStringBuilder().appendUtf8("cn=testvalue,org=example").toByteString();
+        assertThat(byteString.toHexPlusAsciiString(10)).isEqualTo(
+              "          63 6E 3D 74 65 73 74 76   61 6C 75 65 2C 6F 72 67  cn=testv alue,org" + eol
+            + "          3D 65 78 61 6D 70 6C 65                            =example " + eol);
+
+        assertThat(byteString.toHexPlusAsciiString(0)).isEqualTo(
+              "63 6E 3D 74 65 73 74 76   61 6C 75 65 2C 6F 72 67  cn=testv alue,org" + eol
+            + "3D 65 78 61 6D 70 6C 65                            =example " + eol);
+    }
+
+    @Test
+    public void testValueOfHex() {
+        ByteString byteString = ByteString.valueOfHex("636E3D7465737476616C7565");
+        assertThat(byteString.toString()).isEqualTo("cn=testvalue");
+    }
+
+    @Test
+    public void testToHexValueOfHex() {
+        ByteString bs = ByteString.valueOfUtf8("cn=testvalue");
+        ByteString roundtrippedBS = ByteString.valueOfHex(bs.toHexString());
+        assertThat(roundtrippedBS).isEqualTo(bs);
+    }
+
+    @Test
+    public void testValueOfEmptyHex() {
+        ByteString byteString = ByteString.valueOfHex("");
+        assertThat(byteString.toString()).isEqualTo("");
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfInvalidHex() {
+        ByteString.valueOfHex("636E3D746573x7476616C7565");
+    }
+
+    @Test(dataProvider = "comparatorTest")
+    public void testComparator(final ByteSequence bs, final byte[] ba, final int expectedCmp) throws Exception {
+        assertThat(ByteSequence.COMPARATOR.compare(bs, ByteString.wrap(ba))).isEqualTo(expectedCmp);
+    }
+
+    @Test(dataProvider = "comparatorTest")
+    public void testByteArrayComparator(final ByteSequence bs, final byte[] ba, final int expectedCmp)
+            throws Exception {
+        assertThat(ByteSequence.BYTE_ARRAY_COMPARATOR.compare(bs.toByteArray(), ba)).isEqualTo(expectedCmp);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java
new file mode 100644
index 0000000..2b26ad0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java
@@ -0,0 +1,90 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+
+/**
+ * This class defines a set of tests for the org.forgerock.opendj.ldap.DN.CompactDn class.
+ */
+@SuppressWarnings("javadoc")
+public class CompactDnTestCase extends SdkTestCase {
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider
+    public Object[][] equivalentDnRepresentations() {
+        return new Object[][] {
+            { "" , "" },
+            { "   " , "" },
+            { "cn=" , "cn=" },
+            { "cn= " , "cn=" },
+            { "cn =" , "cn=" },
+            { "cn = " , "cn=" },
+            { "dc=com" , "dc=com" },
+            { "dc=com+o=com" , "dc=com+o=com" },
+            { "DC=COM" , "DC=COM" },
+            { "dc = com" , "dc=com" },
+            { " dc = com " , "dc=com" },
+            { "dc=example,dc=com" , "dc=example,dc=com" },
+            { "dc=example, dc=com" , "dc=example,dc=com" },
+            { "dc=example ,dc=com" , "dc=example,dc=com" },
+            { "dc =example , dc  =   com" , "dc=example,dc=com" },
+            { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
+                "givenName=John+cn=Doe,ou=People,dc=example,dc=com" },
+            { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
+                "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com" },
+            { "cn=Doe\\, John,ou=People,dc=example,dc=com", "cn=Doe\\, John,ou=People,dc=example,dc=com" },
+            { "UID=jsmith,DC=example,DC=net", "UID=jsmith,DC=example,DC=net" },
+            { "OU=Sales+CN=J. Smith,DC=example,DC=net",
+                "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+                "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net" },
+            { "CN=John Smith\\2C III,DC=example,DC=net", "CN=John Smith\\, III,DC=example,DC=net" },
+            { "CN=\\23John Smith\\20,DC=example,DC=net", "CN=\\#John Smith\\ ,DC=example,DC=net" },
+            { "CN=Before\\0dAfter,DC=example,DC=net",
+                // \0d is a hex representation of Carriage return. It is mapped
+                // to a SPACE as defined in the MAP ( RFC 4518)
+                "CN=Before\\0dAfter,DC=example,DC=net" },
+            { "2.5.4.3=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "2.5.4.3=\\04\\02Hi" },
+            { "1.1.1=" , "1.1.1=" },
+            { "CN=Lu\\C4\\8Di\\C4\\87" , "CN=Lu\u010di\u0107" },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8,o=Airius", "ou=\u55b6\u696d\u90e8,o=Airius" },
+            { "photo=\\ john \\ ,dc=com" , "photo=\\ john \\ ,dc=com" },
+            { "AB-global=" , "AB-global=" },
+            { "OU= Sales + CN = J. Smith ,DC=example,DC=net", "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "cn=John+a=b" , "cn=John+a=b" },
+            { "O=\"Sue, Grabbit and Runn\",C=US", "O=Sue\\, Grabbit and Runn,C=US" }, };
+    }
+
+    @Test(dataProvider = "equivalentDnRepresentations")
+    public void testEquals(String dn, String otherDn) throws Exception {
+        assertThat(DN.valueOf(dn).compact()).isEqualTo(DN.valueOf(otherDn).compact());
+    }
+
+    @Test(dataProvider = "equivalentDnRepresentations")
+    public void testCompareTo(String dn, String otherDn) throws Exception {
+        assertThat(DN.valueOf(dn).compact().compareTo(DN.valueOf(otherDn).compact())).isEqualTo(0);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConditionResultTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConditionResultTestCase.java
new file mode 100644
index 0000000..c61adf9
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConditionResultTestCase.java
@@ -0,0 +1,175 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008 Sun Microsystems, Inc.
+ * Portions Copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.List;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.ConditionResult.*;
+import static org.testng.Assert.*;
+
+/**
+ * A set of test cases for the ConditionResult class.
+ */
+@SuppressWarnings("javadoc")
+public class ConditionResultTestCase extends SdkTestCase {
+
+    @DataProvider
+    public Iterator<Object[]> allPermutationsUpTo4Operands() {
+        final ConditionResult[] values = ConditionResult.values();
+
+        final List<Object[]> results = new ArrayList<>();
+        results.add(new Object[] { new ConditionResult[0] });
+        for (int arrayLength = 1; arrayLength <= 4; arrayLength++) {
+            final ConditionResult[] template = new ConditionResult[arrayLength];
+            allSubPermutations(values, results, template, 0, arrayLength);
+        }
+        return results.iterator();
+    }
+
+    private void allSubPermutations(ConditionResult[] values, final List<Object[]> results,
+            final ConditionResult[] template, int currentIndex, int endIndex) {
+        if (currentIndex < endIndex) {
+            for (ConditionResult r : values) {
+                template[currentIndex] = r;
+                allSubPermutations(values, results, template, currentIndex + 1, endIndex);
+                if (currentIndex + 1 == endIndex) {
+                    results.add(new Object[] {
+                          Arrays.copyOf(template, template.length, ConditionResult[].class)
+                    });
+                }
+            }
+        }
+    }
+
+    /**
+     * Tests some basic assumptions of the enumeration.
+     */
+    @Test
+    public void testBasic() {
+        assertEquals(values().length, 3);
+        assertNotSame(TRUE, FALSE);
+        assertNotSame(FALSE, UNDEFINED);
+        assertNotSame(UNDEFINED, TRUE);
+    }
+
+    @Test
+    public void testNot() {
+        assertEquals(not(FALSE), TRUE);
+        assertEquals(not(TRUE), FALSE);
+        assertEquals(not(UNDEFINED), UNDEFINED);
+    }
+
+    @Test
+    public void testAnd() {
+        assertEquals(and(), TRUE);
+
+        assertEquals(and(TRUE), TRUE);
+        assertEquals(and(FALSE), FALSE);
+        assertEquals(and(UNDEFINED), UNDEFINED);
+
+        assertEquals(and(TRUE, TRUE), TRUE);
+        assertEquals(and(FALSE, FALSE), FALSE);
+        assertEquals(and(UNDEFINED, UNDEFINED), UNDEFINED);
+
+        assertAndIsCommutative(TRUE, FALSE, FALSE);
+        assertAndIsCommutative(TRUE, UNDEFINED, UNDEFINED);
+        assertAndIsCommutative(FALSE, UNDEFINED, FALSE);
+    }
+
+    private void assertAndIsCommutative(ConditionResult operand1, ConditionResult operand2,
+            ConditionResult expectedResult) {
+        assertEquals(and(operand1, operand2), expectedResult);
+        assertEquals(and(operand2, operand1), expectedResult);
+    }
+
+    @Test(dataProvider = "allPermutationsUpTo4Operands")
+    public void testVarargsAndIsCommutative(ConditionResult[] operands) {
+        if (operands.length == 0) {
+            assertEquals(and(operands), and());
+            return;
+        }
+
+        final EnumSet<ConditionResult> ops = EnumSet.copyOf(Arrays.asList(operands));
+        if (ops.contains(FALSE)) {
+            assertEquals(and(operands), FALSE);
+        } else if (ops.contains(UNDEFINED)) {
+            assertEquals(and(operands), UNDEFINED);
+        } else {
+            assertEquals(and(operands), TRUE);
+        }
+    }
+
+    @Test
+    public void testOr() {
+        assertEquals(or(), FALSE);
+
+        assertEquals(or(TRUE), TRUE);
+        assertEquals(or(FALSE), FALSE);
+        assertEquals(or(UNDEFINED), UNDEFINED);
+
+        assertEquals(or(TRUE, TRUE), TRUE);
+        assertEquals(or(FALSE, FALSE), FALSE);
+        assertEquals(or(UNDEFINED, UNDEFINED), UNDEFINED);
+
+        assertOrIsCommutative(TRUE, FALSE, TRUE);
+        assertOrIsCommutative(TRUE, UNDEFINED, TRUE);
+        assertOrIsCommutative(FALSE, UNDEFINED, UNDEFINED);
+    }
+
+    private void assertOrIsCommutative(ConditionResult operand1, ConditionResult operand2,
+            ConditionResult expectedResult) {
+        assertEquals(or(operand1, operand2), expectedResult);
+        assertEquals(or(operand2, operand1), expectedResult);
+    }
+
+    @Test(dataProvider = "allPermutationsUpTo4Operands")
+    public void testVarargsOrIsCommutative(ConditionResult[] operands) {
+        if (operands.length == 0) {
+            assertEquals(or(operands), or());
+            return;
+        }
+
+        final EnumSet<ConditionResult> ops = EnumSet.copyOf(Arrays.asList(operands));
+        if (ops.contains(TRUE)) {
+            assertEquals(or(operands), TRUE);
+        } else if (ops.contains(UNDEFINED)) {
+            assertEquals(or(operands), UNDEFINED);
+        } else {
+            assertEquals(or(operands), FALSE);
+        }
+    }
+
+    @Test
+    public void testValueOf() {
+        assertEquals(valueOf(true), TRUE);
+        assertEquals(valueOf(false), FALSE);
+    }
+
+    @Test
+    public void testToBoolean() {
+        assertEquals(TRUE.toBoolean(), true);
+        assertEquals(FALSE.toBoolean(), false);
+        assertEquals(UNDEFINED.toBoolean(), false);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionPoolTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionPoolTestCase.java
new file mode 100644
index 0000000..3261d98
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionPoolTestCase.java
@@ -0,0 +1,539 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.Connections.*;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests the connection pool implementation..
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionPoolTestCase extends SdkTestCase {
+
+    /**
+     * A connection event listener registered against a pooled connection should
+     * be notified when the pooled connection is closed, NOT when the underlying
+     * connection is closed.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testConnectionEventListenerClose() throws Exception {
+        final Connection pooledConnection = mock(Connection.class);
+        when(pooledConnection.isValid()).thenReturn(true);
+        final ConnectionFactory factory = mockConnectionFactory(pooledConnection);
+        final ConnectionPool pool = newFixedConnectionPool(factory, 1);
+        final Connection connection = pool.getConnection();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        connection.addConnectionEventListener(listener);
+        connection.close();
+
+        verify(listener).handleConnectionClosed();
+        verify(listener, times(0)).handleConnectionError(anyBoolean(), any(LdapException.class));
+        verify(listener, times(0)).handleUnsolicitedNotification(any(ExtendedResult.class));
+
+        // Get a connection again and make sure that the listener is no longer invoked.
+        pool.getConnection().close();
+        verifyNoMoreInteractions(listener);
+    }
+
+    /**
+     * A connection event listener registered against a pooled connection should
+     * be notified whenever an error occurs on the underlying connection.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testConnectionEventListenerError() throws Exception {
+        final List<ConnectionEventListener> listeners = new LinkedList<>();
+        final Connection mockConnection = mockConnection(listeners);
+        final ConnectionFactory factory = mockConnectionFactory(mockConnection);
+        final ConnectionPool pool = newFixedConnectionPool(factory, 1);
+        final Connection connection = pool.getConnection();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        connection.addConnectionEventListener(listener);
+        assertThat(listeners).hasSize(1);
+        listeners.get(0).handleConnectionError(false,
+                newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN));
+        verify(listener, times(0)).handleConnectionClosed();
+        verify(listener).handleConnectionError(eq(false), isA(ConnectionException.class));
+        verify(listener, times(0)).handleUnsolicitedNotification(any(ExtendedResult.class));
+        connection.close();
+        assertThat(listeners).hasSize(0);
+    }
+
+    /**
+     * A connection event listener registered against a pooled connection should
+     * be notified whenever an unsolicited notification is received on the
+     * underlying connection.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testConnectionEventListenerUnsolicitedNotification() throws Exception {
+        final List<ConnectionEventListener> listeners = new LinkedList<>();
+        final Connection mockConnection = mockConnection(listeners);
+        final ConnectionFactory factory = mockConnectionFactory(mockConnection);
+        final ConnectionPool pool = newFixedConnectionPool(factory, 1);
+        final Connection connection = pool.getConnection();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        connection.addConnectionEventListener(listener);
+        assertThat(listeners).hasSize(1);
+        listeners.get(0).handleUnsolicitedNotification(
+                Responses.newGenericExtendedResult(ResultCode.OTHER));
+        verify(listener, times(0)).handleConnectionClosed();
+        verify(listener, times(0)).handleConnectionError(anyBoolean(), any(LdapException.class));
+        verify(listener).handleUnsolicitedNotification(any(ExtendedResult.class));
+        connection.close();
+        assertThat(listeners).hasSize(0);
+    }
+
+    /**
+     * Test basic pool functionality:
+     * <ul>
+     * <li>create a pool of size 2
+     * <li>get 2 connections and make sure that they are usable
+     * <li>close the pooled connections and check that the underlying
+     * connections are not closed
+     * <li>close the pool and check that underlying connections are closed.
+     * </ul>
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testConnectionLifeCycle() throws Exception {
+        // Setup.
+        final BindRequest bind1 =
+                Requests.newSimpleBindRequest("cn=test1", "password".toCharArray());
+        final Connection connection1 = mock(Connection.class);
+        when(connection1.bind(bind1)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection1.isValid()).thenReturn(true);
+
+        final BindRequest bind2 =
+                Requests.newSimpleBindRequest("cn=test2", "password".toCharArray());
+        final Connection connection2 = mock(Connection.class);
+        when(connection2.bind(bind2)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection2.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory = mockConnectionFactory(connection1, connection2);
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, 2);
+
+        verifyZeroInteractions(factory);
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+
+        /*
+         * Obtain connections and verify that the correct underlying connection
+         * is used. We can check the returned connection directly since it is a
+         * wrapper, so we'll route a bind request instead and check that the
+         * underlying connection got it.
+         */
+        final Connection pc1 = pool.getConnection();
+        assertThat(pc1.bind(bind1).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(1)).getConnection();
+        verify(connection1).bind(bind1);
+        verifyZeroInteractions(connection2);
+
+        final Connection pc2 = pool.getConnection();
+        assertThat(pc2.bind(bind2).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(2)).getConnection();
+        verifyNoMoreInteractions(connection1);
+        verify(connection2).bind(bind2);
+
+        // Release pooled connections (should not close underlying connection).
+        pc1.close();
+        assertThat(pc1.isValid()).isFalse();
+        assertThat(pc1.isClosed()).isTrue();
+        verify(connection1, times(0)).close();
+
+        pc2.close();
+        assertThat(pc2.isValid()).isFalse();
+        assertThat(pc2.isClosed()).isTrue();
+        verify(connection2, times(0)).close();
+
+        // Close the pool (should close underlying connections).
+        pool.close();
+        verify(connection1).close();
+        verify(connection2).close();
+    }
+
+    /**
+     * Test behavior of pool at capacity.
+     * <ul>
+     * <li>create a pool of size 2
+     * <li>get 2 connections and make sure that they are usable
+     * <li>attempt to get third connection asynchronously and verify that no
+     * connection was available
+     * <li>release (close) a pooled connection
+     * <li>verify that third attempt now completes.
+     * </ul>
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testGetConnectionAtCapacity() throws Exception {
+        // Setup.
+        final Connection connection1 = mock(Connection.class);
+        when(connection1.isValid()).thenReturn(true);
+
+        final BindRequest bind2 =
+                Requests.newSimpleBindRequest("cn=test2", "password".toCharArray());
+        final Connection connection2 = mock(Connection.class);
+        when(connection2.bind(bind2)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection2.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory = mockConnectionFactory(connection1, connection2);
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, 2);
+
+        // Fully utilize the pool.
+        final Connection pc1 = pool.getConnection();
+        final Connection pc2 = pool.getConnection();
+
+        /*
+         * Grab another connection and check that this attempt blocks (if there
+         * is a connection available immediately then the promise will be
+         * completed immediately).
+         */
+        final Promise<? extends Connection, LdapException> promise = pool.getConnectionAsync();
+        assertThat(promise.isDone()).isFalse();
+
+        // Release a connection and verify that it is immediately redeemed by the promise.
+        pc2.close();
+        assertThat(promise.isDone()).isTrue();
+
+        // Check that returned connection routes request to released connection.
+        final Connection pc3 = promise.get();
+        assertThat(pc3.bind(bind2).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(2)).getConnection();
+        verify(connection2).bind(bind2);
+
+        pc1.close();
+        pc2.close();
+        pc3.close();
+        pool.close();
+    }
+
+    /**
+     * Verifies that stale connections which have become invalid while in use
+     * are not placed back in the pool after being closed.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testSkipStaleConnectionsOnClose() throws Exception {
+        // Setup.
+        final Connection connection1 = mock(Connection.class);
+        when(connection1.isValid()).thenReturn(true);
+
+        final BindRequest bind2 =
+                Requests.newSimpleBindRequest("cn=test2", "password".toCharArray());
+        final Connection connection2 = mock(Connection.class);
+        when(connection2.bind(bind2)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection2.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory = mockConnectionFactory(connection1, connection2);
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, 2);
+
+        /*
+         * Simulate remote disconnect of connection1 while application is using
+         * the pooled connection. The pooled connection should be recycled
+         * immediately on release.
+         */
+        final Connection pc1 = pool.getConnection();
+        when(connection1.isValid()).thenReturn(false);
+        assertThat(connection1.isValid()).isFalse();
+        assertThat(pc1.isValid()).isFalse();
+        pc1.close();
+        assertThat(connection1.isValid()).isFalse();
+        verify(connection1).close();
+
+        // Now get another connection and check that it is ok.
+        final Connection pc2 = pool.getConnection();
+        assertThat(pc2.isValid()).isTrue();
+        assertThat(pc2.bind(bind2).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(2)).getConnection();
+        verify(connection2).bind(bind2);
+
+        pc2.close();
+        pool.close();
+    }
+
+    /**
+     * Verifies that stale connections which have become invalid while cached in
+     * the internal pool are not returned to the caller. This may occur when an
+     * idle pooled connection is disconnect by the remote server after a
+     * timeout. The connection pool must detect that the pooled connection is
+     * invalid in order to avoid returning it to the caller.
+     * <p>
+     * See issue OPENDJ-590.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testSkipStaleConnectionsOnGet() throws Exception {
+        // Setup.
+        final Connection connection1 = mock(Connection.class);
+        when(connection1.isValid()).thenReturn(true);
+
+        final BindRequest bind2 =
+                Requests.newSimpleBindRequest("cn=test2", "password".toCharArray());
+        final Connection connection2 = mock(Connection.class);
+        when(connection2.bind(bind2)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection2.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory = mockConnectionFactory(connection1, connection2);
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, 2);
+
+        // Get and release a single connection.
+        pool.getConnection().close();
+
+        /*
+         * Simulate remote disconnect of connection1. The next connection
+         * attempt should return connection2.
+         */
+        when(connection1.isValid()).thenReturn(false);
+        assertThat(connection1.isValid()).isFalse();
+        assertThat(connection2.isValid()).isTrue();
+        final Connection pc = pool.getConnection();
+
+        // Check that returned connection routes request to connection2.
+        assertThat(pc.isValid()).isTrue();
+        verify(connection1).close();
+        assertThat(pc.bind(bind2).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(2)).getConnection();
+        verify(connection2).bind(bind2);
+
+        pc.close();
+        pool.close();
+    }
+
+    /**
+     * Verifies that a fully allocated pool whose connections are stale due to
+     * idle timeouts still allocates new connections.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testSkipStaleConnectionsOnGetWhenAtCapacity() throws Exception {
+        // Setup.
+        final Connection connection1 = mock(Connection.class);
+        when(connection1.isValid()).thenReturn(true);
+
+        final Connection connection2 = mock(Connection.class);
+        when(connection2.isValid()).thenReturn(true);
+
+        final BindRequest bind3 =
+                Requests.newSimpleBindRequest("cn=test2", "password".toCharArray());
+        final Connection connection3 = mock(Connection.class);
+        when(connection3.bind(bind3)).thenReturn(Responses.newBindResult(ResultCode.SUCCESS));
+        when(connection3.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory =
+                mockConnectionFactory(connection1, connection2, connection3);
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, 2);
+
+        // Fully allocate the pool.
+        final Connection pc1 = pool.getConnection();
+        final Connection pc2 = pool.getConnection();
+        pc1.close();
+        pc2.close();
+
+        /*
+         * Simulate remote disconnect of connection1 and connection2. The next
+         * connection attempt should return connection3.
+         */
+        when(connection1.isValid()).thenReturn(false);
+        when(connection2.isValid()).thenReturn(false);
+        final Connection pc3 = pool.getConnection();
+
+        // Check that returned connection routes request to connection3.
+        assertThat(pc3.isValid()).isTrue();
+        verify(connection1).close();
+        verify(connection2).close();
+        assertThat(pc3.bind(bind3).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+        verify(factory, times(3)).getConnection();
+        verify(connection3).bind(bind3);
+
+        pc3.close();
+        pool.close();
+    }
+
+    /**
+     * Verifies that a pool with connection keep alive correctly purges idle
+     * connections after the keepalive period has expired.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testConnectionKeepAliveExpiration() throws Exception {
+        final Connection pooledConnection1 = mock(Connection.class, "pooledConnection1");
+        final Connection pooledConnection2 = mock(Connection.class, "pooledConnection2");
+        final Connection pooledConnection3 = mock(Connection.class, "pooledConnection3");
+        final Connection pooledConnection4 = mock(Connection.class, "pooledConnection4");
+        final Connection pooledConnection5 = mock(Connection.class, "pooledConnection5");
+        final Connection pooledConnection6 = mock(Connection.class, "pooledConnection6");
+
+        when(pooledConnection1.isValid()).thenReturn(true);
+        when(pooledConnection2.isValid()).thenReturn(true);
+        when(pooledConnection3.isValid()).thenReturn(true);
+        when(pooledConnection4.isValid()).thenReturn(true);
+        when(pooledConnection5.isValid()).thenReturn(true);
+        when(pooledConnection6.isValid()).thenReturn(true);
+
+        final ConnectionFactory factory =
+                mockConnectionFactory(pooledConnection1, pooledConnection2, pooledConnection3,
+                        pooledConnection4, pooledConnection5, pooledConnection6);
+        final MockScheduler scheduler = new MockScheduler();
+        final CachedConnectionPool pool =
+                new CachedConnectionPool(factory, 2, 4, 100, TimeUnit.MILLISECONDS, scheduler);
+        assertThat(scheduler.isScheduled()).isTrue();
+
+        // First populate the pool with idle connections at time 0.
+        pool.timeService = mockTimeService(0);
+
+        assertThat(pool.currentPoolSize()).isEqualTo(0);
+        Connection c1 = pool.getConnection();
+        Connection c2 = pool.getConnection();
+        Connection c3 = pool.getConnection();
+        Connection c4 = pool.getConnection();
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+        c1.close();
+        c2.close();
+        c3.close();
+        c4.close();
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+
+        // First purge at time 50 is no-op because no connections have expired.
+        when(pool.timeService.now()).thenReturn(50L);
+        scheduler.runFirstTask();
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+
+        // Second purge at time 150 should remove 2 non-core connections.
+        when(pool.timeService.now()).thenReturn(150L);
+        scheduler.runFirstTask();
+        assertThat(pool.currentPoolSize()).isEqualTo(2);
+
+        verify(pooledConnection1, times(1)).close();
+        verify(pooledConnection2, times(1)).close();
+        verify(pooledConnection3, times(0)).close();
+        verify(pooledConnection4, times(0)).close();
+
+        // Regrow the pool at time 200.
+        when(pool.timeService.now()).thenReturn(200L);
+        Connection c5 = pool.getConnection(); // pooledConnection3
+        Connection c6 = pool.getConnection(); // pooledConnection4
+        Connection c7 = pool.getConnection(); // pooledConnection5
+        Connection c8 = pool.getConnection(); // pooledConnection6
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+        c5.close();
+        c6.close();
+        c7.close();
+        c8.close();
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+
+        // Third purge at time 250 should not remove any connections.
+        when(pool.timeService.now()).thenReturn(250L);
+        scheduler.runFirstTask();
+        assertThat(pool.currentPoolSize()).isEqualTo(4);
+
+        // Fourth purge at time 350 should remove 2 non-core connections.
+        when(pool.timeService.now()).thenReturn(350L);
+        scheduler.runFirstTask();
+        assertThat(pool.currentPoolSize()).isEqualTo(2);
+
+        verify(pooledConnection3, times(1)).close();
+        verify(pooledConnection4, times(1)).close();
+        verify(pooledConnection5, times(0)).close();
+        verify(pooledConnection6, times(0)).close();
+
+        pool.close();
+        verify(pooledConnection5, times(1)).close();
+        verify(pooledConnection6, times(1)).close();
+        assertThat(scheduler.isScheduled()).isFalse();
+    }
+
+    /**
+     * Test that all outstanding pending connection promises are completed when a
+     * connection request fails.
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test(description = "OPENDJ-1348", timeOut = 10000)
+    public void testNewConnectionFailureFlushesAllPendingPromises() throws Exception {
+        final ConnectionFactory factory = mock(ConnectionFactory.class);
+        final int poolSize = 2;
+        final ConnectionPool pool = Connections.newFixedConnectionPool(factory, poolSize);
+        doAnswer(new Answer<Promise<Connection, LdapException>>() {
+            @Override
+            public Promise<Connection, LdapException> answer(final InvocationOnMock invocation)
+                    throws Throwable {
+                return PromiseImpl.create();
+            }
+        }).when(factory).getConnectionAsync();
+
+        List<Promise<? extends Connection, LdapException>> promises = new ArrayList<>();
+        for (int i = 0; i < poolSize + 1; i++) {
+            promises.add(pool.getConnectionAsync());
+        }
+        // factory.getConnectionAsync() has been called by the pool poolSize times
+        verify(factory, times(poolSize)).getConnectionAsync();
+        final LdapException connectError = newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+        for (Promise<? extends Connection, LdapException> promise : promises) {
+            // Simulate that an error happened with the created connections
+            ((PromiseImpl) promise).handleException(connectError);
+
+            try {
+                // Before the fix for OPENDJ-1348 the third promise.get() would hang.
+                promise.getOrThrow();
+                Assert.fail("Expected an exception to be thrown");
+            } catch (LdapException e) {
+                assertThat(e).isSameAs(connectError);
+            }
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionsTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionsTestCase.java
new file mode 100644
index 0000000..5d72977
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ConnectionsTestCase.java
@@ -0,0 +1,152 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Connections.newShardedRequestLoadBalancerFunction;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.GSSAPISASLBindRequest;
+import org.forgerock.opendj.ldap.requests.GenericExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
+import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class ConnectionsTestCase extends SdkTestCase {
+
+    @Test
+    public void testUncloseableConnectionClose() throws Exception {
+        final Connection connection = mock(Connection.class);
+        final Connection uncloseable = Connections.uncloseable(connection);
+        uncloseable.close();
+        verifyZeroInteractions(connection);
+    }
+
+    @Test
+    public void testUncloseableConnectionNotClose() throws Exception {
+        final Connection connection = mock(Connection.class);
+        final Connection uncloseable = Connections.uncloseable(connection);
+        uncloseable.applyChange(null);
+        verify(connection).applyChange(null);
+    }
+
+    @Test
+    public void testUncloseableConnectionUnbind() throws Exception {
+        final Connection connection = mock(Connection.class);
+        final Connection uncloseable = Connections.uncloseable(connection);
+        uncloseable.close(null, null);
+        verifyZeroInteractions(connection);
+    }
+
+    @Test
+    public void shardedRequestLoadBalancerUsesConsistentIndexing() {
+        final Function<Request, Integer, NeverThrowsException> f =
+                newShardedRequestLoadBalancerFunction(asList(mock(ConnectionFactory.class),
+                                                             mock(ConnectionFactory.class)));
+
+        // These two DNs have a different hash code.
+        final DN dn1 = DN.valueOf("cn=target1,dc=example,dc=com");
+        final DN dn2 = DN.valueOf("cn=target2,dc=example,dc=com");
+
+        final AddRequest addRequest = mock(AddRequest.class);
+        when(addRequest.getName()).thenReturn(dn1, dn2);
+        final int dn1index = index(f, addRequest);
+        final int dn2index = index(f, addRequest);
+        assertThat(dn1index).isNotEqualTo(dn2index);
+
+        final SimpleBindRequest simpleBindRequest = mock(SimpleBindRequest.class);
+        when(simpleBindRequest.getName()).thenReturn(dn1.toString(), dn2.toString());
+        assertRequestsAreRoutedConsistently(f, simpleBindRequest, dn1index, dn2index);
+
+        final CompareRequest compareRequest = mock(CompareRequest.class);
+        when(compareRequest.getName()).thenReturn(dn1, dn2);
+        assertRequestsAreRoutedConsistently(f, compareRequest, dn1index, dn2index);
+
+        final DeleteRequest deleteRequest = mock(DeleteRequest.class);
+        when(deleteRequest.getName()).thenReturn(dn1, dn2);
+        assertRequestsAreRoutedConsistently(f, deleteRequest, dn1index, dn2index);
+
+        final ModifyRequest modifyRequest = mock(ModifyRequest.class);
+        when(modifyRequest.getName()).thenReturn(dn1, dn2);
+        assertRequestsAreRoutedConsistently(f, modifyRequest, dn1index, dn2index);
+
+        final ModifyDNRequest modifyDNRequest = mock(ModifyDNRequest.class);
+        when(modifyDNRequest.getName()).thenReturn(dn1, dn2);
+        assertRequestsAreRoutedConsistently(f, modifyDNRequest, dn1index, dn2index);
+
+        final SearchRequest searchRequest = mock(SearchRequest.class);
+        when(searchRequest.getName()).thenReturn(dn1, dn2);
+        assertRequestsAreRoutedConsistently(f, searchRequest, dn1index, dn2index);
+
+        // Authzid based operations.
+        final String authzid1 = "dn:" + dn1.toString();
+        final String authzid2 = "dn:" + dn2.toString();
+
+        final PasswordModifyExtendedRequest passwordModifyRequest = mock(PasswordModifyExtendedRequest.class);
+        when(passwordModifyRequest.getUserIdentityAsString()).thenReturn(authzid1, authzid2);
+        assertRequestsAreRoutedConsistently(f, passwordModifyRequest, dn1index, dn2index);
+
+        final PlainSASLBindRequest plainSASLBindRequest = mock(PlainSASLBindRequest.class);
+        when(plainSASLBindRequest.getAuthenticationID()).thenReturn(authzid1, authzid2);
+        assertRequestsAreRoutedConsistently(f, plainSASLBindRequest, dn1index, dn2index);
+
+        final CRAMMD5SASLBindRequest cramMD5SASLBindRequest = mock(CRAMMD5SASLBindRequest.class);
+        when(cramMD5SASLBindRequest.getAuthenticationID()).thenReturn(authzid1, authzid2);
+        assertRequestsAreRoutedConsistently(f, cramMD5SASLBindRequest, dn1index, dn2index);
+
+        final DigestMD5SASLBindRequest digestMD5SASLBindRequest = mock(DigestMD5SASLBindRequest.class);
+        when(digestMD5SASLBindRequest.getAuthenticationID()).thenReturn(authzid1, authzid2);
+        assertRequestsAreRoutedConsistently(f, digestMD5SASLBindRequest, dn1index, dn2index);
+
+        final GSSAPISASLBindRequest gssapiSASLBindRequest = mock(GSSAPISASLBindRequest.class);
+        when(gssapiSASLBindRequest.getAuthenticationID()).thenReturn(authzid1, authzid2);
+        assertRequestsAreRoutedConsistently(f, gssapiSASLBindRequest, dn1index, dn2index);
+
+        // Requests that have no target will return a random index, but since we only have one factory the index will
+        // always be 0.
+        final GenericExtendedRequest genericExtendedRequest = mock(GenericExtendedRequest.class);
+        assertThat(index(f, genericExtendedRequest)).isBetween(0, 1);
+    }
+
+    private void assertRequestsAreRoutedConsistently(final Function<Request, Integer, NeverThrowsException> f,
+                                                     final Request r,
+                                                     final int firstExpectedIndex,
+                                                     final int secondExpectedIndex) {
+        assertThat(index(f, r)).isEqualTo(firstExpectedIndex);
+        assertThat(index(f, r)).isEqualTo(secondExpectedIndex);
+    }
+
+    private int index(final Function<Request, Integer, NeverThrowsException> function, final Request request) {
+        return function.apply(request);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
new file mode 100644
index 0000000..6c03e6f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
@@ -0,0 +1,1396 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.lang.Integer.signum;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.UUID;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This class defines a set of tests for the org.forgerock.opendj.ldap.DN class.
+ */
+@SuppressWarnings("javadoc")
+public class DNTestCase extends SdkTestCase {
+    /**
+     * Child DN test data provider.
+     *
+     * @return The array of test data.
+     */
+    @DataProvider(name = "createChildDNTestData")
+    public Object[][] createChildDNTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", "" },
+            { "", "dc=org", "dc=org" },
+            { "", "dc=opendj,dc=org", "dc=opendj,dc=org" },
+            { "dc=org", "", "dc=org" },
+            { "dc=org", "dc=opendj", "dc=opendj,dc=org" },
+            { "dc=org", "dc=foo,dc=opendj", "dc=foo,dc=opendj,dc=org" },
+            { "dc=opendj,dc=org", "", "dc=opendj,dc=org" },
+            { "dc=opendj,dc=org", "dc=foo", "dc=foo,dc=opendj,dc=org" },
+            { "dc=opendj,dc=org", "dc=bar,dc=foo", "dc=bar,dc=foo,dc=opendj,dc=org" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Child RDN test data provider.
+     *
+     * @return The array of test data.
+     */
+    @DataProvider(name = "createChildRDNTestData")
+    public Object[][] createChildRDNTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "dc=org", "dc=org" },
+            { "dc=org", "dc=opendj", "dc=opendj,dc=org" },
+            { "dc=opendj,dc=org", "dc=foo", "dc=foo,dc=opendj,dc=org" },
+        };
+        // @formatter:on
+
+    }
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "testDNs")
+    public Object[][] createData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", "" },
+            { "   ", "", "" },
+            { "cn=", "cn=", "cn=" },
+            { "cn= ", "cn=", "cn=" },
+            { "cn =", "cn=", "cn=" },
+            { "cn = ", "cn=", "cn=" },
+            { "dc=com", "dc=com", "dc=com" },
+            { "dc=com+o=com", "dc=com+o=com", "dc=com+o=com" },
+            { "DC=COM", "dc=com", "DC=COM" },
+            { "dc = com", "dc=com", "dc=com" },
+            { " dc = com ", "dc=com", "dc=com" },
+            { "dc=example,dc=com", "dc=example,dc=com", "dc=example,dc=com" },
+            { "dc=example, dc=com", "dc=example,dc=com", "dc=example,dc=com" },
+            { "dc=example ,dc=com", "dc=example,dc=com", "dc=example,dc=com" },
+            { "dc =example , dc  =   com", "dc=example,dc=com", "dc=example,dc=com" },
+            { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
+                "cn=doe+givenname=john,ou=people,dc=example,dc=com",
+                "givenName=John+cn=Doe,ou=People,dc=example,dc=com" },
+            { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
+                "givenname=john\\+cn=doe,ou=people,dc=example,dc=com",
+                "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com" },
+            { "cn=Doe\\, John,ou=People,dc=example,dc=com",
+                "cn=doe\\, john,ou=people,dc=example,dc=com",
+                "cn=Doe\\, John,ou=People,dc=example,dc=com" },
+            { "UID=jsmith,DC=example,DC=net", "uid=jsmith,dc=example,dc=net",
+                "UID=jsmith,DC=example,DC=net" },
+            { "OU=Sales+CN=J. Smith,DC=example,DC=net", "cn=j. smith+ou=sales,dc=example,dc=net",
+                "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+                "cn=james \\\"jim\\\" smith\\, iii,dc=example,dc=net",
+                "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net" },
+            { "CN=John Smith\\2C III,DC=example,DC=net", "cn=john smith\\, iii,dc=example,dc=net",
+                "CN=John Smith\\, III,DC=example,DC=net" },
+            { "CN=\\23John Smith\\20,DC=example,DC=net", "cn=\\#john smith,dc=example,dc=net",
+                "CN=\\#John Smith\\ ,DC=example,DC=net" },
+            { "CN=Before\\0dAfter,DC=example,DC=net",
+                // \0d is a hex representation of Carriage return. It is mapped
+                // to a SPACE as defined in the MAP ( RFC 4518)
+                "cn=before after,dc=example,dc=net", "CN=Before\\0dAfter,DC=example,DC=net" },
+            { "2.5.4.3=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "cn=hi", "2.5.4.3=#04024869" },
+            { "1.1.1=", "1.1.1=", "1.1.1=" },
+            { "CN=Lu\\C4\\8Di\\C4\\87", "cn=lu\u010di\u0107", "CN=Lu\u010di\u0107" },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8,o=Airius", "ou=\u55b6\u696d\u90e8,o=airius",
+                "ou=\u55b6\u696d\u90e8,o=Airius" },
+            { "photo=\\ john \\ ,dc=com", "photo=\\ john \\ ,dc=com", "photo=\\ john \\ ,dc=com" },
+            { "AB-global=", "ab-global=", "AB-global=" },
+            { "OU= Sales + CN = J. Smith ,DC=example,DC=net",
+                "cn=j. smith+ou=sales,dc=example,dc=net", "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "cn=John+dc=", "dc=+cn=john", "cn=John+dc=" },
+            { "O=\"Sue, Grabbit + Runn\",C=US", "o=sue\\, grabbit \\+ runn,c=us",
+                "O=Sue\\, Grabbit \\+ Runn,C=US" },
+            { "O=\"John \\\"Tiger\\\" Smith\",C=US", "o=john \\\"tiger\\\" smith,c=us",
+                "O=John \\\"Tiger\\\" Smith,C=US" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * DN comparison test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "createDNComparisonData")
+    public Object[][] createDNComparisonData() {
+        // @formatter:off
+        return new Object[][] {
+            { "cn=hello world,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=hello world,dc=com", "CN=hello world,dc=com", 0 },
+            { "cn=hello   world,dc=com", "cn=hello world,dc=com", 0 },
+            { "  cn =  hello world  ,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=hello world\\ ,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=HELLO WORLD,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=HELLO+sn=WORLD,dc=com", "sn=world+cn=hello,dc=com", 0 },
+            { "governingStructureRule=10,dc=com", "governingStructureRule=9,dc=com", 1 },
+            { "governingStructureRule=999,dc=com", "governingStructureRule=1000,dc=com", -1 },
+            { "governingStructureRule=-1,dc=com", "governingStructureRule=0,dc=com", -1 },
+            { "governingStructureRule=0,dc=com", "governingStructureRule=-1,dc=com", 1 },
+            { "cn=aaa,dc=com", "cn=aaaa,dc=com", -1 }, { "cn=AAA,dc=com", "cn=aaaa,dc=com", -1 },
+            { "cn=aaa,dc=com", "cn=AAAA,dc=com", -1 }, { "cn=aaaa,dc=com", "cn=aaa,dc=com", 1 },
+            { "cn=AAAA,dc=com", "cn=aaa,dc=com", 1 }, { "cn=aaaa,dc=com", "cn=AAA,dc=com", 1 },
+            { "cn=aaab,dc=com", "cn=aaaa,dc=com", 1 }, { "cn=aaaa,dc=com", "cn=aaab,dc=com", -1 },
+            { "dc=aaa,dc=aaa", "dc=bbb", -1 }, { "dc=bbb,dc=aaa", "dc=bbb", -1 },
+            { "dc=ccc,dc=aaa", "dc=bbb", -1 }, { "dc=aaa,dc=bbb", "dc=bbb", 1 },
+            { "dc=bbb,dc=bbb", "dc=bbb", 1 }, { "dc=ccc,dc=bbb", "dc=bbb", 1 },
+            { "dc=aaa,dc=ccc", "dc=bbb", 1 }, { "dc=bbb,dc=ccc", "dc=bbb", 1 },
+            { "dc=ccc,dc=ccc", "dc=bbb", 1 }, { "", "dc=bbb", -1 }, { "dc=bbb", "", 1 }
+        };
+        // @formatter:on
+    }
+
+    /**
+     * DN equality test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "createDNEqualityData")
+    public Object[][] createDNEqualityData() {
+        // @formatter:off
+        return new Object[][] {
+            { "cn=hello world,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=hello world,dc=com", "CN=hello world,dc=com", 0 },
+            { "cn=hello   world,dc=com", "cn=hello world,dc=com", 0 },
+            { "  cn =  hello world  ,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=hello world\\ ,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=HELLO WORLD,dc=com", "cn=hello world,dc=com", 0 },
+            { "cn=HELLO+sn=WORLD,dc=com", "sn=world+cn=hello,dc=com", 0 },
+            { "governingStructureRule=10,dc=com", "governingStructureRule=9,dc=com", 1 },
+            { "governingStructureRule=999,dc=com", "governingStructureRule=1000,dc=com", -1 },
+            { "governingStructureRule=-1,dc=com", "governingStructureRule=0,dc=com", -1 },
+            { "governingStructureRule=0,dc=com", "governingStructureRule=-1,dc=com", 1 },
+            { "cn=aaa,dc=com", "cn=aaaa,dc=com", -1 },
+            { "cn=AAA,dc=com", "cn=aaaa,dc=com", -1 },
+            { "cn=aaa,dc=com", "cn=AAAA,dc=com", -1 },
+            { "cn=aaaa,dc=com", "cn=aaa,dc=com", 1 },
+            { "cn=AAAA,dc=com", "cn=aaa,dc=com", 1 },
+            { "cn=aaaa,dc=com", "cn=AAA,dc=com", 1 },
+            { "cn=aaab,dc=com", "cn=aaaa,dc=com", 1 },
+            { "cn=aaaa,dc=com", "cn=aaab,dc=com", -1 },
+            { "dc=aaa,dc=aaa", "dc=bbb", -1 },
+            { "dc=bbb,dc=aaa", "dc=bbb", -1 },
+            { "dc=ccc,dc=aaa", "dc=bbb", -1 },
+            { "dc=aaa,dc=bbb", "dc=bbb", 1 },
+            { "dc=bbb,dc=bbb", "dc=bbb", 1 },
+            { "dc=ccc,dc=bbb", "dc=bbb", 1 },
+            { "dc=aaa,dc=ccc", "dc=bbb", 1 },
+            { "dc=bbb,dc=ccc", "dc=bbb", 1 },
+            { "dc=ccc,dc=ccc", "dc=bbb", 1 },
+            { "", "dc=bbb", -1 },
+            { "dc=bbb", "", 1 }
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Illegal DN test data provider.
+     *
+     * @return The array of illegal test DN strings.
+     */
+    @DataProvider(name = "illegalDNs")
+    public Object[][] createIllegalData() {
+        // @formatter:off
+        return new Object[][] {
+            { "manager" },
+            { "manager " },
+            { "=Jim" },
+            { " =Jim" },
+            { "= Jim" },
+            { " = Jim" },
+            { "cn+Jim" },
+            { "cn + Jim" },
+            { "cn=Jim+" },
+            { "cn=Jim+manager" },
+            { "cn=Jim+manager " },
+            { "cn=Jim+manager," },
+            { "cn=Jim," },
+            { "cn=Jim,  " },
+            { "c[n]=Jim" },
+            { "_cn=Jim" },
+            { "c_n=Jim" },
+            { "cn\"=Jim" },
+            { "c\"n=Jim" },
+            { "1cn=Jim" },
+            { "cn+uid=Jim" },
+            { "-cn=Jim" },
+            { "/tmp=a" },
+            { "\\tmp=a" },
+            { "cn;lang-en=Jim" },
+            { "@cn=Jim" },
+            { "_name_=Jim" },
+            { "\u03c0=pi" },
+            { "v1.0=buggy" },
+            { "1.=buggy" },
+            { ".1=buggy" },
+            { "oid.1." },
+            { "1.3.6.1.4.1.1466..0=#04024869" },
+            { "cn=#a" },
+            { "cn=#ag" },
+            { "cn=#ga" },
+            { "cn=#abcdefgh" },
+            { "cn=a\\b" },
+            { "cn=a\\bg" },
+            { "cn=\"hello" },
+            { "cn=+mail=,dc=example,dc=com" },
+            { "cn=xyz+sn=,dc=example,dc=com" },
+            { "cn=,dc=example,dc=com" },
+            { "cn=a+cn=b,dc=example,dc=com" }
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Is Child of test data provider.
+     *
+     * @return The array of test data.
+     */
+    @DataProvider(name = "createIsChildOfTestData")
+    public Object[][] createIsChildOfTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", false },
+            { "", "dc=org", false },
+            { "", "dc=opendj,dc=org", false },
+            { "", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=org", "", true },
+            { "dc=org", "dc=org", false },
+            { "dc=org", "dc=opendj,dc=org", false },
+            { "dc=org", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=opendj,dc=org", "", false },
+            { "dc=opendj,dc=org", "dc=org", true },
+            { "dc=opendj,dc=org", "dc=opendj,dc=org", false },
+            { "dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=foo,dc=opendj,dc=org", "", false },
+            { "dc=foo,dc=opendj,dc=org", "dc=org", false },
+            { "dc=foo,dc=opendj,dc=org", "dc=opendj,dc=org", true },
+            { "dc=foo,dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=org", "dc=com", false },
+            { "dc=opendj,dc=org", "dc=foo,dc=org", false },
+            { "dc=opendj,dc=org", "dc=opendj,dc=com", false },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "createNumComponentsTestData")
+    public Object[][] createNumComponentsTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", 0 },
+            { "dc=com", 1 },
+            { "dc=opendj,dc=com", 2 },
+            { "dc=world,dc=opendj,dc=com", 3 },
+            { "dc=hello,dc=world,dc=opendj,dc=com", 4 },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "createParentAndRDNTestData")
+    public Object[][] createParentAndRDNTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", null, null },
+            { "dc=com", "", "dc=com" },
+            { "dc=opendj,dc=com", "dc=com", "dc=opendj" },
+            { "dc=world,dc=opendj,dc=com", "dc=opendj,dc=com", "dc=world" },
+            { "dc=hello,dc=world,dc=opendj,dc=com", "dc=world,dc=opendj,dc=com", "dc=hello" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "createRDNTestData")
+    public Object[][] createRDNTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "dc=com", 0, "dc=com" },
+            { "dc=opendj,dc=com", 0, "dc=opendj" },
+            { "dc=opendj,dc=com", 1, "dc=com" },
+            { "dc=hello,dc=world,dc=opendj,dc=com", 0, "dc=hello" },
+            { "dc=hello,dc=world,dc=opendj,dc=com", 1, "dc=world" },
+            { "dc=hello,dc=world,dc=opendj,dc=com", 2, "dc=opendj" },
+            { "dc=hello,dc=world,dc=opendj,dc=com", 3, "dc=com" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Subordinate test data provider.
+     *
+     * @return The array of subordinate and superior DN Strings.
+     */
+    @DataProvider(name = "createSubordinateTestData")
+    public Object[][] createSubordinateTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", true },
+            { "", "dc=org", false },
+            { "", "dc=opendj,dc=org", false },
+            { "", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=org", "", true },
+            { "dc=org", "dc=org", true },
+            { "dc=org", "dc=opendj,dc=org", false },
+            { "dc=org", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=opendj,dc=org", "", true },
+            { "dc=opendj,dc=org", "dc=org", true },
+            { "dc=opendj,dc=org", "dc=opendj,dc=org", true },
+            { "dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", false },
+            { "dc=foo,dc=opendj,dc=org", "", true },
+            { "dc=foo,dc=opendj,dc=org", "dc=org", true },
+            { "dc=foo,dc=opendj,dc=org", "dc=opendj,dc=org", true },
+            { "dc=foo,dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", true },
+            { "dc=org", "dc=com", false },
+            { "dc=opendj,dc=org", "dc=foo,dc=org", false },
+            { "dc=opendj,dc=org", "dc=opendj,dc=com", false },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Superior test data provider.
+     *
+     * @return The array of superior and subordinate DN Strings.
+     */
+    @DataProvider(name = "createSuperiorTestData")
+    public Object[][] createSuperiorTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", true },
+            { "", "dc=org", true },
+            { "", "dc=opendj,dc=org", true },
+            { "", "dc=foo,dc=opendj,dc=org", true },
+            { "dc=org", "", false },
+            { "dc=org", "dc=org", true },
+            { "dc=org", "dc=opendj,dc=org", true },
+            { "dc=org", "dc=foo,dc=opendj,dc=org", true },
+            { "dc=opendj,dc=org", "", false },
+            { "dc=opendj,dc=org", "dc=org", false },
+            { "dc=opendj,dc=org", "dc=opendj,dc=org", true },
+            { "dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", true },
+            { "dc=foo,dc=opendj,dc=org", "", false },
+            { "dc=foo,dc=opendj,dc=org", "dc=org", false },
+            { "dc=foo,dc=opendj,dc=org", "dc=opendj,dc=org", false },
+            { "dc=foo,dc=opendj,dc=org", "dc=foo,dc=opendj,dc=org", true },
+            { "dc=org", "dc=com", false },
+            { "dc=opendj,dc=org", "dc=foo,dc=org", false },
+            { "dc=opendj,dc=org", "dc=opendj,dc=com", false },
+        };
+        // @formatter:on
+    }
+
+    @Test
+    public void testAdminData() {
+        DN.valueOf("cn=cn\\=admin data");
+        final DN theDN = DN.valueOf("cn=my dn");
+        final RDN theRDN = new RDN("cn", "my rdn");
+        final DN theChildDN = theDN.child(theRDN);
+        DN.valueOf(theChildDN.toString());
+    }
+
+    /**
+     * Test the child(DN) method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param l
+     *            The local name to be appended.
+     * @param e
+     *            The expected DN.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createChildDNTestData")
+    public void testChildDN(final String s, final String l, final String e) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final DN localName = DN.valueOf(l);
+        final DN expected = DN.valueOf(e);
+
+        assertEquals(dn.child(localName), expected);
+    }
+
+    /**
+     * Test the child(DN) method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = { NullPointerException.class, AssertionError.class })
+    public void testChildDNException() throws Exception {
+        final DN dn = DN.valueOf("dc=org");
+        dn.child((DN) null);
+    }
+
+    /**
+     * Test the child(DN) method's interaction with other methods.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testChildDNInteraction() throws Exception {
+        final DN p = DN.valueOf("dc=opendj,dc=org");
+        final DN l = DN.valueOf("dc=foo");
+        final DN e = DN.valueOf("dc=foo,dc=opendj,dc=org");
+        final DN c = p.child(l);
+
+        assertEquals(c.size(), 3);
+
+        assertEquals(signum(c.compareTo(p)), 1);
+        assertEquals(signum(p.compareTo(c)), -1);
+
+        assertTrue(p.isParentOf(c));
+        assertFalse(c.isParentOf(p));
+
+        assertTrue(c.isChildOf(p));
+        assertFalse(p.isChildOf(c));
+
+        assertEquals(c, e);
+        assertEquals(c.hashCode(), e.hashCode());
+
+        assertEquals(c.toString(), e.toString());
+
+        assertEquals(c.rdn(), RDN.valueOf("dc=foo"));
+
+        assertEquals(c.parent(), DN.valueOf("dc=opendj,dc=org"));
+        assertEquals(c.parent(), e.parent());
+
+        assertEquals(c.child(RDN.valueOf("dc=xxx")), DN.valueOf("dc=xxx,dc=foo,dc=opendj,dc=org"));
+        assertEquals(c.child(DN.valueOf("dc=xxx,dc=yyy")), DN
+                .valueOf("dc=xxx,dc=yyy,dc=foo,dc=opendj,dc=org"));
+    }
+
+    /**
+     * Test the child(RDN...) method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param r
+     *            The RDN to be appended.
+     * @param e
+     *            The expected DN.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createChildRDNTestData")
+    public void testChildSingleRDN(final String s, final String r, final String e) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final RDN rdn = RDN.valueOf(r);
+        final DN expected = DN.valueOf(e);
+
+        assertEquals(dn.child(rdn), expected);
+    }
+
+    /**
+     * Test the child(String attributeType, Object attributeValue) method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param r
+     *            The RDN to be appended.
+     * @param e
+     *            The expected DN.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createChildRDNTestData")
+    public void testChildTypeValue(final String s, final String r, final String e) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final RDN rdn = RDN.valueOf(r);
+        final DN expected = DN.valueOf(e);
+
+        assertEquals(dn.child(rdn.getFirstAVA().getAttributeType().getNameOrOID(), rdn
+                .getFirstAVA().getAttributeValue()), expected);
+    }
+
+    /**
+     * Test DN compareTo
+     *
+     * @param first
+     *            First DN to compare.
+     * @param second
+     *            Second DN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createDNComparisonData")
+    public void testCompareTo(final String first, final String second, final int result)
+            throws Exception {
+        final DN dn1 = DN.valueOf(first);
+        final DN dn2 = DN.valueOf(second);
+
+        int rc = dn1.compareTo(dn2);
+
+        // Normalize the result.
+        if (rc < 0) {
+            rc = -1;
+        } else if (rc > 0) {
+            rc = 1;
+        }
+
+        assertEquals(rc, result, "Comparison for <" + first + "> and <" + second + ">.");
+    }
+
+    /**
+     * Test DN equality
+     *
+     * @param first
+     *            First DN to compare.
+     * @param second
+     *            Second DN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createDNEqualityData")
+    public void testEquality(final String first, final String second, final int result)
+            throws Exception {
+        final DN dn1 = DN.valueOf(first);
+        final DN dn2 = DN.valueOf(second);
+
+        if (result == 0) {
+            assertTrue(dn1.equals(dn2), "DN equality for <" + first + "> and <" + second + ">");
+        } else {
+            assertFalse(dn1.equals(dn2), "DN equality for <" + first + "> and <" + second + ">");
+        }
+    }
+
+    /**
+     * Tests the equals method with a value that's not a DN.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testEqualsNonDN() throws Exception {
+        final DN dn = DN.valueOf("dc=example,dc=com");
+
+        assertFalse(dn.equals(DN.valueOf("not a DN")));
+    }
+
+    /**
+     * Test DN hashCode
+     *
+     * @param first
+     *            First DN to compare.
+     * @param second
+     *            Second DN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createDNEqualityData")
+    public void testHashCode(final String first, final String second, final int result)
+            throws Exception {
+        final DN dn1 = DN.valueOf(first);
+        final DN dn2 = DN.valueOf(second);
+
+        final int h1 = dn1.hashCode();
+        final int h2 = dn2.hashCode();
+
+        if (result == 0) {
+            assertThat(h1)
+                    .as("Hash codes for <" + first + "> and <" + second + "> should be the same.")
+                    .isEqualTo(h2);
+        } else {
+            assertThat(h1)
+                    .as("Hash codes for <" + first + "> and <" + second + "> should NOT be the same.")
+                    .isNotEqualTo(h2);
+        }
+    }
+
+    /**
+     * Test that decoding an illegal DN as a String throws an exception.
+     *
+     * @param dn
+     *            The illegal DN to be tested.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "illegalDNs", expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testIllegalStringDNs(final String dn) throws Exception {
+        DN.valueOf(dn);
+    }
+
+    @Test(dataProvider = "illegalDNs", expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testIllegalByteStringDNs(final String dn) throws Exception {
+        DN.valueOf(ByteString.valueOfUtf8(dn));
+    }
+
+    /**
+     * Test the isChildOf method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param d
+     *            The dn parameter.
+     * @param e
+     *            The expected result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createIsChildOfTestData")
+    public void testIsChildOf(final String s, final String d, final boolean e) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final DN other = DN.valueOf(d);
+
+        assertEquals(dn.isChildOf(other), e, s + " isChildOf " + d);
+    }
+
+    /**
+     * Test the isChildOf method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = { NullPointerException.class, AssertionError.class })
+    public void testIsChildOfException() throws Exception {
+        final DN dn = DN.valueOf("dc=com");
+        dn.isChildOf((String) null);
+    }
+
+    /** Tests the parent and rdn method that require iteration. */
+    @Test
+    public void testIterableParentAndRdn() {
+        final String str = "ou=people,dc=example,dc=com";
+        final DN dn = DN.valueOf(str);
+        // Parent at index 0 is self.
+        assertEquals(dn.parent(0), dn);
+        assertEquals(dn.parent(1), DN.valueOf("dc=example,dc=com"));
+        assertEquals(dn.parent(2), DN.valueOf("dc=com"));
+        assertEquals(dn.parent(3), DN.rootDN());
+        assertEquals(dn.parent(4), null);
+
+        assertEquals(dn.rdn(0), RDN.valueOf("ou=people"));
+        assertEquals(dn.rdn(1), RDN.valueOf("dc=example"));
+        assertEquals(dn.rdn(2), RDN.valueOf("dc=com"));
+        assertEquals(dn.rdn(3), null);
+    }
+
+    /**
+     * Test the getNumComponents method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param sz
+     *            The expected number of RDNs.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createNumComponentsTestData")
+    public void testNumComponents(final String s, final int sz) throws Exception {
+        final DN dn = DN.valueOf(s);
+        assertEquals(dn.size(), sz);
+    }
+
+    /**
+     * Test the parent method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param p
+     *            The expected parent.
+     * @param r
+     *            The expected rdn.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createParentAndRDNTestData")
+    public void testParent(final String s, final String p, final String r) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final DN parent = (p != null ? DN.valueOf(p) : null);
+
+        assertEquals(dn.parent(), parent, "For DN " + s);
+    }
+
+    /**
+     * Test the parent method's interaction with other methods.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testParentInteraction() throws Exception {
+        final DN c = DN.valueOf("dc=foo,dc=bar,dc=opendj,dc=org");
+        final DN e = DN.valueOf("dc=bar,dc=opendj,dc=org");
+        final DN p = c.parent();
+
+        assertEquals(p.size(), 3);
+
+        assertEquals(signum(p.compareTo(c)), -1);
+        assertEquals(signum(c.compareTo(p)), 1);
+
+        assertTrue(p.isParentOf(c));
+        assertFalse(c.isParentOf(p));
+
+        assertTrue(c.isChildOf(p));
+        assertFalse(p.isChildOf(c));
+
+        assertEquals(p, e);
+        assertEquals(p.hashCode(), e.hashCode());
+
+        assertEquals(p.toString(), e.toString());
+
+        assertEquals(p.rdn(), RDN.valueOf("dc=bar"));
+
+        assertEquals(p.parent(), DN.valueOf("dc=opendj,dc=org"));
+        assertEquals(p.parent(), e.parent());
+
+        assertEquals(p.child(RDN.valueOf("dc=foo")), DN.valueOf("dc=foo,dc=bar,dc=opendj,dc=org"));
+        assertEquals(p.child(RDN.valueOf("dc=foo")), c);
+        assertEquals(p.child(DN.valueOf("dc=xxx,dc=foo")), DN
+                .valueOf("dc=xxx,dc=foo,dc=bar,dc=opendj,dc=org"));
+    }
+
+    /**
+     * Test the getRDN method.
+     *
+     * @param s
+     *            The test DN string.
+     * @param p
+     *            The expected parent.
+     * @param r
+     *            The expected rdn.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createParentAndRDNTestData")
+    public void testRDN(final String s, final String p, final String r) throws Exception {
+        final DN dn = DN.valueOf(s);
+        final RDN rdn = (r != null ? RDN.valueOf(r) : null);
+
+        assertEquals(dn.rdn(), rdn, "For DN " + s);
+    }
+
+    /**
+     * Tests the root DN.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testRootDN1() throws Exception {
+        final DN dn = DN.valueOf("");
+        assertTrue(dn.isRootDN());
+        assertEquals(dn, DN.rootDN());
+    }
+
+    /**
+     * Tests {@link DN#valueOf(String)}.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void valueOfStringShouldThrowNPEForNullParameter() {
+        DN.valueOf((String) null);
+    }
+
+    /**
+     * Tests {@link DN#valueOf(ByteString)}.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void valueOfByteStringShouldThrowNPEForNullParameter() throws Exception {
+        DN.valueOf((ByteString) null);
+    }
+
+    /**
+     * Test the root dn.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testRootDN3() throws Exception {
+        final DN nullDN = DN.rootDN();
+        assertTrue(nullDN.isRootDN());
+        assertTrue(nullDN.size() == 0);
+    }
+
+    /**
+     * Test the root dn.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testRootDN4() throws Exception {
+        final DN dn = DN.valueOf("dc=com");
+        assertFalse(dn.isRootDN());
+    }
+
+    /**
+     * Tests the subordinate dns.
+     */
+    @Test(dataProvider = "createSubordinateTestData")
+    public void testSubordinateDN(final String sub, final String base, final boolean e)
+            throws Exception {
+        final DN dn = DN.valueOf(sub);
+        final DN other = DN.valueOf(base);
+        assertEquals(dn.isSubordinateOrEqualTo(other), e, sub + " isSubordinateOf " + base);
+    }
+
+    /** Tests the superior dns. */
+    @Test(dataProvider = "createSuperiorTestData")
+    public void testSuperiorDN(final String base, final String sub, final boolean e)
+            throws Exception {
+        final DN dn = DN.valueOf(base);
+        final DN other = DN.valueOf(sub);
+        assertEquals(dn.isSuperiorOrEqualTo(other), e, base + " isSuperiorOf " + sub);
+    }
+
+    /**
+     * Test the RFC 4514 string representation of the DN.
+     *
+     * @param rawDN
+     *            Raw DN string representation.
+     * @param normDN
+     *            Normalized DN string representation.
+     * @param stringDN
+     *            String representation.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "testDNs")
+    public void testToString(final String rawDN, final String normDN, final String stringDN)
+            throws Exception {
+        DN dn = DN.valueOf(rawDN);
+        assertThat(dn.toString()).isEqualTo(stringDN);
+    }
+
+    /**
+     * Tests the <CODE>valueOf</CODE> method which takes a String argument.
+     *
+     * @param rawDN
+     *            Raw DN string representation.
+     * @param normDN
+     *            Normalized DN string representation.
+     * @param stringDN
+     *            String representation.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "testDNs")
+    public void testValueOfString(final String rawDN, final String normDN, final String stringDN)
+            throws Exception {
+        final DN raw = DN.valueOf(rawDN);
+        final DN string = DN.valueOf(stringDN);
+        assertEquals(raw, string);
+    }
+
+    /**
+     * Test data for testInScopeOf tests.
+     *
+     * @return Test data.
+     */
+    @DataProvider
+    public Object[][] createIsInScopeOfTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "dc=x,dc=y", "dc=x,dc=y", SearchScope.BASE_OBJECT, true  },
+            { "dc=x,dc=y", "dc=z,dc=y", SearchScope.BASE_OBJECT, false },
+            { "dc=x,dc=z", "dc=x,dc=y", SearchScope.BASE_OBJECT, false },
+            { "dc=x,dc=y", "dc=y",      SearchScope.BASE_OBJECT, false },
+            { "dc=y",      "dc=x,dc=y", SearchScope.BASE_OBJECT, false },
+
+            { "dc=x,dc=y",      "dc=x,dc=y", SearchScope.SINGLE_LEVEL, false },
+            { "dc=x,dc=y",      "dc=y",      SearchScope.SINGLE_LEVEL, true  },
+            { "dc=z,dc=x,dc=y", "dc=y",      SearchScope.SINGLE_LEVEL, false },
+            { "dc=y",           "dc=x,dc=y", SearchScope.SINGLE_LEVEL, false },
+            { "dc=x,dc=z",      "dc=y",      SearchScope.SINGLE_LEVEL, false },
+
+            { "dc=x,dc=y",      "dc=x,dc=y", SearchScope.SUBORDINATES, false },
+            { "dc=x,dc=y",      "dc=y",      SearchScope.SUBORDINATES, true  },
+            { "dc=z,dc=x,dc=y", "dc=y",      SearchScope.SUBORDINATES, true  },
+            { "dc=y",           "dc=x,dc=y", SearchScope.SUBORDINATES, false },
+            { "dc=x,dc=z",      "dc=y",      SearchScope.SUBORDINATES, false },
+
+            { "dc=x,dc=y",      "dc=x,dc=y", SearchScope.WHOLE_SUBTREE, true },
+            { "dc=x,dc=y",      "dc=y",      SearchScope.WHOLE_SUBTREE, true  },
+            { "dc=z,dc=x,dc=y", "dc=y",      SearchScope.WHOLE_SUBTREE, true  },
+            { "dc=y",           "dc=x,dc=y", SearchScope.WHOLE_SUBTREE, false },
+            { "dc=x,dc=z",      "dc=y",      SearchScope.WHOLE_SUBTREE, false },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests {@link DN#isInScopeOf(String, SearchScope)}.
+     *
+     * @param dn
+     *            The target DN.
+     * @param baseDN
+     *            The scope base DN.
+     * @param scope
+     *            The search scope.
+     * @param expectedResult
+     *            The expected result.
+     */
+    @Test(dataProvider = "createIsInScopeOfTestData")
+    public void testIsInScopeOfString(final String dn, final String baseDN,
+            final SearchScope scope, final boolean expectedResult) {
+        assertEquals(DN.valueOf(dn).isInScopeOf(baseDN, scope), expectedResult);
+    }
+
+    /**
+     * Tests {@link DN#isInScopeOf(DN, SearchScope)}.
+     *
+     * @param dn
+     *            The target DN.
+     * @param baseDN
+     *            The scope base DN.
+     * @param scope
+     *            The search scope.
+     * @param expectedResult
+     *            The expected result.
+     */
+    @Test(dataProvider = "createIsInScopeOfTestData")
+    public void testIsInScopeOfDN(final String dn, final String baseDN, final SearchScope scope,
+            final boolean expectedResult) {
+        assertEquals(DN.valueOf(dn).isInScopeOf(DN.valueOf(baseDN), scope), expectedResult);
+    }
+
+    /**
+     * Test data for testLocalName tests.
+     *
+     * @return Test data.
+     */
+    @DataProvider
+    public Object[][] createLocalNameTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", 0, "" },
+            { "", 1, "" },
+
+            { "dc=x", 0, "" },
+            { "dc=x", 1, "dc=x" },
+            { "dc=x", 2, "dc=x" },
+
+            { "dc=x,dc=y", 0, "" },
+            { "dc=x,dc=y", 1, "dc=x" },
+            { "dc=x,dc=y", 2, "dc=x,dc=y" },
+            { "dc=x,dc=y", 3, "dc=x,dc=y" },
+
+            { "dc=x,dc=y,dc=z", 0, "" },
+            { "dc=x,dc=y,dc=z", 1, "dc=x" },
+            { "dc=x,dc=y,dc=z", 2, "dc=x,dc=y" },
+            { "dc=x,dc=y,dc=z", 3, "dc=x,dc=y,dc=z" },
+            { "dc=x,dc=y,dc=z", 4, "dc=x,dc=y,dc=z" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests {@link DN#localName(int)}.
+     *
+     * @param dn
+     *            The DN whose local name is to be obtained.
+     * @param index
+     *            The number of RDNs in the local name.
+     * @param expectedDN
+     *            The expected local name.
+     */
+    @Test(dataProvider = "createLocalNameTestData")
+    public void testLocalName(final String dn, final int index, final String expectedDN) {
+        assertEquals(DN.valueOf(dn).localName(index), DN.valueOf(expectedDN));
+    }
+
+    /**
+     * Test data for testLocalName tests.
+     *
+     * @return Test data.
+     */
+    @DataProvider
+    public Object[][] createRenameTestData() {
+        // @formatter:off
+        return new Object[][] {
+            { "", "", "", "" },
+            { "", "", "dc=x", "dc=x" },
+            { "dc=x", "", "dc=y", "dc=x,dc=y" },
+            { "dc=x", "dc=x", "dc=y", "dc=y" },
+            { "dc=x,dc=y", "dc=y", "dc=z", "dc=x,dc=z" },
+            { "dc=x,dc=y", "dc=x,dc=y", "dc=z", "dc=z" },
+            { "dc=x,dc=y", "dc=x", "dc=z", "dc=x,dc=y" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests {@link DN#rename(DN, DN)}.
+     *
+     * @param dn
+     *            The DN to be renamed.
+     * @param fromDN
+     *            The source DN.
+     * @param toDN
+     *            The destination DN.
+     * @param expectedDN
+     *            The expected result.
+     */
+    @Test(dataProvider = "createRenameTestData")
+    public void testRename(final String dn, final String fromDN, final String toDN,
+            final String expectedDN) {
+        DN actual = DN.valueOf(dn).rename(DN.valueOf(fromDN), DN.valueOf(toDN));
+        assertEquals(actual, DN.valueOf(expectedDN));
+    }
+
+    /**
+     * Tests the {@link DN#format(String, Object...)} method.
+     */
+    @Test
+    public void testFormatNoEscape() {
+        DN actual = DN.format("deviceId=%s,uid=%s,dc=test", 123, "bjensen");
+        DN expected = DN.valueOf("dc=test").child("uid", "bjensen").child("deviceId", 123);
+        assertEquals(actual, expected);
+        assertEquals(actual.toString(), "deviceId=123,uid=bjensen,dc=test");
+    }
+
+    /** Tests the {@link DN#format(String, Object...)} method. */
+    @Test
+    public void testFormatEscape() {
+        DN actual = DN.format("uid=%s,dc=test", "#cn=foo+sn=bar");
+        DN expected = DN.valueOf("dc=test").child("uid", "#cn=foo+sn=bar");
+        assertEquals(actual, expected);
+        assertEquals(actual.toString(), "uid=\\#cn=foo\\+sn=bar,dc=test");
+    }
+
+    /** Tests the {@link DN#escapeAttributeValue(Object)} method. */
+    @Test
+    public void testEscapeAttributeValue() {
+        String actual = DN.escapeAttributeValue("#cn=foo+sn=bar");
+        assertEquals(actual, "\\#cn=foo\\+sn=bar");
+    }
+
+    /** Tests the {@link DN#toNormalizedByteString()} method. */
+    @Test
+    public void testToNormalizedByteStringWithRootDN() {
+        ByteString actual = DN.rootDN().toNormalizedByteString();
+        assertEquals(actual, ByteString.empty());
+    }
+
+    /** Tests the {@link DN#iterator()} method. */
+    @Test
+    public void testIterator() {
+        final String childRdn = "dc=example";
+        final String parentRdn = "dc=com";
+        final Iterator<RDN> it = DN.valueOf(childRdn + "," + parentRdn).iterator();
+        assertTrue(it.hasNext());
+        assertEquals(it.next(), RDN.valueOf(childRdn));
+        assertTrue(it.hasNext());
+        assertEquals(it.next(), RDN.valueOf(parentRdn));
+        assertFalse(it.hasNext());
+        try {
+            it.next();
+            fail("Expected NoSuchElementException to be thrown");
+        } catch (NoSuchElementException expected) {
+            // do nothing
+        }
+        try {
+            it.remove();
+            fail("Expected UnsupportedOperationException to be thrown");
+        } catch (UnsupportedOperationException expected) {
+            // do nothing
+        }
+    }
+
+    @DataProvider
+    public Object[][] toNormalizedByteStringDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            // first value to normalize, second value to normalize, expected sign of comparison between the two
+            { "dc=com", "dc=com", 0 },
+            { "dc=example,dc=com", "dc=example,dc=com", 0 },
+            { "cn=test+dc=example,dc=com", "cn=test+dc=example,dc=com", 0 },
+            { "dc=example+cn=test,dc=com", "cn=test+dc=example,dc=com", 0 },
+            // siblings
+            { "cn=test,dc=com", "cn=test+dc=example,dc=com", -1 },
+            { "cn=test+dc=example,dc=com", "cn=test,dc=com", 1 },
+            { "dc=example,dc=com", "cn=test+dc=example,dc=com", 1 },
+            { "cn=test+dc=example,dc=com", "dc=example,dc=com", -1 },
+            { "dc=example,dc=com", "dc=example+cn=test,dc=com", 1 },
+            { "dc=example+cn=test,dc=com", "dc=example,dc=com", -1 },
+            // parent entry is followed by its children, not its siblings
+            { "dc=com", "dc=example,dc=com", -1 },
+            { "dc=com", "dc=test,dc=example,dc=com", -1},
+            { "dc=example,dc=com", "dc=test,dc=example,dc=com", -1},
+            { "dc=example,dc=com", "dc=example2,dc=com", -1},
+            { "dc=example2,dc=com", "dc=test,dc=example,dc=com", 1},
+            // with space
+            { "dc=example,dc=com", "dc = example, dc = com", 0 },
+            { "dc=example\\20test,dc=com", "dc=example test,dc=com", 0 },
+            { "dc=example test,dc=com", "dc=exampletest,dc=com", -1 },
+            // with various escaped characters
+            { "dc=example\\2Dtest,dc=com", "dc=example-test,dc=com", 0 },
+            { "dc=example\\28test,dc=com", "dc=example(test,dc=com", 0 },
+            { "dc=example\\3Ftest,dc=com", "dc=example?test,dc=com", 0 },
+            // with escaped comma
+            { "dc=example\\,dc=com,dc=com", "dc=example\\2Cdc=com,dc=com", 0 },
+            { "dc=example\\2Cdc=com,dc=com", "dc=example\\2Cdc\\3Dcom,dc=com", 0 },
+            { "dc=example,dc=com", "dc=example\\,dc=com,dc=com", -1 },
+            { "dc=example2,dc=com", "dc=example\\,dc=com,dc=com", 1 },
+            // with escaped "="
+            { "dc=example\\=other,dc=com", "dc=example\\3Dother,dc=com", 0 },
+            // with escaped "+"
+            { "dc=example\\+other,dc=com", "dc=example\\2Bother,dc=com", 0 },
+            // integer
+            { "governingStructureRule=10,dc=com", "governingStructureRule=10, dc=com", 0 },
+            { "governingStructureRule=99,dc=com", "governingStructureRule=100, dc=com", -1 },
+            { "governingStructureRule=999999,dc=com", "governingStructureRule=1000000, dc=com", -1 },
+            // no matching rule for the attribute
+            { "dummy=9,dc=com", "dummy=10,dc=com", 1 }
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toNormalizedByteStringDataProvider")
+    public void testToNormalizedByteString(String first, String second, int expectedCompareResult) {
+        DN actual = DN.valueOf(first);
+        DN expected = DN.valueOf(second);
+        int cmp = actual.toNormalizedByteString().compareTo(expected.toNormalizedByteString());
+        assertThat(signum(cmp)).isEqualTo(expectedCompareResult);
+    }
+
+    /** Additional tests with testDNs data provider. */
+    @Test(dataProvider = "testDNs")
+    public void testToNormalizedByteString2(String one, String two, String three) {
+        DN dn1 = DN.valueOf(one);
+        DN dn2 = DN.valueOf(two);
+        DN dn3 = DN.valueOf(three);
+        int cmp = dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString());
+        assertThat(cmp).isEqualTo(0);
+        int cmp2 = dn1.toNormalizedByteString().compareTo(dn3.toNormalizedByteString());
+        assertThat(cmp2).isEqualTo(0);
+    }
+
+    @DataProvider
+    private Object[][] minAndMaxRdnsDataProvider() {
+        DN dcCom          = DN.valueOf("dc=com");
+        DN dcExampleDcCom = DN.valueOf("dc=example,dc=com");
+        DN cnTestDcCom    = DN.valueOf("cn=test,dc=com");
+        return new Object[][] {
+            { dcCom,          dcCom.child(RDN.minValue()),          -1 },
+            { dcCom,          dcCom.child(RDN.maxValue()),          -1 },
+            { dcExampleDcCom, dcExampleDcCom.child(RDN.minValue()), -1 },
+            { dcExampleDcCom, dcExampleDcCom.child(RDN.maxValue()), -1 },
+            { dcExampleDcCom, dcCom.child(RDN.minValue()),           1 },
+            { dcExampleDcCom, dcCom.child(RDN.maxValue()),          -1 },
+            // siblings
+            { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 },
+            { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.minValue()), 1 },
+            { DN.valueOf("cn=test+dc=example,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 },
+            { DN.valueOf("dc=example+cn=test,dc=com"), cnTestDcCom.child(RDN.maxValue()), 1 },
+        };
+    }
+
+    /** Using DN as a Map key depends on this behaviour. In particular MemoryBackend depends on this behaviour. */
+    @Test(dataProvider = "minAndMaxRdnsDataProvider")
+    public void testToNormalizedByteStringWithMinAndMaxRdns(DN dn1, DN dn2, int expectedCompareResult) {
+        int cmp = dn1.toNormalizedByteString().compareTo(dn2.toNormalizedByteString());
+        assertThat(signum(cmp)).isEqualTo(expectedCompareResult);
+    }
+
+    @Test
+    public void testToNormalizedByteStringWithMinAndMaxRdnsInOrderedCollection() {
+        DN dcCom = DN.valueOf("dc=com");
+        DN cnTestDcCom = DN.valueOf("cn=test,dc=com");
+        DN cnDeeperCnTestDcCom = DN.valueOf("cn=deeper,cn=test,dc=com");
+        DN cnTestAndDcExampleDcCom = DN.valueOf("cn=test+dc=example,dc=com");
+        DN dcExampleDcCom = DN.valueOf("dc=example,dc=com");
+
+        TreeMap<ByteString, DN> map = new TreeMap<>();
+        putAll(map, dcCom, cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
+
+        assertThat(subordinates(map, dcCom))
+            .containsExactly(cnTestDcCom, cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
+        assertThat(subordinates(map, cnTestDcCom))
+            .containsExactly(cnDeeperCnTestDcCom);
+
+        assertThat(after(map, cnTestDcCom))
+            .containsExactly(cnDeeperCnTestDcCom, cnTestAndDcExampleDcCom, dcExampleDcCom);
+        assertThat(after(map, cnDeeperCnTestDcCom))
+            .containsExactly(cnTestAndDcExampleDcCom, dcExampleDcCom);
+
+        assertThat(before(map, cnTestDcCom))
+            .containsExactly(dcCom);
+        assertThat(before(map, cnDeeperCnTestDcCom))
+            .containsExactly(dcCom, cnTestDcCom);
+    }
+
+    private void putAll(Map<ByteString, DN> map, DN... dns) {
+        for (DN dn : dns) {
+            map.put(dn.toNormalizedByteString(), dn);
+        }
+    }
+
+    private Collection<DN> subordinates(TreeMap<ByteString, DN> map, DN dn) {
+        return map.subMap(
+            dn.child(RDN.minValue()).toNormalizedByteString(),
+            dn.child(RDN.maxValue()).toNormalizedByteString()).values();
+    }
+
+    private Collection<DN> before(TreeMap<ByteString, DN> map, DN dn) {
+        return map.headMap(dn.toNormalizedByteString(), false).values();
+    }
+
+    private Collection<DN> after(TreeMap<ByteString, DN> map, DN dn) {
+        return map.tailMap(dn.toNormalizedByteString(), false).values();
+    }
+
+    @DataProvider
+    public Object[][] toNormalizedUrlSafeStringDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            // first value = string used to build DN, second value = expected readable string
+            { "dc=com", "dc=com" },
+            { "dc=example,dc=com", "dc=com,dc=example" },
+            { "dc = example, dc = com", "dc=com,dc=example" },
+            { "dc=example+cn=test,dc=com", "dc=com,cn=test+dc=example" },
+            { "cn=test+dc=example,dc=com", "dc=com,cn=test+dc=example" },
+            // with space
+            { "dc=example test,dc=com", "dc=com,dc=example%20test" },
+            { "dc=example\\20test,dc=com", "dc=com,dc=example%20test" },
+            // with escaped comma
+            { "dc=example\\,dc=com,dc=com", "dc=com,dc=example%2Cdc%3Dcom" },
+            { "dc=example\\2Cdc=com,dc=com", "dc=com,dc=example%2Cdc%3Dcom" },
+            // with escaped "="
+            { "dc=example\\=other,dc=com", "dc=com,dc=example%3Dother" },
+            { "dc=example\\3Dother,dc=com", "dc=com,dc=example%3Dother" },
+            // with escaped "+"
+            { "dc=example\\+other,dc=com", "dc=com,dc=example%2Bother" },
+            { "dc=example\\2Bother,dc=com", "dc=com,dc=example%2Bother" },
+            // integer
+            { "governingStructureRule=256,dc=com", "dc=com,governingstructurerule=%82%01%00" },
+            // uuid
+            { "entryUUID=597ae2f6-16a6-1027-98f4-d28b5365dc14,dc=com",
+              "dc=com,entryuuid=%59%7A%E2%F6%16%A6%10%27%98%F4%D2%8B%53%65%DC%14" },
+            // characters unescaped by URL encoding (-, _, ., ~)
+            { "dc=example\\2Dtest,dc=com", "dc=com,dc=example-test" },
+            { "dc=example\\5Ftest,dc=com", "dc=com,dc=example_test" },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toNormalizedUrlSafeStringDataProvider")
+    public void testToNormalizedUrlSafeString(String dnAsString, String expectedReadableString) {
+        DN actual = DN.valueOf(dnAsString);
+        assertEquals(actual.toNormalizedUrlSafeString(), expectedReadableString);
+    }
+
+    /** Additional tests with testDNs data provider. */
+    @Test(dataProvider = "testDNs")
+    public void testToNormalizedUrlSafeString2(String one, String two, String three) {
+        DN dn1 = DN.valueOf(one);
+        DN dn2 = DN.valueOf(two);
+        DN dn3 = DN.valueOf(three);
+        String irreversibleReadableString = dn1.toNormalizedUrlSafeString();
+        assertEquals(irreversibleReadableString, dn2.toNormalizedUrlSafeString());
+        assertEquals(irreversibleReadableString, dn3.toNormalizedUrlSafeString());
+    }
+
+    @Test
+    public void toUUID() {
+        UUID uuid1 = DN.valueOf("dc=example+cn=test,dc=com").toUUID();
+        UUID uuid2 = DN.valueOf("cn=test+dc=example,dc=com").toUUID();
+        assertEquals(uuid1, uuid2);
+    }
+
+    @DataProvider
+    public Object[][] toStringShouldStripOutIllegalWhitespaceDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            { " ", "" },
+            { " dc = hello  world ", "dc=hello  world" },
+            { " dc =\\  hello  world\\  ", "dc=\\  hello  world\\ " },
+            { " dc = example , dc = com ", "dc=example,dc=com" },
+            { " uid = crystal + dc = example , uid = palace + dc = com ", "uid=crystal+dc=example,uid=palace+dc=com" },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toStringShouldStripOutIllegalWhitespaceDataProvider")
+    public void toStringShouldStripOutIllegalWhitespace(String withWhiteSpace, String withoutWhiteSpace) {
+        assertThat(DN.valueOf(withWhiteSpace).toString()).isEqualTo(withoutWhiteSpace);
+        assertThat(DN.valueOf(withWhiteSpace).toNormalizedByteString())
+                .isEqualTo(DN.valueOf(withoutWhiteSpace).toNormalizedByteString());
+    }
+
+    @DataProvider
+    public Object[][] rdnShouldReturnNullWhenIndexIsOutOfRangeData() {
+        return new Object[][] {
+            { "", 0 },
+            { "", 1 },
+            { "dc=com", 1 },
+            { "dc=opends,dc=com", 2 },
+            { "dc=hello,dc=world,dc=opends,dc=com", 4 },
+        };
+    }
+
+    @Test(dataProvider = "rdnShouldReturnNullWhenIndexIsOutOfRangeData")
+    public void rdnShouldReturnNullWhenIndexIsOutOfRange(String rdn, int i) {
+        assertThat((Object) DN.valueOf(rdn).rdn(i)).isNull();
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void rdnShouldThrowIAEForNegativeIndexes() throws Exception {
+        DN.valueOf("dc=example,dc=com").rdn(-1);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DataProviderIterator.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DataProviderIterator.java
new file mode 100644
index 0000000..6dabc39
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/DataProviderIterator.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.testng.annotations.DataProvider;
+
+/**
+ * An iterator implementation that converts an Iterator/Iterable/array to an
+ * Iterator suitable to return from a {@link DataProvider} methods.
+ */
+@SuppressWarnings("javadoc")
+public class DataProviderIterator implements Iterator<Object[]> {
+    private final Iterator<?> iter;
+
+    public DataProviderIterator(Iterable<?> iterable) {
+        this.iter = iterable != null
+                ? iterable.iterator()
+                : Collections.emptySet().iterator();
+    }
+
+    @Override
+    public boolean hasNext() {
+        return iter.hasNext();
+    }
+
+    @Override
+    public Object[] next() {
+        return new Object[] { iter.next() };
+    }
+
+    @Override
+    public void remove() {
+        iter.remove();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java
new file mode 100644
index 0000000..a78ee93
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntriesTestCase.java
@@ -0,0 +1,476 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Entries.diffEntries;
+import static org.forgerock.opendj.ldap.Entries.diffOptions;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.util.Iterator;
+
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.Test;
+
+/**
+ * Test {@code Entries}.
+ */
+@SuppressWarnings("javadoc")
+public final class EntriesTestCase extends SdkTestCase {
+
+    @Test
+    public void testContainsObjectClass() throws Exception {
+        Entry entry =
+                new LinkedHashMapEntry("dn: cn=test", "objectClass: top", "objectClass: person");
+        Schema schema = Schema.getDefaultSchema();
+
+        assertTrue("should contain top", Entries.containsObjectClass(entry, schema
+                .getObjectClass("top")));
+        assertTrue("should contain person", Entries.containsObjectClass(entry, schema
+                .getObjectClass("person")));
+        assertFalse("should not contain country", Entries.containsObjectClass(entry, schema
+                .getObjectClass("country")));
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteAddIntermediateAttribute() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "sn: ignore");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value",
+            "sn: ignore");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "add: description",
+            "description: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteAddTrailingAttributes() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore",
+            "description: value",
+            "sn: value");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "add: description",
+            "description: value",
+            "-",
+            "add: sn",
+            "sn: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteDeleteIntermediateAttribute() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value",
+            "sn: ignore");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "sn: ignore");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteDeleteTrailingAttributes() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore",
+            "description: value",
+            "sn: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value",
+            "-",
+            "delete: sn",
+            "sn: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteMultiValueAddSingleValue() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "add: description",
+            "description: value2");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteMultiValueDeleteSingleValue() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value2");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteMultiValueSameSizeDifferentValues() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE2",
+            "description: VALUE3");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value1",
+            "-",
+            "add: description",
+            "description: VALUE3");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteMultiValueSameSizeDifferentValuesExact() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE2",
+            "description: VALUE3");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value1",
+            "description: value2",
+
+            "-",
+            "add: description",
+            "description: VALUE2",
+            "description: VALUE3");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().useExactMatching()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteSingleValue() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: from");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: to");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: from",
+            "-",
+            "add: description",
+            "description: to");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteSingleValueExactMatch() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "delete: description",
+            "description: value",
+            "-",
+            "add: description",
+            "description: VALUE");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().useExactMatching()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesAddDeleteSingleValueNoChange() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify");
+        // @formatter:on
+        assertEquals(diffEntries(from, to), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceAddTrailingAttributes() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore",
+            "description: value",
+            "sn: value");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: value",
+            "-",
+            "replace: sn",
+            "sn: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceDeleteTrailingAttributes() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore",
+            "description: value",
+            "sn: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: ignore");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "-",
+            "replace: sn");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceFilteredAttributes() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: from");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "cn: to",
+            "description: value",
+            "sn: value");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: cn",
+            "cn: to",
+            "-",
+            "replace: sn",
+            "sn: value");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().attributes("cn", "sn")),
+                expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceMultiValueChangeSize() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: value1",
+            "description: value2");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceMultiValueSameSize() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE2",
+            "description: VALUE3");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: VALUE2",
+            "description: VALUE3");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceMultiValueSameSizeExact() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value1",
+            "description: value2");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value2",
+            "description: value3");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: value2",
+            "description: value3");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().useExactMatching()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceSingleValue() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: from");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: to");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: to");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().replaceSingleValuedAttributes()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceSingleValueExactMatch() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify",
+            "replace: description",
+            "description: VALUE");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes().useExactMatching()), expected);
+    }
+
+    @Test
+    public void testDiffEntriesReplaceSingleValueNoChange() {
+        // @formatter:off
+        Entry from = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: value");
+        Entry to = new LinkedHashMapEntry(
+            "dn: cn=test",
+            "description: VALUE");
+        ModifyRequest expected = Requests.newModifyRequest(
+            "dn: cn=test",
+            "changetype: modify");
+        // @formatter:on
+        assertEquals(diffEntries(from, to, diffOptions().alwaysReplaceAttributes()), expected);
+    }
+
+    private void assertEquals(ModifyRequest actual, ModifyRequest expected) {
+        assertThat((Object) actual.getName()).isEqualTo(expected.getName());
+        assertThat(actual.getModifications()).hasSize(expected.getModifications().size());
+        Iterator<Modification> i1 = actual.getModifications().iterator();
+        Iterator<Modification> i2 = expected.getModifications().iterator();
+        while (i1.hasNext()) {
+            Modification m1 = i1.next();
+            Modification m2 = i2.next();
+            assertThat(m1.getModificationType()).isEqualTo(m2.getModificationType());
+            assertThat(m1.getAttribute()).isEqualTo(m2.getAttribute());
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java
new file mode 100644
index 0000000..468e060
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/EntryTestCase.java
@@ -0,0 +1,812 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
+import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test {@code Entry}.
+ */
+@SuppressWarnings("javadoc")
+public final class EntryTestCase extends SdkTestCase {
+
+    private static interface EntryFactory {
+        Entry newEntry(String... ldifLines) throws Exception;
+    }
+
+    private static final class LinkedHashMapEntryFactory implements EntryFactory {
+        @Override
+        public Entry newEntry(final String... ldifLines) throws Exception {
+            final LDIFEntryReader reader = new LDIFEntryReader(ldifLines).setSchema(SCHEMA);
+            final Entry entry = reader.readEntry();
+            assertThat(reader.hasNext()).isFalse();
+            return new LinkedHashMapEntry(entry);
+        }
+    }
+
+    private static final class TreeMapEntryFactory implements EntryFactory {
+        @Override
+        public Entry newEntry(final String... ldifLines) throws Exception {
+            final LDIFEntryReader reader = new LDIFEntryReader(ldifLines).setSchema(SCHEMA);
+            final Entry entry = reader.readEntry();
+            assertThat(reader.hasNext()).isFalse();
+            return new TreeMapEntry(entry);
+        }
+    }
+
+    private static final AttributeDescription AD_CN;
+    private static final AttributeDescription AD_CUSTOM1;
+    private static final AttributeDescription AD_CUSTOM2;
+    private static final AttributeDescription AD_NAME;
+
+    private static final AttributeDescription AD_SN;
+
+    private static final Schema SCHEMA;
+
+    static {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addAttributeType("( 9.9.9.1 NAME 'custom1' SUP name )", false);
+        builder.addAttributeType("( 9.9.9.2 NAME 'custom2' SUP name )", false);
+        SCHEMA = builder.toSchema();
+        AD_CUSTOM1 = AttributeDescription.valueOf("custom1", SCHEMA);
+        AD_CUSTOM2 = AttributeDescription.valueOf("custom2", SCHEMA);
+        AD_CN = AttributeDescription.valueOf("cn");
+        AD_SN = AttributeDescription.valueOf("sn");
+        AD_NAME = AttributeDescription.valueOf("name");
+    }
+
+    @DataProvider(name = "EntryFactory")
+    Object[][] entryFactory() {
+        // Value, type, options, containsOptions("foo")
+        return new Object[][] { { new TreeMapEntryFactory() }, { new LinkedHashMapEntryFactory() } };
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttribute(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("sn", "sn"))).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollection(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<>();
+        assertThat(entry.addAttribute(new LinkedAttribute("sn", "sn"), duplicateValues)).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+        assertThat(duplicateValues).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollectionValueMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<>();
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "newcn"), duplicateValues))
+                .isTrue();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+        assertThat(duplicateValues).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeCollectionValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> duplicateValues = new LinkedList<>();
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "test"), duplicateValues))
+                .isFalse();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+        assertThat(duplicateValues).hasSize(1);
+        assertThat(duplicateValues).contains(ByteString.valueOfUtf8("test"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeValueMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "newcn"))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeAttributeValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute(new LinkedAttribute("cn", "test"))).isFalse();
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("sn", "sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom2", "custom2")).isSameAs(entry);
+        // This is expected to be null since the type was decoded using the
+        // default schema and a temporary oid was allocated.
+        assertThat(entry.getAttribute(AD_CUSTOM2)).isNull();
+        assertThat(entry.getAttribute("custom2")).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustomValueMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom1", "xxxx")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringCustomValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("custom1", "custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringValueMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("cn", "newcn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testAddAttributeStringValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.addAttribute("cn", "test")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testClearAttributes(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.clearAttributes()).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CUSTOM2), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomPresent1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CUSTOM1), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomPresent2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute("custom1"), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeCustomValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM2, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_SN), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributePresent1(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute(AD_CN), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributePresent2(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(emptyAttribute("cn"), missingValues)).isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueCustomMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM1, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueCustomPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(
+                entry.containsAttribute(singletonAttribute(AD_CUSTOM1, "custom1"), missingValues))
+                .isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_SN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_CN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeAttributeValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.containsAttribute(singletonAttribute(AD_CN, "test"), missingValues))
+                .isTrue();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringCustomMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom2")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringCustomPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom1")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("sn")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("custom1", "custom1")).isTrue();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn", "missing")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("sn", "missing")).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testContainsAttributeStringValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.containsAttribute("cn", "test")).isTrue();
+    }
+
+    @Test
+    public void testEqualsHashCodeDifferentContentDifferentTypes1() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        // Extra attributes.
+        final Entry e2 = createTestEntry(new LinkedHashMapEntryFactory()).addAttribute("sn", "sn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test
+    public void testEqualsHashCodeDifferentContentDifferentTypes2() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        // Same attributes, extra values.
+        final Entry e2 =
+                createTestEntry(new LinkedHashMapEntryFactory()).addAttribute("cn", "newcn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentContentSameTypes1(final EntryFactory factory)
+            throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        // Extra attributes.
+        final Entry e2 = createTestEntry(factory).addAttribute("sn", "sn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentContentSameTypes2(final EntryFactory factory)
+            throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        // Same attributes, extra values.
+        final Entry e2 = createTestEntry(factory).addAttribute("cn", "newcn");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e2).isNotEqualTo(e1);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeDifferentDN(final EntryFactory factory) throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        final Entry e2 = createTestEntry(factory).setName("cn=foobar");
+        assertThat(e1).isNotEqualTo(e2);
+        assertThat(e1.hashCode()).isNotEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeMutates(final EntryFactory factory) throws Exception {
+        final Entry e = createTestEntry(factory);
+        final int hc1 = e.hashCode();
+        e.addAttribute("sn", "sn");
+        final int hc2 = e.hashCode();
+        assertThat(hc1).isNotEqualTo(hc2);
+    }
+
+    @Test
+    public void testEqualsHashCodeSameContentDifferentTypes() throws Exception {
+        final Entry e1 = createTestEntry(new TreeMapEntryFactory());
+        final Entry e2 = createTestEntry(new LinkedHashMapEntryFactory());
+        assertThat(e1).isEqualTo(e2);
+        assertThat(e2).isEqualTo(e1);
+        assertThat(e1.hashCode()).isEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testEqualsHashCodeSameContentSameTypes(final EntryFactory factory) throws Exception {
+        final Entry e1 = createTestEntry(factory);
+        final Entry e2 = createTestEntry(factory);
+        assertThat(e1).isEqualTo(e1);
+        assertThat(e1).isEqualTo(e2);
+        assertThat(e1.hashCode()).isEqualTo(e2.hashCode());
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributes(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes().iterator()).hasSize(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_SN)).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_CN)).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionPresentOptions(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute(singletonAttribute(AD_CN.withOption("lang-fr"), "xxxx"));
+        assertThat(entry.getAllAttributes(AD_CN)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesAttributeDescriptionSupertype(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes(AD_NAME)).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute(singletonAttribute(AD_CUSTOM1.withOption("lang-fr"), "xxxx"));
+        assertThat(entry.getAllAttributes("custom1")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringCustomOptions(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        entry.addAttribute("custom2", "value1");
+        entry.addAttribute("custom2;lang-fr", "value2");
+        assertThat(entry.getAllAttributes("custom2")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("sn")).hasSize(0);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("cn")).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAllAttributesStringSupertype(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAllAttributes("name")).hasSize(2);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeCount(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("custom1")).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("sn")).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.getAttribute("cn")).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testGetName(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=test"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionCustom(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_CUSTOM1).asString()).isEqualTo("custom1");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_SN).asString()).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute(AD_CN).asString()).isEqualTo("test");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("custom1").asString()).isEqualTo("custom1");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("sn").asString()).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testParseAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.parseAttribute("cn").asString()).isEqualTo("test");
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeDescriptionMissing(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute(AD_SN)).isFalse();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeDescriptionPresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute(AD_CN)).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.removeAttribute(emptyAttribute(AD_SN), missingValues)).isFalse();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.removeAttribute(emptyAttribute(AD_CN), missingValues)).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValueMissing1(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_CN, "missing"), missingValues))
+                .isFalse();
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValueMissing2(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_SN, "missing"), missingValues))
+                .isFalse();
+        assertThat(missingValues).hasSize(1);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeAttributeValuePresent(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        final List<ByteString> missingValues = new LinkedList<>();
+        assertThat(entry.removeAttribute(singletonAttribute(AD_CN, "test"), missingValues))
+                .isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+        assertThat(missingValues).isEmpty();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringCustom(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringMissing(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("sn")).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringPresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValueMissing1(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn", "missing")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNotNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValueMissing2(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("sn", "missing")).isSameAs(entry);
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testRemoveAttributeStringValuePresent(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.removeAttribute("cn", "test")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributeMissingEmpty(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(emptyAttribute(AD_SN))).isFalse();
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributeMissingValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(singletonAttribute(AD_SN, "sn"))).isTrue();
+        assertThat(entry.getAttribute(AD_SN)).isEqualTo(singletonAttribute(AD_SN, "sn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributePresentEmpty(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(emptyAttribute(AD_CN))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeAttributePresentValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute(singletonAttribute(AD_CN, "newcn"))).isTrue();
+        assertThat(entry.getAttribute(AD_CN)).isEqualTo(singletonAttribute(AD_CN, "newcn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom1")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomMissingValue(final EntryFactory factory)
+            throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom2", "xxxx")).isSameAs(entry);
+        // This is expected to be null since the type was decoded using the
+        // default schema and a temporary oid was allocated.
+        assertThat(entry.getAttribute(AD_CUSTOM2)).isNull();
+        assertThat(entry.getAttribute("custom2")).isEqualTo(singletonAttribute("custom2", "xxxx"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringCustomValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("custom1", "xxxx")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CUSTOM1))
+                .isEqualTo(singletonAttribute(AD_CUSTOM1, "xxxx"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringMissingEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringMissingValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("sn", "sn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_SN)).isEqualTo(singletonAttribute(AD_SN, "sn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringPresentEmpty(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("cn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isNull();
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testReplaceAttributeStringPresentValue(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.replaceAttribute("cn", "newcn")).isSameAs(entry);
+        assertThat(entry.getAttribute(AD_CN)).isEqualTo(singletonAttribute(AD_CN, "newcn"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testSetNameDN(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.setName(DN.valueOf("cn=foobar"))).isSameAs(entry);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=foobar"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testSetNameString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        assertThat(entry.setName("cn=foobar")).isSameAs(entry);
+        assertThat((Object) entry.getName()).isEqualTo(DN.valueOf("cn=foobar"));
+    }
+
+    @Test(dataProvider = "EntryFactory")
+    public void testToString(final EntryFactory factory) throws Exception {
+        final Entry entry = createTestEntry(factory);
+        // The String representation is unspecified but we should at least
+        // expect the DN to be present.
+        assertThat(entry.toString()).contains("cn=test");
+    }
+
+    private Entry createTestEntry(final EntryFactory factory) throws Exception {
+        return factory.newEntry("dn: cn=test", "objectClass: top", "objectClass: extensibleObject",
+                "cn: test", "custom1: custom1");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
new file mode 100644
index 0000000..74a97c8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
@@ -0,0 +1,329 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static java.util.Arrays.asList;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class FilterTestCase extends SdkTestCase {
+    @DataProvider(name = "badfilterstrings")
+    public Object[][] getBadFilterStrings() throws Exception {
+        return new Object[][] { { null, null }, { "", null }, { "=", null }, { "()", null },
+            { "(&(objectClass=*)(sn=s*s)", null }, { "(dob>12221)", null },
+            { "(cn=bob\\2 doe)", null }, { "(cn=\\4j\\w2\\yu)", null }, { "(cn=ds\\2)", null },
+            { "(&(givenname=bob)|(sn=pep)dob=12))", null }, { "(:=bob)", null },
+            { "(=sally)", null }, { "(cn=billy bob", null },
+            { "(|(!(title=sweep*)(l=Paris*)))", null }, { "(|(!))", null },
+            { "((uid=user.0))", null }, { "(&&(uid=user.0))", null }, { "!uid=user.0", null },
+            { "(:dn:=Sally)", null }, };
+    }
+
+    @DataProvider(name = "filterstrings")
+    public Object[][] getFilterStrings() throws Exception {
+        final Filter equal =
+                Filter.equality("objectClass", ByteString.valueOfUtf8("\\test*(Value)"));
+        final Filter equal2 = Filter.equality("objectClass", ByteString.valueOfUtf8(""));
+        final Filter approx =
+                Filter.approx("sn", ByteString.valueOfUtf8("\\test*(Value)"));
+        final Filter greater =
+                Filter.greaterOrEqual("employeeNumber", ByteString
+                        .valueOfUtf8("\\test*(Value)"));
+        final Filter less =
+                Filter.lessOrEqual("dob", ByteString.valueOfUtf8("\\test*(Value)"));
+        final Filter presense = Filter.present("login");
+
+        final ArrayList<ByteString> any = new ArrayList<>(0);
+        final ArrayList<ByteString> multiAny = new ArrayList<>(1);
+        multiAny.add(ByteString.valueOfUtf8("\\wid*(get)"));
+        multiAny.add(ByteString.valueOfUtf8("*"));
+
+        final Filter substring1 =
+                Filter.substrings("givenName", ByteString.valueOfUtf8("\\Jo*()"), any,
+                        ByteString.valueOfUtf8("\\n*()"));
+        final Filter substring2 =
+                Filter.substrings("givenName", ByteString.valueOfUtf8("\\Jo*()"), multiAny,
+                        ByteString.valueOfUtf8("\\n*()"));
+        final Filter substring3 =
+                Filter.substrings("givenName", ByteString.valueOfUtf8(""), any, ByteString
+                        .valueOfUtf8("\\n*()"));
+        final Filter substring4 =
+                Filter.substrings("givenName", ByteString.valueOfUtf8("\\Jo*()"), any,
+                        ByteString.valueOfUtf8(""));
+        final Filter substring5 =
+                Filter.substrings("givenName", ByteString.valueOfUtf8(""), multiAny,
+                        ByteString.valueOfUtf8(""));
+        final Filter extensible1 =
+                Filter.extensible("2.4.6.8.19", "cn", ByteString
+                        .valueOfUtf8("\\John* (Doe)"), false);
+        final Filter extensible2 =
+                Filter.extensible("2.4.6.8.19", "cn", ByteString
+                        .valueOfUtf8("\\John* (Doe)"), true);
+        final Filter extensible3 =
+                Filter.extensible("2.4.6.8.19", null, ByteString
+                        .valueOfUtf8("\\John* (Doe)"), true);
+        final Filter extensible4 =
+                Filter.extensible(null, "cn", ByteString.valueOfUtf8("\\John* (Doe)"),
+                        true);
+        final Filter extensible5 =
+                Filter.extensible("2.4.6.8.19", null, ByteString
+                        .valueOfUtf8("\\John* (Doe)"), false);
+
+        final ArrayList<Filter> list1 = new ArrayList<>();
+        list1.add(equal);
+        list1.add(approx);
+
+        final Filter and = Filter.and(list1);
+
+        final ArrayList<Filter> list2 = new ArrayList<>();
+        list2.add(substring1);
+        list2.add(extensible1);
+        list2.add(and);
+
+        return new Object[][] {
+            { "(objectClass=\\5Ctest\\2A\\28Value\\29)", equal },
+
+            { "(objectClass=)", equal2 },
+
+            { "(sn~=\\5Ctest\\2A\\28Value\\29)", approx },
+
+            { "(employeeNumber>=\\5Ctest\\2A\\28Value\\29)", greater },
+
+            { "(dob<=\\5Ctest\\2A\\28Value\\29)", less },
+
+            { "(login=*)", presense },
+
+            { "(givenName=\\5CJo\\2A\\28\\29*\\5Cn\\2A\\28\\29)", substring1 },
+
+            { "(givenName=\\5CJo\\2A\\28\\29*\\5Cwid\\2A\\28get\\29*\\2A*\\5Cn\\2A\\28\\29)",
+                substring2 },
+
+            { "(givenName=*\\5Cn\\2A\\28\\29)", substring3 },
+
+            { "(givenName=\\5CJo\\2A\\28\\29*)", substring4 },
+
+            { "(givenName=*\\5Cwid\\2A\\28get\\29*\\2A*)", substring5 },
+
+            { "(cn:2.4.6.8.19:=\\5CJohn\\2A \\28Doe\\29)", extensible1 },
+
+            { "(cn:dn:2.4.6.8.19:=\\5CJohn\\2A \\28Doe\\29)", extensible2 },
+
+            { "(:dn:2.4.6.8.19:=\\5CJohn\\2A \\28Doe\\29)", extensible3 },
+
+            { "(cn:dn:=\\5CJohn\\2A \\28Doe\\29)", extensible4 },
+
+            { "(:2.4.6.8.19:=\\5CJohn\\2A \\28Doe\\29)", extensible5 },
+
+            { "(&(objectClass=\\5Ctest\\2A\\28Value\\29)(sn~=\\5Ctest\\2A\\28Value\\29))",
+                Filter.and(list1) },
+
+            { "(|(objectClass=\\5Ctest\\2A\\28Value\\29)(sn~=\\5Ctest\\2A\\28Value\\29))",
+                Filter.or(list1) },
+
+            { "(!(objectClass=\\5Ctest\\2A\\28Value\\29))", Filter.not(equal) },
+
+            {
+                "(|(givenName=\\5CJo\\2A\\28\\29*\\5Cn\\2A\\28\\29)(cn:2.4.6.8.19:=\\5CJohn\\2A \\28Doe\\29)"
+                        + "(&(objectClass=\\5Ctest\\2A\\28Value\\29)(sn~=\\5Ctest\\2A\\28Value\\29)))",
+                Filter.or(list2) }
+
+        };
+    }
+
+    /**
+     * Decodes the specified filter strings.
+     *
+     * @param filterStr
+     * @param filter
+     * @throws Exception
+     */
+    @Test(dataProvider = "filterstrings")
+    public void testDecode(final String filterStr, final Filter filter) throws Exception {
+        final Filter decoded = Filter.valueOf(filterStr);
+        assertEquals(decoded.toString(), filter.toString());
+    }
+
+    /**
+     * Decodes the specified filter strings.
+     *
+     * @param filterStr
+     * @param filter
+     * @throws Exception
+     */
+    @Test(dataProvider = "filterstrings")
+    public void testToString(final String filterStr, final Filter filter) throws Exception {
+        assertEquals(filterStr, filter.toString());
+    }
+
+    /**
+     * Decodes the erroneous filter strings.
+     *
+     * @param filterStr
+     * @param filter
+     * @throws Exception
+     */
+    @Test(dataProvider = "badfilterstrings", expectedExceptions = {
+            LocalizedIllegalArgumentException.class, NullPointerException.class })
+    public void testDecodeException(final String filterStr, final Filter filter) throws Exception {
+        Filter.valueOf(filterStr);
+    }
+
+    @Test
+    public void testGreaterThanFalse1() throws Exception {
+        final Filter filter = Filter.greaterThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=bbb", "objectclass: top", "cn: bbb");
+        final Matcher matcher = filter.matcher();
+        assertFalse(matcher.matches(entry).toBoolean());
+    }
+
+    @Test
+    public void testGreaterThanFalse2() throws Exception {
+        final Filter filter = Filter.greaterThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=aaa", "objectclass: top", "cn: aaa");
+        final Matcher matcher = filter.matcher();
+        assertFalse(matcher.matches(entry).toBoolean());
+    }
+
+    @Test
+    public void testGreaterThanTrue() throws Exception {
+        final Filter filter = Filter.greaterThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=ccc", "objectclass: top", "cn: ccc");
+        final Matcher matcher = filter.matcher();
+        assertTrue(matcher.matches(entry).toBoolean());
+    }
+
+    @Test
+    public void testLessThanFalse1() throws Exception {
+        final Filter filter = Filter.lessThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=bbb", "objectclass: top", "cn: bbb");
+        final Matcher matcher = filter.matcher();
+        assertFalse(matcher.matches(entry).toBoolean());
+    }
+
+    @Test
+    public void testLessThanFalse2() throws Exception {
+        final Filter filter = Filter.lessThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=ccc", "objectclass: top", "cn: ccc");
+        final Matcher matcher = filter.matcher();
+        assertFalse(matcher.matches(entry).toBoolean());
+    }
+
+    @Test
+    public void testLessThanTrue() throws Exception {
+        final Filter filter = Filter.lessThan("cn", "bbb");
+        final Entry entry = new LinkedHashMapEntry("dn: cn=aaa", "objectclass: top", "cn: aaa");
+        final Matcher matcher = filter.matcher();
+        assertTrue(matcher.matches(entry).toBoolean());
+    }
+
+    /**
+     * Tests the matcher.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testMatcher() throws Exception {
+        final Filter equal =
+                Filter.equality("cn", ByteString.valueOfUtf8("\\test*(Value)"));
+        final LinkedHashMapEntry entry =
+                new LinkedHashMapEntry(DN.valueOf("cn=\\test*(Value),dc=org"));
+        entry.addAttribute("cn", "\\test*(Value)");
+        entry.addAttribute("objectclass", "top,person");
+        final Matcher matcher = equal.matcher();
+        assertTrue(matcher.matches(entry).toBoolean());
+    }
+
+    @DataProvider
+    public Object[][] getAssertionValues() {
+        // Use List for assertion values instead of an array because a List has a
+        // String representation which can be displayed by debuggers, etc.
+
+        // @formatter:off
+        return new Object[][] {
+            {
+                "(objectClass=*)", Collections.emptyList(), "(objectClass=*)"
+            },
+            {
+                "(objectClass=*)", asList("dummy"), "(objectClass=*)"
+            },
+            {
+                "(objectClass=*)", asList("dummy", "dummy"), "(objectClass=*)"
+            },
+            {
+                "(cn=%s)", asList("dummy"), "(cn=dummy)"
+            },
+            {
+                "(|(cn=%s)(uid=user.%s))", asList("alice", (Object) 1234), "(|(cn=alice)(uid=user.1234))"
+            },
+            {
+                "(|(cn=%1$s)(sn=%1$s))", asList("alice"), "(|(cn=alice)(sn=alice))"
+            },
+            // Check escaping.
+            {
+                "(cn=%s)", asList("*"), "(cn=\\2A)"
+            },
+            {
+                "(|(cn=%1$s)(sn=%1$s))", asList("alice)(objectClass=*"),
+                "(|(cn=alice\\29\\28objectClass=\\2A)(sn=alice\\29\\28objectClass=\\2A))"
+            },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "getAssertionValues")
+    public void testValueOfTemplate(String template, List<?> assertionValues, String expected)
+            throws Exception {
+        Filter filter = Filter.format(template, assertionValues.toArray());
+        assertEquals(filter.toString(), expected);
+    }
+
+    @DataProvider
+    public Object[][] getEscapeAssertionValues() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                "dummy", "dummy"
+            },
+            {
+                1234, "1234"
+            },
+            {
+                "*", "\\2A"
+            },
+            {
+                "alice)(objectClass=*", "alice\\29\\28objectClass=\\2A"
+            },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "getEscapeAssertionValues")
+    public void testEscapeAssertionValue(Object unescaped, String expected) throws Exception {
+        assertEquals(Filter.escapeAssertionValue(unescaped), expected);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GSERParserTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GSERParserTestCase.java
new file mode 100644
index 0000000..5b63103
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GSERParserTestCase.java
@@ -0,0 +1,291 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.ldap;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * This class tests the GSERParser.
+ */
+@SuppressWarnings("javadoc")
+public class GSERParserTestCase extends SdkTestCase {
+
+    /**
+     * Try to create a GSER Parser with <CODE>null</CODE> as parameter.
+     */
+    @Test(expectedExceptions = { NullPointerException.class })
+    public void testGSERParserInitWithNull() throws Exception {
+        new GSERParser(null);
+    }
+
+    /**
+     * Test the <CODE>hasNext</CODE> method.
+     */
+    @Test
+    public void testHasNext() throws Exception {
+        GSERParser parser = new GSERParser("0");
+        assertTrue(parser.hasNext());
+        assertEquals(parser.nextInteger(), 0);
+        assertFalse(parser.hasNext());
+    }
+
+    /**
+     * Test the <CODE>skipSP</CODE> method.
+     */
+    @Test
+    public void testSkipSP() throws Exception {
+        String[] values = {" 42", "  42", "42"};
+        for (String value : values) {
+            GSERParser parser = new GSERParser(value);
+            assertEquals(parser.skipSP().nextInteger(), 42);
+            assertFalse(parser.hasNext());
+        }
+    }
+
+    /**
+     * Test the <CODE>skipMSP</CODE> method.
+     */
+    @Test
+    public void testSkipMSP() throws Exception {
+        String[] values = {" 42", "  42", "           42"};
+        for (String value : values) {
+            GSERParser parser = new GSERParser(value);
+            assertEquals(parser.skipMSP().nextInteger(), 42);
+            assertFalse(parser.hasNext());
+        }
+    }
+
+    /**
+     * Verify that <CODE>skipMSP</CODE> requires at least one space.
+     */
+    @Test(expectedExceptions = { DecodeException.class })
+    public void testSkipMSPwithZeroSpaces() throws Exception {
+        GSERParser parser = new GSERParser("42");
+        parser.skipMSP();
+    }
+
+    /**
+     * Create data for the <CODE>testSequence</CODE> test case.
+     */
+    @DataProvider(name = "sequenceValues")
+    public Object[][] createSequenceValues() {
+        return new Object[][]{
+            {"{123,122}", true},
+            {"{ 123,1}", true},
+            {"{ 123   ,   1   }", true},
+            {"{0123,}", false},
+            {"{0123 42 }", false},
+            {"{123  , 11 ", false},
+            {" {123  , 11 ", false},
+            {" 123  , 11}", false}
+        };
+    }
+
+    /**
+     * Test sequence parsing.
+     */
+    @Test(dataProvider = "sequenceValues")
+    public void testSequence(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            parser.readStartSequence();
+            parser.nextInteger();
+            parser.skipSP().skipSeparator();
+            parser.nextInteger();
+            parser.readEndSequence();
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+
+    /**
+     * Create data for the <CODE>testString</CODE> test case.
+     */
+    @DataProvider(name = "stringValues")
+    public Object[][] createStringValues() {
+        return new Object[][]{
+            {"\"\"", true},
+            {"\"escaped\"\"dquotes\"", true},
+            {"\"valid Unicode \u00D6\u00C4\"", true},
+            {"\"only one \" \"", false},
+            {"invalid without dquotes", false},
+            {"\"missing end", false},
+            {"\"valid string\" with extra trailing characters", false}
+        };
+    }
+
+    /**
+     * Test the parsing of String values.
+     */
+    @Test(dataProvider = "stringValues")
+    public void testString(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            assertNotNull(parser.nextString());
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+
+    /**
+     * Create data for the <CODE>testInteger</CODE> test case.
+     */
+    @DataProvider(name = "integerValues")
+    public Object[][] createIntegerValues() {
+        return new Object[][]{
+            {"0123456", true},
+            {"42", true},
+            {"0", true},
+            {"", false},
+            {"0xFF", false},
+            {"NULL", false},
+            {"Not a Number", false}
+        };
+    }
+
+    /**
+     * Create data for the <CODE>testBigInteger</CODE> test case.
+     */
+    @DataProvider(name = "bigIntegerValues")
+    public Object[][] createBigIntegerValues() {
+        return new Object[][]{
+            {"0123456", true},
+            {"42", true},
+            {"0", true},
+            {"", false},
+            {"0xFF", false},
+            {"NULL", false},
+            {"Not a Number", false},
+            {"2147483648", true}
+        };
+    }
+
+    /**
+     * Test the parsing of Integer values.
+     */
+    @Test(dataProvider = "integerValues")
+    public void testInteger(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            parser.nextInteger();
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+
+    /**
+     * Test the parsing of BigInteger values.
+     */
+    @Test(dataProvider = "bigIntegerValues")
+    public void testBigInteger(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            parser.nextBigInteger();
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+
+    /**
+     * Create data for the <CODE>testNamedValueIdentifier</CODE> test case.
+     */
+    @DataProvider(name = "namedValueIdentifierValues")
+    public Object[][] createNamedValueIdentifierValues() {
+        return new Object[][]{
+            {"serialNumber ", true},
+            {"issuer ", true},
+            {"Serialnumber ", false},
+            {"0serialnumber ", false},
+            {"serial Number ", false},
+            {"missingSpace", false}
+        };
+    }
+
+    /**
+     * Test the parsing of NamedValue identifiers.
+     */
+    @Test(dataProvider = "namedValueIdentifierValues")
+    public void testNamedValueIdentifier(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            assertNotNull(parser.nextNamedValueIdentifier());
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+
+    /**
+     * Create data for the <CODE>testIdentifiedChoiceIdentifier</CODE> test
+     * case.
+     */
+    @DataProvider(name = "identifiedChoicdeIdentifierValues")
+    public Object[][] createIdentifiedChoicdeIdentifierValues() {
+        return new Object[][]{
+            {"serialNumber:", true},
+            {"issuer1:", true},
+            {"Serialnumber:", false},
+            {"0serialnumber:", false},
+            {"serial Number:", false},
+            {"missingColon", false}
+        };
+    }
+
+    /**
+     * Test the parsing of IdentifiedChoice identifiers.
+     */
+    @Test(dataProvider = "identifiedChoicdeIdentifierValues")
+    public void testIdentifiedChoicdeIdentifier(String value, boolean expectedResult) throws Exception {
+        GSERParser parser = new GSERParser(value);
+        boolean result = true;
+        try {
+            assertNotNull(parser.nextChoiceValueIdentifier());
+            if (parser.hasNext()) {
+                result = false;
+            }
+        } catch (DecodeException e) {
+            result = false;
+        }
+        assertEquals(expectedResult, result);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java
new file mode 100644
index 0000000..a08e020
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java
@@ -0,0 +1,220 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Generalized time tests.
+ */
+@SuppressWarnings("javadoc")
+public class GeneralizedTimeTest extends SdkTestCase {
+
+    @DataProvider
+    public Object[][] calendars() {
+        // Test time zone.
+        final GregorianCalendar europeWinter = new GregorianCalendar();
+        europeWinter.setLenient(false);
+        europeWinter.setTimeZone(TimeZone.getTimeZone("Europe/Paris"));
+        europeWinter.set(2013, 0, 1, 13, 0, 0);
+        europeWinter.set(Calendar.MILLISECOND, 0);
+
+        // Test daylight savings.
+        final GregorianCalendar europeSummer = new GregorianCalendar();
+        europeSummer.setLenient(false);
+        europeSummer.setTimeZone(TimeZone.getTimeZone("Europe/Paris"));
+        europeSummer.set(2013, 5, 1, 13, 0, 0);
+        europeSummer.set(Calendar.MILLISECOND, 0);
+
+        return new Object[][] { { europeWinter }, { europeSummer } };
+    }
+
+    @Test(dataProvider = "calendars")
+    public void fromToCalendary(final Calendar calendar) {
+        final String s1 = GeneralizedTime.valueOf(calendar).toString();
+        final Calendar reparsed1 = GeneralizedTime.valueOf(s1).toCalendar();
+        assertThat(reparsed1.getTimeInMillis()).isEqualTo(calendar.getTimeInMillis());
+    }
+
+    @DataProvider
+    public Object[][] invalidStrings() {
+        return new Object[][] { {"20060906135030+3359" }, { "20060906135030+2389" },
+            { "20060906135030+2361" }, { "20060906135030+" }, { "20060906135030+0" },
+            { "20060906135030+010" }, { "20061200235959Z" }, { "2006121a235959Z" },
+            { "2006122a235959Z" }, { "20060031235959Z" }, { "20061331235959Z" },
+            { "20062231235959Z" }, { "20061232235959Z" }, { "2006123123595aZ" },
+            { "200a1231235959Z" }, { "2006j231235959Z" }, { "200612-1235959Z" },
+            { "20061231#35959Z" }, { "2006" }, };
+    }
+
+    @Test
+    public void testCompareEquals() {
+        final GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        final GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906125030Z");
+        assertThat(gt1.compareTo(gt2)).isEqualTo(0);
+    }
+
+    @Test
+    public void testCompareGreaterThan() {
+        final GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030Z");
+        final GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030+01");
+        assertThat(gt1.compareTo(gt2) > 0).isTrue();
+    }
+
+    @Test
+    public void testCompareLessThan() {
+        final GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        final GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030Z");
+        assertThat(gt1.compareTo(gt2) < 0).isTrue();
+    }
+
+    @Test
+    public void testEqualsFalse() {
+        final GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030Z");
+        final GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030+01");
+        assertThat(gt1).isNotEqualTo(gt2);
+    }
+
+    @Test
+    public void testEqualsTrue() {
+        final GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        final GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906125030Z");
+        assertThat(gt1).isEqualTo(gt2);
+    }
+
+    @Test
+    public void testValueOfCalendar() {
+        final Calendar calendar = Calendar.getInstance();
+        final GeneralizedTime time = GeneralizedTime.valueOf(calendar);
+        assertThat(time.getTimeInMillis()).isEqualTo(calendar.getTimeInMillis());
+        assertThat(time.toCalendar()).isEqualTo(calendar);
+        assertThat(time.toDate()).isEqualTo(calendar.getTime());
+    }
+
+    @Test
+    public void testValueOfDate() {
+        final Date date = new Date();
+        final GeneralizedTime time = GeneralizedTime.valueOf(date);
+        assertThat(time.getTimeInMillis()).isEqualTo(date.getTime());
+        assertThat(time.toDate()).isEqualTo(date);
+    }
+
+    @Test(expectedExceptions = { LocalizedIllegalArgumentException.class },
+            dataProvider = "invalidStrings")
+    public void testValueOfInvalidString(final String s) {
+        GeneralizedTime.valueOf(s);
+    }
+
+    @Test
+    public void testValueOfLong() {
+        final Date date = new Date();
+        final GeneralizedTime time = GeneralizedTime.valueOf(date.getTime());
+        assertThat(time.getTimeInMillis()).isEqualTo(date.getTime());
+        assertThat(time.toDate()).isEqualTo(date);
+    }
+
+    @Test(dataProvider = "validStrings")
+    public void testValueOfValidString(final String s) {
+        assertThat(GeneralizedTime.valueOf(s).toString()).isEqualTo(s);
+    }
+
+    @DataProvider
+    public Object[][] validStrings() {
+        return new Object[][] { { "2006090613Z" }, { "20060906135030+01" }, { "200609061350Z" },
+            { "20060906135030Z" }, { "20061116135030Z" }, { "20061126135030Z" },
+            { "20061231235959Z" }, { "20060906135030+0101" }, { "20060906135030+2359" }, };
+    }
+
+    /**
+     * Create data for toString test.
+     *
+     * @return Returns test data.
+     */
+    @DataProvider()
+    public Object[][] createToStringData() {
+        return new Object[][] {
+            // Note that Calendar months run from 0-11,
+            // and that there was no such year as year 0 (1 BC -> 1 AD).
+            {   1,  0,  1,  0,  0,  0,   0, "00010101000000Z"},
+            {   9,  0,  1,  0,  0,  0,   0, "00090101000000Z"},
+            {  10,  0,  1,  0,  0,  0,   0, "00100101000000Z"},
+            {  99,  0,  1,  0,  0,  0,   0, "00990101000000Z"},
+            { 100,  0,  1,  0,  0,  0,   0, "01000101000000Z"},
+            { 999,  0,  1,  0,  0,  0,   0, "09990101000000Z"},
+            {1000,  0,  1,  0,  0,  0,   0, "10000101000000Z"},
+            {2000,  0,  1,  0,  0,  0,   0, "20000101000000Z"},
+            {2099,  0,  1,  0,  0,  0,   0, "20990101000000Z"},
+            {2000,  8,  1,  0,  0,  0,   0, "20000901000000Z"},
+            {2000,  9,  1,  0,  0,  0,   0, "20001001000000Z"},
+            {2000, 10,  1,  0,  0,  0,   0, "20001101000000Z"},
+            {2000, 11,  1,  0,  0,  0,   0, "20001201000000Z"},
+            {2000,  0,  9,  0,  0,  0,   0, "20000109000000Z"},
+            {2000,  0, 10,  0,  0,  0,   0, "20000110000000Z"},
+            {2000,  0, 19,  0,  0,  0,   0, "20000119000000Z"},
+            {2000,  0, 20,  0,  0,  0,   0, "20000120000000Z"},
+            {2000,  0, 29,  0,  0,  0,   0, "20000129000000Z"},
+            {2000,  0, 30,  0,  0,  0,   0, "20000130000000Z"},
+            {2000,  0, 31,  0,  0,  0,   0, "20000131000000Z"},
+            {2000,  0,  1,  9,  0,  0,   0, "20000101090000Z"},
+            {2000,  0,  1, 10,  0,  0,   0, "20000101100000Z"},
+            {2000,  0,  1, 19,  0,  0,   0, "20000101190000Z"},
+            {2000,  0,  1, 20,  0,  0,   0, "20000101200000Z"},
+            {2000,  0,  1, 23,  0,  0,   0, "20000101230000Z"},
+            {2000,  0,  1,  0,  9,  0,   0, "20000101000900Z"},
+            {2000,  0,  1,  0, 10,  0,   0, "20000101001000Z"},
+            {2000,  0,  1,  0, 59,  0,   0, "20000101005900Z"},
+            {2000,  0,  1,  0,  0,  9,   0, "20000101000009Z"},
+            {2000,  0,  1,  0,  0, 10,   0, "20000101000010Z"},
+            {2000,  0,  1,  0,  0, 59,   0, "20000101000059Z"},
+            {2000,  0,  1,  0,  0,  0,   9, "20000101000000.009Z"},
+            {2000,  0,  1,  0,  0,  0,  10, "20000101000000.010Z"},
+            {2000,  0,  1,  0,  0,  0,  99, "20000101000000.099Z"},
+            {2000,  0,  1,  0,  0,  0, 100, "20000101000000.100Z"},
+            {2000,  0,  1,  0,  0,  0, 999, "20000101000000.999Z"},
+        };
+    }
+
+    private static final String TIME_ZONE_UTC = "UTC";
+
+
+    @Test(dataProvider = "createToStringData")
+    public void testToString(int yyyy, int month, int dd, int hour, int mm, int ss, int millis, String expected)
+            throws Exception {
+        Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone(TIME_ZONE_UTC));
+        calendar.set(yyyy, month, dd, hour, mm, ss);
+        calendar.set(Calendar.MILLISECOND, millis);
+
+        long time = calendar.getTimeInMillis();
+        // test creation with long only if it is positive because negative values will be rejected
+        if (time > 0) {
+            assertThat(GeneralizedTime.valueOf(time).toString()).isEqualTo(expected);
+        }
+
+        Date date = new Date(time);
+        assertThat(GeneralizedTime.valueOf(date).toString()).isEqualTo(expected);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java
new file mode 100644
index 0000000..98787a8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java
@@ -0,0 +1,508 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.ResultCode.SUCCESS;
+import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
+import static org.forgerock.opendj.ldap.TestCaseUtils.mockTimeService;
+import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.newLdapPromiseImpl;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newBindLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSearchLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.ResultHandler;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the connection pool implementation..
+ */
+@SuppressWarnings("javadoc")
+public class HeartBeatConnectionFactoryTestCase extends SdkTestCase {
+
+    // @formatter:off
+
+    /*
+     * Key features which need testing:
+     *
+     * - lazy scheduler registration and deregistration
+     * - scheduled task only sends heart-beat when connection is open
+     * - connection remains valid when any response is received
+     * - connection remains valid when a heart beat response is received
+     * - connection becomes invalid if no response is received during timeout
+     * - heart beat only sent when connection is idle
+     * - slow bind / startTLS prevents heart beat
+     * - slow heart beat prevents bind / start TLS
+     * - support concurrent bind / start TLS
+     */
+
+    // @formatter:on
+
+    private static final SearchRequest HEARTBEAT = newSearchRequest("dc=test", BASE_OBJECT, "(objectclass=*)", "1.1");
+
+    private LDAPConnectionImpl ldapConnection;
+    private LDAPConnectionFactoryImpl ldapFactory;
+    private Connection hbc;
+    private LDAPConnectionFactory hbcf;
+    private List<ConnectionEventListener> listeners;
+    private MockScheduler scheduler;
+
+    /**
+     * Disables logging before the tests.
+     */
+    @BeforeClass
+    public void disableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.SEVERE);
+    }
+
+    /**
+     * Re-enable logging after the tests.
+     */
+    @AfterClass
+    public void enableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.INFO);
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDown() {
+        try {
+            if (hbc != null) {
+                hbc.close();
+                assertThat(hbc.isValid()).isFalse();
+                assertThat(hbc.isClosed()).isTrue();
+            }
+            scheduler.runAllTasks(); // Flush any remaining timeout tasks.
+            assertThat(scheduler.isScheduled()).isFalse(); // No more connections to check.
+            hbcf.close();
+        } finally {
+            ldapConnection = null;
+            ldapFactory = null;
+            hbcf = null;
+            listeners = null;
+            scheduler = null;
+            hbc = null;
+        }
+    }
+
+    @Test
+    public void testBindWhileHeartBeatInProgress() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        mockBindAsyncResponse();
+        hbc = hbcf.getConnection();
+
+        /*
+         * Send a heartbeat, trapping the search call-back so that we can send
+         * the response once we have attempted a bind.
+         */
+        final SearchResultLdapPromiseImpl heartBeatPromise =
+                newSearchLdapPromise(-1, HEARTBEAT, null, null, ldapConnection);
+        when(ldapConnection.searchAsync(same(HEARTBEAT),
+                                        any(IntermediateResponseHandler.class),
+                                        any(SearchResultHandler.class)))
+                .thenReturn(heartBeatPromise);
+        when(hbcf.timeService.now()).thenReturn(11000L);
+        scheduler.runAllTasks(); // Send the heartbeat.
+
+        // Capture the heartbeat search result handler.
+        verify(ldapConnection, times(2)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+        assertThat(hbc.isValid()).isTrue(); // Not checked yet.
+
+        /*
+         * Now attempt a bind request, which should be held in a queue until the
+         * heart beat completes.
+         */
+        hbc.bindAsync(newSimpleBindRequest());
+        verify(ldapConnection, times(0)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+
+        // Send fake heartbeat response, releasing the bind request.
+        heartBeatPromise.getWrappedPromise().handleResult(newResult(SUCCESS));
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+    }
+
+    @Test
+    public void testGetConnection() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        hbc = hbcf.getConnection();
+        assertThat(hbc).isNotNull();
+        assertThat(hbc.isValid()).isTrue();
+    }
+
+    @Test
+    public void testGetConnectionAsync() throws Exception {
+        @SuppressWarnings("unchecked")
+        final ResultHandler<Connection> mockResultHandler = mock(ResultHandler.class);
+
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        hbc = hbcf.getConnectionAsync().thenOnResult(mockResultHandler).getOrThrow();
+        assertThat(hbc).isNotNull();
+        assertThat(hbc.isValid()).isTrue();
+
+        verify(mockResultHandler).handleResult(any(Connection.class));
+        verifyNoMoreInteractions(mockResultHandler);
+    }
+
+    @Test
+    public void testGetConnectionAsyncWithInitialHeartBeatError() throws Exception {
+        @SuppressWarnings("unchecked")
+        final ResultHandler<Connection> mockResultHandler = mock(ResultHandler.class);
+        final PromiseImpl<LdapException, NeverThrowsException> promisedError = PromiseImpl.create();
+
+        mockConnectionWithInitialHeartbeatResult(ResultCode.BUSY);
+        Promise<? extends Connection, LdapException> promise = hbcf.getConnectionAsync();
+        promise.thenOnResult(mockResultHandler).thenOnException(new ExceptionHandler<LdapException>() {
+            @Override
+            public void handleException(LdapException exception) {
+                promisedError.handleResult(exception);
+            }
+        });
+
+        checkInitialHeartBeatFailure(promisedError.getOrThrow());
+
+        try {
+            promise.getOrThrow();
+            fail("Unexpectedly obtained a connection");
+        } catch (final LdapException e) {
+            checkInitialHeartBeatFailure(e);
+        }
+
+        verifyNoMoreInteractions(mockResultHandler);
+    }
+
+    @Test
+    public void testGetConnectionWithInitialHeartBeatError() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.BUSY);
+        try {
+            hbcf.getConnection();
+            fail("Unexpectedly obtained a connection");
+        } catch (final LdapException e) {
+            checkInitialHeartBeatFailure(e);
+        }
+    }
+
+    @Test
+    public void testHeartBeatSucceedsThenFails() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+
+        // Get a connection and check that it was pinged.
+        hbc = hbcf.getConnection();
+
+        verifyHeartBeatSent(ldapConnection, 1);
+        assertThat(scheduler.isScheduled()).isTrue(); // heartbeater
+        assertThat(listeners).hasSize(1);
+        assertThat(hbc.isValid()).isTrue();
+
+        // Invoke heartbeat before the connection is considered idle.
+        scheduler.runAllTasks();
+        verifyHeartBeatSent(ldapConnection, 1); // No heartbeat sent - not idle yet.
+        assertThat(hbc.isValid()).isTrue();
+
+        // Invoke heartbeat after the connection is considered idle.
+        when(hbcf.timeService.now()).thenReturn(6000L);
+        scheduler.runAllTasks();
+        verifyHeartBeatSent(ldapConnection, 2); // Heartbeat sent.
+        assertThat(hbc.isValid()).isTrue();
+
+        // Now force the heartbeat to fail.
+        mockHeartBeatResponse(ldapConnection, listeners, ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        when(hbcf.timeService.now()).thenReturn(11000L);
+        scheduler.runAllTasks();
+        verifyHeartBeatSent(ldapConnection, 3);
+        assertThat(hbc.isValid()).isFalse();
+
+        // Flush redundant timeout tasks.
+        scheduler.runAllTasks();
+
+        // Attempt to send a new request: it should fail immediately.
+        @SuppressWarnings("unchecked")
+        final ExceptionHandler<LdapException> mockHandler = mock(ExceptionHandler.class);
+        hbc.modifyAsync(newModifyRequest(DN.rootDN())).thenOnException(mockHandler);
+        final ArgumentCaptor<LdapException> arg = ArgumentCaptor.forClass(LdapException.class);
+        verify(mockHandler).handleException(arg.capture());
+        assertThat(arg.getValue().getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+
+        assertThat(hbc.isValid()).isFalse();
+        assertThat(hbc.isClosed()).isFalse();
+    }
+
+    @Test
+    public void testHeartBeatTimeout() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+
+        // Get a connection and check that it was pinged.
+        hbc = hbcf.getConnection();
+
+        // Now force the heartbeat to fail due to timeout.
+        mockHeartBeatResponse(ldapConnection, listeners, null /* no response */);
+        when(hbcf.timeService.now()).thenReturn(11000L);
+        scheduler.runAllTasks(); // Send the heartbeat.
+        verifyHeartBeatSent(ldapConnection, 2);
+        assertThat(hbc.isValid()).isTrue(); // Not checked yet.
+        when(hbcf.timeService.now()).thenReturn(12000L);
+        scheduler.runAllTasks(); // Check for heartbeat.
+        assertThat(hbc.isValid()).isFalse(); // Now invalid.
+        assertThat(hbc.isClosed()).isFalse();
+    }
+
+    @Test(description = "OPENDJ-1348")
+    public void testBindPreventsHeartBeatTimeout() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        BindResultLdapPromiseImpl promise = mockBindAsyncResponse();
+        hbc = hbcf.getConnection();
+
+        /*
+         * Send a bind request, trapping the bind call-back so that we can send
+         * the response once we have attempted a heartbeat.
+         */
+        when(hbcf.timeService.now()).thenReturn(11000L);
+        hbc.bindAsync(newSimpleBindRequest());
+
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+
+        // Verify no heartbeat is sent because there is a bind in progress.
+        when(hbcf.timeService.now()).thenReturn(11001L);
+        scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.sendHeartBeat()
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+
+        // Send fake bind response, releasing the heartbeat.
+        when(hbcf.timeService.now()).thenReturn(11099L);
+        promise.getWrappedPromise().handleResult(newBindResult(SUCCESS));
+
+        // Check that bind response acts as heartbeat.
+        assertThat(hbc.isValid()).isTrue();
+        when(hbcf.timeService.now()).thenReturn(11100L);
+        scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.checkForHeartBeat()
+        assertThat(hbc.isValid()).isTrue();
+    }
+
+    @Test(description = "OPENDJ-1348")
+    public void testBindTriggersHeartBeatTimeoutWhenTooSlow() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        mockBindAsyncResponse();
+        hbc = hbcf.getConnection();
+
+        // Send another bind request which will timeout.
+        when(hbcf.timeService.now()).thenReturn(20000L);
+        hbc.bindAsync(newSimpleBindRequest());
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+
+        // Verify no heartbeat is sent because there is a bind in progress.
+        when(hbcf.timeService.now()).thenReturn(20001L);
+        scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.sendHeartBeat()
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+
+        // Check that lack of bind response acts as heartbeat timeout.
+        assertThat(hbc.isValid()).isTrue();
+        when(hbcf.timeService.now()).thenReturn(20100L);
+        scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.checkForHeartBeat()
+        assertThat(hbc.isValid()).isFalse();
+    }
+
+    @Test
+    public void testHeartBeatWhileBindInProgress() throws Exception {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        //Trapping the callback of mockedConnection.bindAsync
+        BindResultLdapPromiseImpl promise = mockBindAsyncResponse();
+        hbc = hbcf.getConnection();
+        hbc.bindAsync(newSimpleBindRequest());
+
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+
+        /*
+         * Now attempt the heartbeat which should not happen because there is a
+         * bind in progress.
+         */
+        when(hbcf.timeService.now()).thenReturn(11000L);
+        // Attempt to send the heartbeat.
+        scheduler.runAllTasks();
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+
+        // Send fake bind response, releasing the heartbeat.
+        promise.getWrappedPromise().handleResult(newBindResult(SUCCESS));
+
+        // Attempt to send a heartbeat again.
+        when(hbcf.timeService.now()).thenReturn(16000L);
+        // Attempt to send the heartbeat.
+        scheduler.runAllTasks();
+        verify(ldapConnection, times(2)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+    }
+
+    @Test
+    public void testToString() {
+        mockConnectionWithInitialHeartbeatResult(ResultCode.SUCCESS);
+        assertThat(hbcf.toString()).isNotNull();
+    }
+
+    private void checkInitialHeartBeatFailure(final LdapException e) {
+        /*
+         * Initial heartbeat failure should trigger connection exception with
+         * heartbeat cause.
+         */
+        assertThat(e).isInstanceOf(ConnectionException.class);
+        assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        assertThat(e.getCause()).isInstanceOf(LdapException.class);
+        assertThat(((LdapException) e.getCause()).getResult().getResultCode()).isEqualTo(ResultCode.BUSY);
+    }
+
+    private void mockConnectionWithInitialHeartbeatResult(final ResultCode initialHeartBeatResult) {
+        listeners = new LinkedList<>();
+        ldapConnection = mockLDAPConnectionImpl(listeners);
+        when(ldapConnection.isValid()).thenReturn(true);
+        mockHeartBeatResponse(ldapConnection, listeners, initialHeartBeatResult);
+        ldapFactory = mock(LDAPConnectionFactoryImpl.class);
+        when(ldapFactory.getConnectionAsync()).thenAnswer(new Answer<Promise<LDAPConnectionImpl, LdapException>>() {
+            @Override
+            public Promise<LDAPConnectionImpl, LdapException> answer(final InvocationOnMock invocation)
+                    throws Throwable {
+                return newSuccessfulLdapPromise(ldapConnection);
+            }
+        });
+        TransportProvider provider = mock(TransportProvider.class);
+        when(provider.getLDAPConnectionFactory(anyString(), anyInt(), any(Options.class))).thenReturn(ldapFactory);
+        scheduler = new MockScheduler();
+
+        // Create heart beat connection factory.
+        hbcf = new LDAPConnectionFactory("dummyHost",
+                                         1389,
+                                         Options.defaultOptions()
+                                                .set(TRANSPORT_PROVIDER_INSTANCE, provider)
+                                                .set(HEARTBEAT_ENABLED, true)
+                                                .set(HEARTBEAT_TIMEOUT, duration("100 ms"))
+                                                .set(HEARTBEAT_SEARCH_REQUEST, HEARTBEAT)
+                                                .set(HEARTBEAT_SCHEDULER, scheduler));
+
+        // Set initial time stamp.
+        hbcf.timeService = mockTimeService(0);
+    }
+
+    private BindResultLdapPromiseImpl mockBindAsyncResponse() {
+        final BindResultLdapPromiseImpl bindPromise = newBindLdapPromise(-1, null, null, null);
+        when(ldapConnection.bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(bindPromise);
+        return bindPromise;
+    }
+
+    //
+    private void mockHeartBeatResponse(
+            final LDAPConnectionImpl mockConnection,
+            final List<ConnectionEventListener> listeners,
+            final ResultCode resultCode) {
+        // @Checkstyle:off
+        when(mockConnection.searchAsync(any(SearchRequest.class),
+                                        any(IntermediateResponseHandler.class),
+                                        any(SearchResultHandler.class))).thenAnswer(new Answer<LdapPromise<Result>>() {
+            @Override
+            public LdapPromise<Result> answer(final InvocationOnMock invocation) throws Throwable {
+                if (resultCode == null) {
+                    return newLdapPromiseImpl();
+                }
+
+                if (resultCode.isExceptional()) {
+                    final LdapException error = newLdapException(resultCode);
+                    if (error instanceof ConnectionException) {
+                        for (final ConnectionEventListener listener : listeners) {
+                            listener.handleConnectionError(false, error);
+                        }
+                    }
+                    return newFailedLdapPromise(error);
+                } else {
+                    return newSuccessfulLdapPromise(newResult(resultCode));
+                }
+            }
+        });
+        // @Checkstyle:on
+    }
+
+    private void verifyHeartBeatSent(final LDAPConnectionImpl connection, final int times) {
+        verify(connection, times(times)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+    }
+
+    private static LDAPConnectionImpl mockLDAPConnectionImpl(final List<ConnectionEventListener> listeners) {
+        final LDAPConnectionImpl mockConnection = mock(LDAPConnectionImpl.class);
+
+        // Handle listener registration / deregistration in mock connection.
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.add(listener);
+                return null;
+            }
+        }).when(mockConnection).addConnectionEventListener(any(ConnectionEventListener.class));
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.remove(listener);
+                return null;
+            }
+        }).when(mockConnection).removeConnectionEventListener(any(ConnectionEventListener.class));
+
+        return mockConnection;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
new file mode 100644
index 0000000..25abde3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
@@ -0,0 +1,569 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.SSLContext;
+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.sasl.AuthorizeCallback;
+import javax.security.sasl.RealmCallback;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+
+import com.forgerock.opendj.ldap.controls.AccountUsabilityRequestControl;
+import com.forgerock.opendj.ldap.controls.AccountUsabilityResponseControl;
+import org.forgerock.util.Options;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.opendj.ldap.LDAPListener.*;
+
+/**
+ * A simple ldap server that manages 1000 entries and used for running
+ * testcases.
+ * <p>
+ * FIXME: make it MT-safe.
+ */
+@SuppressWarnings("javadoc")
+public class LDAPServer implements ServerConnectionFactory<LDAPClientContext, Integer> {
+    /** Creates an abandonable request from the ordinary requests. */
+    private static class AbandonableRequest implements Request {
+        /** The request. */
+        private final Request request;
+
+        /** Whether is has been cancelled. */
+        private final AtomicBoolean isCanceled;
+
+        /** Ctor. */
+        AbandonableRequest(final Request request) {
+            this.request = request;
+            this.isCanceled = new AtomicBoolean(false);
+        }
+
+        @Override
+        public Request addControl(final Control cntrl) {
+            return request.addControl(cntrl);
+        }
+
+        @Override
+        public boolean containsControl(final String oid) {
+            return request.containsControl(oid);
+        }
+
+        @Override
+        public <C extends Control> C getControl(final ControlDecoder<C> decoder,
+                final DecodeOptions options) throws DecodeException {
+            return request.getControl(decoder, options);
+        }
+
+        @Override
+        public List<Control> getControls() {
+            return request.getControls();
+        }
+
+        void cancel() {
+            isCanceled.set(true);
+        }
+
+        boolean isCanceled() {
+            return isCanceled.get();
+        }
+    }
+
+    /** The singleton instance. */
+    private static final LDAPServer INSTANCE = new LDAPServer();
+
+    /**
+     * Returns the singleton instance.
+     *
+     * @return Singleton instance.
+     */
+    public static LDAPServer getInstance() {
+        return INSTANCE;
+    }
+
+    private class LDAPServerConnection implements ServerConnection<Integer> {
+
+        private final LDAPClientContext clientContext;
+        private SaslServer saslServer;
+
+        private LDAPServerConnection(LDAPClientContext clientContext) {
+            this.clientContext = clientContext;
+        }
+
+        @Override
+        public void handleAbandon(final Integer context, final AbandonRequest request)
+                throws UnsupportedOperationException {
+            // Check if we have any concurrent operation with this message id.
+            final AbandonableRequest req = requestsInProgress.get(request.getRequestID());
+            if (req == null) {
+                // Nothing to do here.
+                return;
+            }
+            // Cancel the request
+            req.cancel();
+            // No response is needed.
+        }
+
+        @Override
+        public void handleAdd(final Integer context, final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> handler) throws UnsupportedOperationException {
+            Result result = null;
+            final AbandonableRequest abReq = new AbandonableRequest(request);
+            requestsInProgress.put(context, abReq);
+            // Get the DN.
+            final DN dn = request.getName();
+            if (entryMap.containsKey(dn)) {
+                // duplicate entry.
+                result = Responses.newResult(ResultCode.ENTRY_ALREADY_EXISTS);
+                handler.handleException(newLdapException(result));
+                // doesn't matter if it was canceled.
+                requestsInProgress.remove(context);
+                return;
+            }
+
+            // Create an entry out of this request.
+            final SearchResultEntry entry = Responses.newSearchResultEntry(dn);
+            for (final Control control : request.getControls()) {
+                entry.addControl(control);
+            }
+
+            for (final Attribute attr : request.getAllAttributes()) {
+                entry.addAttribute(attr);
+            }
+
+            if (abReq.isCanceled()) {
+                result = Responses.newResult(ResultCode.CANCELLED);
+                handler.handleException(newLdapException(result));
+                requestsInProgress.remove(context);
+                return;
+            }
+            // Add this to the map.
+            entryMap.put(dn, entry);
+            requestsInProgress.remove(context);
+            result = Responses.newResult(ResultCode.SUCCESS);
+            handler.handleResult(result);
+        }
+
+        @Override
+        public void handleBind(final Integer context, final int version, final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<BindResult> resultHandler) throws UnsupportedOperationException {
+            // TODO: all bind types.
+            final AbandonableRequest abReq = new AbandonableRequest(request);
+            requestsInProgress.put(context, abReq);
+            if (request.getAuthenticationType() == LDAP.TYPE_AUTHENTICATION_SASL
+                    && request instanceof GenericBindRequest) {
+                ASN1Reader reader =
+                        ASN1.getReader(((GenericBindRequest) request).getAuthenticationValue());
+                try {
+                    String saslMech = reader.readOctetStringAsString();
+                    ByteString saslCred;
+                    if (reader.hasNextElement()) {
+                        saslCred = reader.readOctetString();
+                    } else {
+                        saslCred = ByteString.empty();
+                    }
+
+                    if (saslServer == null
+                            || !saslServer.getMechanismName().equalsIgnoreCase(saslMech)) {
+                        final Map<String, String> props = new HashMap<>();
+                        props.put(Sasl.QOP, "auth-conf,auth-int,auth");
+                        saslServer =
+                                Sasl.createSaslServer(saslMech, "ldap",
+                                        listener.getHostName(), props,
+                                        new CallbackHandler() {
+                                            @Override
+                                            public void handle(Callback[] callbacks)
+                                                    throws IOException,
+                                                    UnsupportedCallbackException {
+                                                for (final Callback callback : callbacks) {
+                                                    if (callback instanceof NameCallback) {
+                                                        // Do nothing
+                                                    } else if (callback instanceof PasswordCallback) {
+                                                        ((PasswordCallback) callback)
+                                                                .setPassword("password"
+                                                                        .toCharArray());
+                                                    } else if (callback instanceof AuthorizeCallback) {
+                                                        ((AuthorizeCallback) callback)
+                                                                .setAuthorized(true);
+                                                    } else if (callback instanceof RealmCallback) {
+                                                        // Do nothing
+                                                    } else {
+                                                        throw new UnsupportedCallbackException(
+                                                                callback);
+
+                                                    }
+                                                }
+                                            }
+                                        });
+                    }
+
+                    byte[] challenge = saslServer.evaluateResponse(saslCred.toByteArray());
+                    if (saslServer.isComplete()) {
+                        resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS)
+                                .setServerSASLCredentials(ByteString.wrap(challenge)));
+
+                        String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
+                        if ("auth-int".equalsIgnoreCase(qop) || "auth-conf".equalsIgnoreCase(qop)) {
+                            ConnectionSecurityLayer csl = new ConnectionSecurityLayer() {
+                                @Override
+                                public void dispose() {
+                                    try {
+                                        saslServer.dispose();
+                                    } catch (SaslException e) {
+                                        e.printStackTrace();
+                                    }
+                                }
+
+                                @Override
+                                public byte[] unwrap(byte[] incoming, int offset, int len) throws LdapException {
+                                    try {
+                                        return saslServer.unwrap(incoming, offset, len);
+                                    } catch (SaslException e) {
+                                        throw newLdapException(
+                                                Responses.newResult(ResultCode.OPERATIONS_ERROR).setCause(e));
+                                    }
+                                }
+
+                                @Override
+                                public byte[] wrap(byte[] outgoing, int offset, int len) throws LdapException {
+                                    try {
+                                        return saslServer.wrap(outgoing, offset, len);
+                                    } catch (SaslException e) {
+                                        throw newLdapException(
+                                                Responses.newResult(ResultCode.OPERATIONS_ERROR).setCause(e));
+                                    }
+                                }
+                            };
+
+                            clientContext.enableConnectionSecurityLayer(csl);
+                        }
+
+                    } else {
+                        resultHandler.handleResult(Responses.newBindResult(
+                                ResultCode.SASL_BIND_IN_PROGRESS).setServerSASLCredentials(
+                                ByteString.wrap(challenge)));
+                    }
+                } catch (Exception e) {
+                    resultHandler.handleException(newLdapException(Responses
+                            .newBindResult(ResultCode.OPERATIONS_ERROR).setCause(e)
+                            .setDiagnosticMessage(e.toString())));
+                }
+            } else {
+                resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
+            }
+            requestsInProgress.remove(context);
+        }
+
+        @Override
+        public void handleConnectionClosed(final Integer context, final UnbindRequest request) {
+            close();
+        }
+
+        private void close() {
+            if (saslServer != null) {
+                try {
+                    saslServer.dispose();
+                } catch (SaslException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        @Override
+        public void handleConnectionDisconnected(ResultCode resultCode, String message) {
+            close();
+        }
+
+        @Override
+        public void handleConnectionError(final Throwable error) {
+            close();
+        }
+
+        @Override
+        public void handleCompare(final Integer context, final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<CompareResult> resultHandler)
+                throws UnsupportedOperationException {
+            CompareResult result = null;
+            final AbandonableRequest abReq = new AbandonableRequest(request);
+            requestsInProgress.put(context, abReq);
+            // Get the DN.
+            final DN dn = request.getName();
+            if (!entryMap.containsKey(dn)) {
+                // entry not found.
+                result = Responses.newCompareResult(ResultCode.NO_SUCH_ATTRIBUTE);
+                resultHandler.handleException(newLdapException(result));
+                // doesn't matter if it was canceled.
+                requestsInProgress.remove(context);
+                return;
+            }
+
+            // Get the entry.
+            final Entry entry = entryMap.get(dn);
+            final AttributeDescription attrDesc = request.getAttributeDescription();
+            for (final Attribute attr : entry.getAllAttributes(attrDesc)) {
+                final Iterator<ByteString> it = attr.iterator();
+                while (it.hasNext()) {
+                    final ByteString s = it.next();
+                    if (abReq.isCanceled()) {
+                        final Result r = Responses.newResult(ResultCode.CANCELLED);
+                        resultHandler.handleException(newLdapException(r));
+                        requestsInProgress.remove(context);
+                        return;
+                    }
+                    if (s.equals(request.getAssertionValue())) {
+                        result = Responses.newCompareResult(ResultCode.COMPARE_TRUE);
+                        resultHandler.handleResult(result);
+                        requestsInProgress.remove(context);
+                        return;
+                    }
+                }
+            }
+            result = Responses.newCompareResult(ResultCode.COMPARE_FALSE);
+            resultHandler.handleResult(result);
+            requestsInProgress.remove(context);
+        }
+
+        @Override
+        public void handleDelete(final Integer context, final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> handler) throws UnsupportedOperationException {
+            Result result = null;
+            final AbandonableRequest abReq = new AbandonableRequest(request);
+            requestsInProgress.put(context, abReq);
+            // Get the DN.
+            final DN dn = request.getName();
+            if (!entryMap.containsKey(dn)) {
+                // entry is not found.
+                result = Responses.newResult(ResultCode.NO_SUCH_OBJECT);
+                handler.handleException(newLdapException(result));
+                // doesn't matter if it was canceled.
+                requestsInProgress.remove(context);
+                return;
+            }
+
+            if (abReq.isCanceled()) {
+                result = Responses.newResult(ResultCode.CANCELLED);
+                handler.handleException(newLdapException(result));
+                requestsInProgress.remove(context);
+                return;
+            }
+            // Remove this from the map.
+            entryMap.remove(dn);
+            requestsInProgress.remove(context);
+            result = Responses.newResult(ResultCode.SUCCESS);
+            handler.handleResult(result);
+        }
+
+        @Override
+        public <R extends ExtendedResult> void handleExtendedRequest(final Integer context,
+                final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<R> resultHandler) throws UnsupportedOperationException {
+            if (request.getOID().equals(StartTLSExtendedRequest.OID)) {
+                final R result = request.getResultDecoder().newExtendedErrorResult(ResultCode.SUCCESS, "", "");
+                resultHandler.handleResult(result);
+                clientContext.enableTLS(sslContext, null, sslContext.getSocketFactory()
+                        .getSupportedCipherSuites(), false, false);
+            }
+        }
+
+        @Override
+        public void handleModify(final Integer context, final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            // TODO:
+        }
+
+        @Override
+        public void handleModifyDN(final Integer context, final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            // TODO
+        }
+
+        @Override
+        public void handleSearch(final Integer context, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
+            final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            Result result = null;
+            final AbandonableRequest abReq = new AbandonableRequest(request);
+            requestsInProgress.put(context, abReq);
+            // Get the DN.
+            final DN dn = request.getName();
+            if (!entryMap.containsKey(dn)) {
+                // Entry not found.
+                result = Responses.newResult(ResultCode.NO_SUCH_OBJECT);
+                resultHandler.handleException(newLdapException(result));
+                // Should searchResultHandler handle anything?
+
+                // doesn't matter if it was canceled.
+                requestsInProgress.remove(context);
+                return;
+            }
+
+            if (abReq.isCanceled()) {
+                result = Responses.newResult(ResultCode.CANCELLED);
+                resultHandler.handleException(newLdapException(result));
+                requestsInProgress.remove(context);
+                return;
+            }
+
+            final SearchResultEntry e = Responses.newSearchResultEntry(new LinkedHashMapEntry(entryMap.get(dn)));
+            // Check we have had any controls in the request.
+            for (final Control control : request.getControls()) {
+                if (control.getOID().equals(AccountUsabilityRequestControl.OID)) {
+                    e.addControl(AccountUsabilityResponseControl.newControl(false, false, false, 10, false, 0));
+                }
+            }
+            entryHandler.handleEntry(e);
+            result = Responses.newResult(ResultCode.SUCCESS);
+            resultHandler.handleResult(result);
+            requestsInProgress.remove(context);
+        }
+    }
+
+    /** The mapping between entry DNs and the corresponding entries. */
+    private final ConcurrentHashMap<DN, Entry> entryMap = new ConcurrentHashMap<>();
+    /** The LDAP listener. */
+    private LDAPListener listener;
+    /** Whether the server is running. */
+    private volatile boolean isRunning;
+
+    /**
+     * The mapping between the message id and the requests the server is
+     * currently handling.
+     */
+    private final ConcurrentHashMap<Integer, AbandonableRequest> requestsInProgress = new ConcurrentHashMap<>();
+
+    private SSLContext sslContext;
+
+    private LDAPServer() {
+        // Add the root dse first.
+        entryMap.put(DN.rootDN(), Entries.unmodifiableEntry(new LinkedHashMapEntry()));
+        for (int i = 0; i < 1000; i++) {
+            final String dn = String.format("uid=user.%d,ou=people,o=test", i);
+            final String cn = String.format("cn: user.%d", i);
+            final String sn = String.format("sn: %d", i);
+            final String uid = String.format("uid: user.%d", i);
+
+            // See
+            // org.forgerock.opendj.ldap.ConnectionFactoryTestCase.testSchemaUsage().
+            final String mail = String.format("mail: user.%d@example.com", i);
+
+            final DN d = DN.valueOf(dn);
+            final Entry e =
+                    new LinkedHashMapEntry("dn: " + dn, "objectclass: person",
+                            "objectclass: inetorgperson", "objectclass: top", cn, sn, uid, mail);
+            entryMap.put(d, Entries.unmodifiableEntry(e));
+        }
+    }
+
+    @Override
+    public ServerConnection<Integer> handleAccept(final LDAPClientContext context) {
+        return new LDAPServerConnection(context);
+    }
+
+    /**
+     * Returns whether the server is running or not.
+     *
+     * @return Whether the server is running.
+     */
+    public boolean isRunning() {
+        return isRunning;
+    }
+
+    /**
+     * Starts the server.
+     *
+     * @exception Exception
+     */
+    public synchronized void start() throws Exception {
+        if (isRunning) {
+            return;
+        }
+        sslContext = new SSLContextBuilder().getSSLContext();
+        listener = new LDAPListener(findFreeSocketAddress(), getInstance(),
+                        Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096));
+        isRunning = true;
+    }
+
+    /**
+     * Stops the server.
+     */
+    public synchronized void stop() {
+        if (!isRunning) {
+            return;
+        }
+        listener.close();
+        isRunning = false;
+    }
+
+    /**
+     * Returns the socket address of the server.
+     *
+     * @return The socket address of the server.
+     */
+    public synchronized InetSocketAddress getSocketAddress() {
+        if (!isRunning) {
+            throw new IllegalStateException("Server is not running");
+        }
+        return listener.getSocketAddress();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java
new file mode 100644
index 0000000..a316d9b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java
@@ -0,0 +1,184 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This class defines a set of tests for the org.forgerock.opendj.ldap.LDAPUrl
+ * class.
+ */
+public class LDAPUrlTestCase extends SdkTestCase {
+    /**
+     * LDAPUrl encoding test data provider.
+     *
+     * @return The array of test encoding of LDAP URL strings.
+     */
+    @DataProvider(name = "ldapurls")
+    public Object[][] createEncodingData() {
+        return new Object[][] {
+            { "ldap://", "ldap://", true },
+            { "ldap:///", "ldap:///", true },
+            { "ldap://ldap.example.net", "ldap://ldap.example.net", true },
+            { "ldap://ldap.example.net/", "ldap://ldap.example.net/", true },
+            { "ldap://ldap.example.net/?", "ldap://ldap.example.net/?", true },
+            { "ldap:///o=University of Michigan,c=US", "ldap:///o=University%20of%20Michigan,c=US",
+                true },
+            { "ldap://ldap1.example.net/o=University of Michigan,c=US",
+                "ldap://ldap1.example.net/o=University%20of%20Michigan,c=US", true },
+            { "ldap://ldap1.example.net/o=University of Michigan,c=US?postalAddress",
+                "ldap://ldap1.example.net/o=University%20of%20Michigan,c=US?postalAddress", true },
+            {
+                "ldap://ldap1.example.net:6666/o=University of Michigan,c=US??sub?(cn=Babs Jensen)",
+                "ldap://ldap1.example.net:6666/o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen)",
+                true },
+            { "LDAP://ldap1.example.com/c=GB?objectClass?ONE",
+                "LDAP://ldap1.example.com/c=GB?objectClass?ONE", true },
+            // { "ldap://ldap2.example.com/o=Question?,c=US?mail",
+            // "ldap://ldap2.example.com/o=Question%3f,c=US?mail",true },
+            { "ldap://ldap3.example.com/o=Babsco,c=US???(four-octet=\00\00\00\04)",
+                "ldap://ldap3.example.com/o=Babsco,c=US???(four-octet=%5c00%5c00%5c00%5c04)", true },
+            { "ldap://ldap.example.com/o=An Example\\2C Inc.,c=US",
+                "ldap://ldap.example.com/o=An%20Example%5C2C%20Inc.,c=US", true },
+            { "ldap:///", "ldap:///", true }, { "ldap:///", "ldap:///", true },
+            { "ldap:///", "ldap:///", true }, };
+    }
+
+    /**
+     * LDAPUrl construction test data provider.
+     *
+     * @return The array of test construction of LDAPUrl objects.
+     */
+    @DataProvider(name = "urlobjects1")
+    public Object[][] createURLObjects1() {
+        return new Object[][] {
+            { new LDAPUrl(false, null, null, null, null, null), "ldap:///???" },
+            { new LDAPUrl(true, null, null, null, null, null), "ldaps:///???" },
+            { new LDAPUrl(true, "void.central.sun.com", null, null, null, null),
+                "ldaps://void.central.sun.com/???" },
+            { new LDAPUrl(true, null, 1245, null, null, null), "ldaps://:1245/???" },
+            { new LDAPUrl(true, "void.central", 123, null, null, null),
+                "ldaps://void.central:123/???" },
+            { new LDAPUrl(true, null, null, null, null, null, "cn", "sn"), "ldaps:///?cn,sn??" },
+            {
+                new LDAPUrl(true, null, null, null, null, Filter.equality("uid",
+                        "abc"), "cn"), "ldaps:///?cn??(uid=abc)" },
+            {
+                new LDAPUrl(true, null, null, null, SearchScope.WHOLE_SUBTREE, Filter
+                        .equality("uid", "abc"), "cn"), "ldaps:///?cn?sub?(uid=abc)" },
+            {
+                new LDAPUrl(true, null, null, DN.valueOf("uid=abc,o=target"),
+                        SearchScope.WHOLE_SUBTREE, Filter.equality("uid", "abc"),
+                        "cn"), "ldaps:///uid=abc,o=target?cn?sub?(uid=abc)" },
+            {
+                new LDAPUrl(true, "localhost", 1345, DN.valueOf("uid=abc,o=target"),
+                        SearchScope.WHOLE_SUBTREE, Filter.equality("uid", "abc"),
+                        "cn"), "ldaps://localhost:1345/uid=abc,o=target?cn?sub?(uid=abc)" }, };
+    }
+
+    /**
+     * LDAPUrl construction test data provider.
+     *
+     * @return The array of test construction of LDAPUrl objects.
+     */
+    @DataProvider(name = "urlobjects2")
+    public Object[][] createURLObjects2() {
+        return new Object[][] {
+            { new LDAPUrl(false, null, null, null, null, null), LDAPUrl.valueOf("ldap:///") },
+            { new LDAPUrl(true, null, null, null, null, null), LDAPUrl.valueOf("ldaps:///") },
+            { new LDAPUrl(true, "void.central.sun.com", null, null, null, null),
+                LDAPUrl.valueOf("ldaps://void.central.sun.com") },
+            { new LDAPUrl(true, null, 1245, null, null, null), LDAPUrl.valueOf("ldaps://:1245") },
+            { new LDAPUrl(true, "void.central", 123, null, null, null),
+                LDAPUrl.valueOf("ldaps://void.central:123") },
+            { new LDAPUrl(true, null, null, null, null, null, "cn", "sn"),
+                LDAPUrl.valueOf("ldaps:///?cn,sn??") },
+            {
+                new LDAPUrl(true, null, null, null, null, Filter.equality("uid",
+                        "abc"), "cn"), LDAPUrl.valueOf("ldaps:///?cn??(uid=abc)") },
+            {
+                new LDAPUrl(true, null, null, null, SearchScope.WHOLE_SUBTREE, Filter
+                        .equality("uid", "abc"), "cn"),
+                LDAPUrl.valueOf("ldaps:///?cn?sub?(uid=abc)") },
+            {
+                new LDAPUrl(true, null, null, DN.valueOf("uid=abc,o=target"),
+                        SearchScope.WHOLE_SUBTREE, Filter.equality("uid", "abc"),
+                        "cn"), LDAPUrl.valueOf("ldaps:///uid=abc,o=target?cn?sub?(uid=abc)") },
+            {
+                new LDAPUrl(true, "localhost", 1345, DN.valueOf("uid=abc,o=target"),
+                        SearchScope.WHOLE_SUBTREE, Filter.equality("uid", "abc"),
+                        "cn"),
+                LDAPUrl.valueOf("ldaps://localhost:1345/uid=abc,o=target?cn?sub?(uid=abc)") }, };
+    }
+
+    /**
+     * Tests equals method of the LDAP URL.
+     *
+     * @param urlObj1
+     *            The LDAPUrl object.
+     * @param urlObj2
+     *            The LDAPUrl object.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "urlobjects2")
+    public void testLDAPURLCtor(final LDAPUrl urlObj1, final LDAPUrl urlObj2) throws Exception {
+        assertTrue(urlObj1.equals(urlObj2));
+    }
+
+    /**
+     * Test Whether the LDAP URL (non-encoded) is constructed properly from the
+     * arguments.
+     *
+     * @param urlObj
+     *            The LDAPUrl object.
+     * @param urlString
+     *            The non-encoded ldap url.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "urlobjects1")
+    public void testLDAPURLCtor(final LDAPUrl urlObj, final String urlString) throws Exception {
+        assertEquals(urlString, urlObj.toString());
+    }
+
+    /**
+     * Test the LDAP URL encoding.
+     *
+     * @param toEncode
+     *            The URL that needs encoding.
+     * @param encoded
+     *            The encoded URL.
+     * @param valid
+     *            if the encoding is valid.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "ldapurls")
+    public void testURLEncoding(final String toEncode, final String encoded, final boolean valid)
+            throws Exception {
+        final LDAPUrl url1 = LDAPUrl.valueOf(toEncode);
+        final LDAPUrl url2 = LDAPUrl.valueOf(encoded);
+        if (valid) {
+            assertTrue(url1.equals(url2));
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LinkedAttributeTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LinkedAttributeTestCase.java
new file mode 100644
index 0000000..114a0a8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LinkedAttributeTestCase.java
@@ -0,0 +1,421 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Test {@code BasicAttribute}.
+ */
+
+@SuppressWarnings("javadoc")
+public final class LinkedAttributeTestCase extends SdkTestCase {
+    @Test
+    public void smokeTest() throws Exception {
+        // TODO: write a proper test suite.
+        final AbstractAttribute attribute =
+                new LinkedAttribute(AttributeDescription.valueOf("ALTSERVER", Schema
+                        .getCoreSchema()));
+
+        attribute.add(1);
+        attribute.add("a value");
+        attribute.add(ByteString.valueOfUtf8("another value"));
+
+        Assert.assertTrue(attribute.contains(1));
+        Assert.assertTrue(attribute.contains("a value"));
+        Assert.assertTrue(attribute.contains(ByteString.valueOfUtf8("another value")));
+
+        Assert.assertEquals(attribute.size(), 3);
+        Assert.assertTrue(attribute.remove(1));
+        Assert.assertEquals(attribute.size(), 2);
+        Assert.assertFalse(attribute.remove("a missing value"));
+        Assert.assertEquals(attribute.size(), 2);
+        Assert.assertTrue(attribute.remove("a value"));
+        Assert.assertEquals(attribute.size(), 1);
+        Assert.assertTrue(attribute.remove(ByteString.valueOfUtf8("another value")));
+        Assert.assertEquals(attribute.size(), 0);
+    }
+
+    @Test
+    public void testAdd() {
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertTrue(a.add(ByteString.valueOfUtf8("value1")));
+        Assert.assertFalse(a.add(ByteString.valueOfUtf8("value1")));
+        Assert.assertTrue(a.add(ByteString.valueOfUtf8("value2")));
+        Assert.assertFalse(a.add(ByteString.valueOfUtf8("value2")));
+        Assert.assertTrue(a.add(ByteString.valueOfUtf8("value3")));
+        Assert.assertFalse(a.add(ByteString.valueOfUtf8("value3")));
+        Assert.assertEquals(a.size(), 3);
+        Iterator<ByteString> i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertFalse(i.hasNext());
+    }
+
+    @Test
+    public void testAddAll() {
+        // addAll to an empty attribute.
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertFalse(a.addAll(Collections.<ByteString> emptyList(), null));
+        Iterator<ByteString> i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test");
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value1")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test");
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+
+        // addAll to a single-valued attribute.
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(a.addAll(Collections.<ByteString> emptyList(), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value2")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value2"), ByteString
+                .valueOfUtf8("value3")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertFalse(i.hasNext());
+
+        // addAll to a multi-valued attribute.
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(a.addAll(Collections.<ByteString> emptyList(), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value3")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        Assert.assertTrue(a.addAll(Arrays.asList(ByteString.valueOfUtf8("value3"), ByteString
+                .valueOfUtf8("value4")), null));
+        i = a.iterator();
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value4"));
+        Assert.assertFalse(i.hasNext());
+    }
+
+    @Test
+    public void testClear() {
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertTrue(a.isEmpty());
+        Assert.assertEquals(a.size(), 0);
+        a.clear();
+        Assert.assertTrue(a.isEmpty());
+        Assert.assertEquals(a.size(), 0);
+
+        a.add(ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(a.isEmpty());
+        Assert.assertEquals(a.size(), 1);
+        a.clear();
+        Assert.assertTrue(a.isEmpty());
+        Assert.assertEquals(a.size(), 0);
+
+        a.add(ByteString.valueOfUtf8("value1"));
+        a.add(ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(a.isEmpty());
+        Assert.assertEquals(a.size(), 2);
+        a.clear();
+        Assert.assertTrue(a.isEmpty());
+        Assert.assertEquals(a.size(), 0);
+
+        a.add(ByteString.valueOfUtf8("value1"));
+        a.add(ByteString.valueOfUtf8("value2"));
+        a.add(ByteString.valueOfUtf8("value3"));
+        Assert.assertFalse(a.isEmpty());
+        Assert.assertEquals(a.size(), 3);
+        a.clear();
+        Assert.assertTrue(a.isEmpty());
+        Assert.assertEquals(a.size(), 0);
+    }
+
+    @Test
+    public void testContains() {
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertFalse(a.contains(ByteString.valueOfUtf8("value4")));
+
+        a.add(ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value1")));
+        Assert.assertFalse(a.contains(ByteString.valueOfUtf8("value4")));
+
+        a.add(ByteString.valueOfUtf8("value2"));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value1")));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value2")));
+        Assert.assertFalse(a.contains(ByteString.valueOfUtf8("value4")));
+
+        a.add(ByteString.valueOfUtf8("value3"));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value1")));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value2")));
+        Assert.assertTrue(a.contains(ByteString.valueOfUtf8("value3")));
+        Assert.assertFalse(a.contains(ByteString.valueOfUtf8("value4")));
+    }
+
+    @Test
+    public void testContainsAll() {
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertTrue(a.containsAll(Collections.<ByteString> emptyList()));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"))));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"))));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"))));
+
+        a.add(ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.containsAll(Collections.<ByteString> emptyList()));
+        Assert.assertTrue(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"))));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"))));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"))));
+
+        a.add(ByteString.valueOfUtf8("value2"));
+        Assert.assertTrue(a.containsAll(Collections.<ByteString> emptyList()));
+        Assert.assertTrue(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"))));
+        Assert.assertTrue(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"))));
+        Assert.assertFalse(a.containsAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"))));
+    }
+
+    @Test
+    public void testFirstValue() {
+        Attribute a = new LinkedAttribute("test");
+        try {
+            a.firstValue();
+            Assert.fail("Expected NoSuchElementException");
+        } catch (NoSuchElementException e) {
+            // Expected.
+        }
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(a.firstValue(), ByteString.valueOfUtf8("value1"));
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(a.firstValue(), ByteString.valueOfUtf8("value1"));
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value2"), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(a.firstValue(), ByteString.valueOfUtf8("value2"));
+    }
+
+    @Test
+    public void testGetAttributeDescription() {
+        AttributeDescription ad = AttributeDescription.valueOf("test");
+        Attribute a = new LinkedAttribute(ad);
+        Assert.assertEquals(a.getAttributeDescription(), ad);
+    }
+
+    @Test
+    public void testIterator() {
+        Attribute a = new LinkedAttribute("test");
+        Iterator<ByteString> i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+    }
+
+    @Test
+    public void testRemove() {
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertFalse(a.remove(ByteString.valueOfUtf8("value1")));
+        Iterator<ByteString> i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(a.remove(ByteString.valueOfUtf8("value2")));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(i.hasNext());
+        Assert.assertTrue(a.remove(ByteString.valueOfUtf8("value1")));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(a.remove(ByteString.valueOfUtf8("value3")));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+        Assert.assertTrue(a.remove(ByteString.valueOfUtf8("value1")));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertFalse(i.hasNext());
+        Assert.assertTrue(a.remove(ByteString.valueOfUtf8("value2")));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+    }
+
+    @Test
+    public void testRemoveAll() {
+        // removeAll from an empty attribute.
+        Attribute a = new LinkedAttribute("test");
+        Assert.assertFalse(a.removeAll(Collections.<ByteString> emptyList(), null));
+        Iterator<ByteString> i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test");
+        Assert.assertFalse(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1")), null));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test");
+        Assert.assertFalse(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"))));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        // removeAll from single-valued attribute.
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(a.removeAll(Collections.<ByteString> emptyList(), null));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1")), null));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a = new LinkedAttribute("test", ByteString.valueOfUtf8("value1"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"))));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        // removeAll from multi-valued attribute.
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertFalse(a.removeAll(Collections.<ByteString> emptyList(), null));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value1"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value4"));
+        Assert.assertFalse(i.hasNext());
+
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1")), null));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value2"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value4"));
+        Assert.assertFalse(i.hasNext());
+
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2")), null));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value3"));
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value4"));
+        Assert.assertFalse(i.hasNext());
+
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3")), null));
+        i = a.iterator();
+        Assert.assertTrue(i.hasNext());
+        Assert.assertEquals(i.next(), ByteString.valueOfUtf8("value4"));
+        Assert.assertFalse(i.hasNext());
+
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString.valueOfUtf8("value4")),
+                null));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+
+        a =
+                new LinkedAttribute("test", ByteString.valueOfUtf8("value1"), ByteString
+                        .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString
+                        .valueOfUtf8("value4"));
+        Assert.assertTrue(a.removeAll(Arrays.asList(ByteString.valueOfUtf8("value1"), ByteString
+                .valueOfUtf8("value2"), ByteString.valueOfUtf8("value3"), ByteString.valueOfUtf8("value4"),
+                ByteString.valueOfUtf8("value5")), null));
+        i = a.iterator();
+        Assert.assertFalse(i.hasNext());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LoadBalancerTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LoadBalancerTestCase.java
new file mode 100644
index 0000000..da0a911
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LoadBalancerTestCase.java
@@ -0,0 +1,179 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.util.Arrays.asList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_EVENT_LISTENER;
+import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL;
+import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_SCHEDULER;
+import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.forgerock.util.promise.Promises.newExceptionPromise;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.logging.Level;
+
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.Promise;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class LoadBalancerTestCase extends SdkTestCase {
+    private static ConnectionFactory mockAsync(final ConnectionFactory mock) {
+        return new ConnectionFactory() {
+            @Override
+            public void close() {
+                mock.close();
+            }
+
+            @Override
+            public Connection getConnection() throws LdapException {
+                return mock.getConnection();
+            }
+
+            @Override
+            public Promise<Connection, LdapException> getConnectionAsync() {
+                try {
+                    return newResultPromise(mock.getConnection());
+                } catch (final LdapException e) {
+                    return newExceptionPromise(e);
+                }
+            }
+
+            @Override
+            public String toString() {
+                return mock.toString();
+            }
+        };
+    }
+
+    /**
+     * Disables logging before the tests.
+     */
+    @BeforeClass
+    public void disableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.SEVERE);
+    }
+
+    /**
+     * Re-enable logging after the tests.
+     */
+    @AfterClass
+    public void enableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.INFO);
+    }
+
+    /**
+     * Tests fix for OPENDJ-1112: when a load balancer fails completely the
+     * connection exception should include the last error that occurred.
+     */
+    @Test
+    public void testFinalFailureExposedAsCause() throws Exception {
+        /*
+         * Create load-balancer with two failed connection factories.
+         */
+        final ConnectionFactory first = mock(ConnectionFactory.class, "first");
+        final LdapException firstError = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        when(first.getConnection()).thenThrow(firstError);
+
+        final ConnectionFactory second = mock(ConnectionFactory.class, "second");
+        final LdapException secondError = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        when(second.getConnection()).thenThrow(secondError);
+
+        final ConnectionFactory loadBalancer = newRoundRobinLoadBalancer(asList(first, second), defaultOptions());
+
+        /*
+         * Belt and braces check to ensure that factory methods don't return
+         * same instance and fool this test.
+         */
+        assertThat(firstError).isNotSameAs(secondError);
+
+        try {
+            loadBalancer.getConnection().close();
+            fail("Unexpectedly obtained a connection");
+        } catch (final LdapException e) {
+            assertThat(e.getCause()).isSameAs(secondError);
+        } finally {
+            loadBalancer.close();
+        }
+    }
+
+    /**
+     * Tests fix for OPENDJ-1112: event listener should be notified when a
+     * factory changes state from online to offline or vice versa.
+     */
+    @Test
+    public void testLoadBalancerEventListenerNotification() throws Exception {
+        /*
+         * Create load-balancer with two failed connection factories and a mock
+         * listener and scheduler. The first factory will succeed on the second
+         * attempt.
+         */
+        final ConnectionFactory first = mock(ConnectionFactory.class, "first");
+        final ConnectionFactory firstAsync = mockAsync(first);
+        final LdapException firstError = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        when(first.getConnection()).thenThrow(firstError).thenReturn(mock(Connection.class));
+
+        final ConnectionFactory second = mock(ConnectionFactory.class, "second");
+        final ConnectionFactory secondAsync = mockAsync(second);
+        final LdapException secondError = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        when(second.getConnection()).thenThrow(secondError);
+
+        final LoadBalancerEventListener listener = mock(LoadBalancerEventListener.class);
+        final MockScheduler scheduler = new MockScheduler();
+        final Options options = defaultOptions()
+                                   .set(LOAD_BALANCER_EVENT_LISTENER, listener)
+                                   .set(LOAD_BALANCER_MONITORING_INTERVAL, duration("1 second"))
+                                   .set(LOAD_BALANCER_SCHEDULER, scheduler);
+        final ConnectionFactory loadBalancer = newRoundRobinLoadBalancer(asList(firstAsync, secondAsync), options);
+
+        /*
+         * Belt and braces check to ensure that factory methods don't return
+         * same instance and fool this test.
+         */
+        assertThat(firstError).isNotSameAs(secondError);
+
+        try {
+            loadBalancer.getConnection().close();
+            fail("Unexpectedly obtained a connection");
+        } catch (final LdapException e) {
+            // Check that the event listener has been fired for both factories.
+            verify(listener).handleConnectionFactoryOffline(firstAsync, firstError);
+            verify(listener).handleConnectionFactoryOffline(secondAsync, secondError);
+            verifyNoMoreInteractions(listener);
+
+            // Check that the factories are being monitored.
+            assertThat(scheduler.isScheduled()).isTrue();
+
+            // Forcefully run the monitor task and check that the first factory is online.
+            scheduler.runFirstTask();
+            verify(listener).handleConnectionFactoryOnline(firstAsync);
+            verifyNoMoreInteractions(listener);
+        } finally {
+            loadBalancer.close();
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
new file mode 100644
index 0000000..f32c608
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
@@ -0,0 +1,673 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.Connections.*;
+import static org.forgerock.opendj.ldap.requests.Requests.*;
+import static org.forgerock.opendj.ldif.LDIFEntryReader.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Memory backend tests.
+ */
+@SuppressWarnings("javadoc")
+public class MemoryBackendTestCase extends SdkTestCase {
+    private int numberOfEntriesInBackend;
+
+    @Test
+    public void testAdd() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        connection.add(newDomain);
+        assertThat(connection.readEntry("dc=new domain,dc=com")).isEqualTo(newDomain);
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddAlreadyExists() throws Exception {
+        final Connection connection = getConnection();
+        connection.add(valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                "objectClass: top", "dc: example"));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testAddNoParent() throws Exception {
+        final Connection connection = getConnection();
+        connection.add(valueOfLDIFEntry("dn: dc=new domain,dc=missing,dc=com",
+                "objectClass: domain", "objectClass: top", "dc: new domain"));
+    }
+
+    @Test
+    public void testAddPostRead() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        assertThat(
+                connection.add(
+                        newAddRequest(newDomain)
+                                .addControl(PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(newDomain);
+    }
+
+    @Test(expectedExceptions = LdapException.class)
+    public void testAddPreRead() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        connection.add(newAddRequest(newDomain).addControl(PreReadRequestControl.newControl(true)));
+    }
+
+    @Test
+    public void testCompareFalse() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.compare("dc=example,dc=com", "objectclass", "person").matched())
+                .isFalse();
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testCompareNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.compare("uid=missing,ou=people,dc=example,dc=com", "uid", "missing");
+    }
+
+    @Test
+    public void testCompareTrue() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.compare("dc=example,dc=com", "objectclass", "domain").matched())
+                .isTrue();
+    }
+
+    @Test(expectedExceptions = AssertionFailureException.class)
+    public void testDeleteAssertionFalse() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
+    }
+
+    @Test
+    public void testDeleteAssertionTrue() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testDeleteNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete("uid=missing,ou=people,dc=example,dc=com");
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testDeleteOnLeaf() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete("uid=test1,ou=people,dc=example,dc=com");
+        connection.readEntry("uid=test1,ou=people,dc=example,dc=com");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testDeleteOnNonLeaf() throws Exception {
+        final Connection connection = getConnection();
+        try {
+            connection.delete("dc=example,dc=com");
+        } finally {
+            assertThat(connection.readEntry("dc=example,dc=com")).isNotNull();
+        }
+    }
+
+    @Test(expectedExceptions = LdapException.class)
+    public void testDeletePostRead() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                PostReadRequestControl.newControl(true)));
+    }
+
+    @Test
+    public void testDeletePreRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.delete(
+                        newDeleteRequest("dc=xxx,dc=com").addControl(
+                                PreReadRequestControl.newControl(true))).getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
+                        "dc: xxx"));
+    }
+
+    @DataProvider
+    public Object[][] deleteSubtreeData() {
+        return new Object[][] {
+            { "dc=example,dc=com" },
+            { "ou=people,dc=example,dc=com" },
+            { "uid=test1,ou=people,dc=example,dc=com" },
+            { "uid=test2,ou=people,dc=example,dc=com" },
+        };
+    }
+
+    @Test(dataProvider = "deleteSubtreeData", expectedExceptions = EntryNotFoundException.class)
+    public void testDeleteSubtree(final String name) throws Exception {
+        final Connection connection = getConnection();
+        connection.deleteSubtree("dc=example,dc=com");
+        try {
+            connection.readEntry(name);
+        } finally {
+            assertThat(connection.readEntry("dc=xxx,dc=com")).isNotNull();
+        }
+    }
+
+    @Test
+    public void testModify() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: description",
+                "description: test description");
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example", "description: test description"));
+    }
+
+    @Test(expectedExceptions = AssertionFailureException.class)
+    public void testModifyAssertionFalse() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: description", "description: test description").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
+    }
+
+    @Test
+    public void testModifyAssertionTrue() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: description", "description: test description").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
+    }
+
+    @Test
+    public void testModifyIncrement() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: 100", "-", "increment: integer", "integer: 10");
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example", "integer: 110"));
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyIncrementBadDelta() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: 100", "-", "increment: integer", "integer: nan");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyIncrementBadValue() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: nan", "-", "increment: integer", "integer: 10");
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testModifyNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=missing,dc=com", "changetype: modify", "add: description",
+                "description: test description");
+    }
+
+    @Test
+    public void testModifyPermissiveWithDuplicateValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: dc", "dc: example").addControl(
+                PermissiveModifyRequestControl.newControl(true)));
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test
+    public void testModifyPermissiveWithMissingValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "delete: dc", "dc: xxx")
+                .addControl(PermissiveModifyRequestControl.newControl(true)));
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test
+    public void testModifyPostRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                                "objectClass: top", "dc: example", "description: test description"));
+    }
+
+    @Test
+    public void testModifyPostReadAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PostReadRequestControl.newControl(true, "dc", "entryDN")))
+                        .getControl(PostReadResponseControl.DECODER, new DecodeOptions())
+                        .getEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "dc: example",
+                        "entryDN: dc=example,dc=com"));
+    }
+
+    @Test
+    public void testModifyPreReadAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PreReadRequestControl.newControl(true, "dc", "entryDN")))
+                        .getControl(PreReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: dc=example,dc=com", "dc: example",
+                                "entryDN: dc=example,dc=com"));
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyStrictWithDuplicateValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: dc", "dc: example");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyStrictWithMissingValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "delete: dc", "dc: xxx");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyStrictWithMissingAttribute() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "delete: cn");
+    }
+
+    @Test
+    public void testSearchAttributesOperational() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "+")).isEqualTo(
+                valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com",
+                        "entryDN: uid=test1,ou=people,dc=example,dc=com",
+                        "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c"));
+    }
+
+    @Test
+    public void testSearchAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "uid", "entryDN"))
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "uid: test1",
+                                "entryDN: uid=test1,ou=People,dc=example,dc=com"));
+    }
+
+    @Test
+    public void testSearchAttributesSelectedTypesOnly() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.searchSingleEntry(Requests.newSearchRequest(
+                        "uid=test1,ou=People,dc=example,dc=com", SearchScope.BASE_OBJECT,
+                        "(objectClass=*)", "uid", "entryDN").setTypesOnly(true))).isEqualTo(
+                new LinkedHashMapEntry("uid=test1,ou=People,dc=example,dc=com").addAttribute("uid")
+                        .addAttribute("entryDN"));
+    }
+
+    @Test
+    public void testSearchAttributesRenamed() throws Exception {
+        final Connection connection = getConnection();
+        final Entry entry =
+                connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "commonName",
+                        "ENTRYDN");
+        assertThat(entry)
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com",
+                                "commonName: test user 1",
+                                "ENTRYDN: uid=test1,ou=People,dc=example,dc=com"));
+        assertThat(entry.getAttribute("cn").getAttributeDescriptionAsString()).isEqualTo(
+                "commonName");
+        assertThat(entry.getAttribute("entryDN").getAttributeDescriptionAsString()).isEqualTo(
+                "ENTRYDN");
+    }
+
+    @Test
+    public void testSearchAttributesUser() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "*")).isEqualTo(
+                getUser1Entry());
+    }
+
+    @Test
+    public void testSearchBase() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testSearchBaseNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.readEntry("dc=missing,dc=com");
+    }
+
+    @Test
+    public void testSearchOneLevel() throws Exception {
+        final Connection connection = getConnection();
+        final ConnectionEntryReader reader =
+                connection.search("dc=com", SearchScope.SINGLE_LEVEL, "(objectClass=*)");
+        assertThat(reader.readEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+        assertThat(reader.readEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
+                        "dc: xxx"));
+        assertThat(reader.hasNext()).isFalse();
+    }
+
+    @Test
+    public void testSearchOneLevelWithSizeLimit() throws Exception {
+        final Connection connection = getConnection();
+        final ConnectionEntryReader reader =
+                connection.search(Requests.newSearchRequest("dc=com", SearchScope.SINGLE_LEVEL, "(objectClass=*)").
+                        setSizeLimit(1));
+        assertThat(reader.readEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+        try {
+            reader.hasNext();
+            TestCaseUtils.failWasExpected(LdapException.class);
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED);
+        }
+    }
+
+    @Test
+    public void testSearchSubtree() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(uid=test1)")).
+                isEqualTo(getUser1Entry());
+    }
+
+    @Test
+    public void testSearchSubtreeReturnsAllEntries() throws Exception {
+        final Connection connection = getConnection();
+        Collection<SearchResultEntry> entries = new ArrayList<>();
+        connection.search(Requests.newSearchRequest("dc=com", SearchScope.WHOLE_SUBTREE, "(objectclass=*)"), entries);
+        assertThat(entries).hasSize(numberOfEntriesInBackend);
+    }
+
+    @Test
+    public void testSearchSubordinatesReturnsAllEntries() throws Exception {
+        final Connection connection = getConnection();
+        Collection<SearchResultEntry> entries = new ArrayList<>();
+        connection.search(Requests.newSearchRequest("dc=com", SearchScope.SUBORDINATES, "(objectclass=*)"), entries);
+        assertThat(entries).hasSize(numberOfEntriesInBackend - 1);
+    }
+
+    @Test
+    public void testSearchSubordinatesEntries() throws Exception {
+        int numberOfUsers = 5;
+        final Connection connection = getConnection();
+        Collection<SearchResultEntry> entries = new ArrayList<>();
+        connection.search(Requests.newSearchRequest("ou=People,dc=example,dc=com", SearchScope.SUBORDINATES,
+            "(objectclass=*)"), entries);
+        assertThat(entries).hasSize(numberOfUsers);
+    }
+
+    @Test
+    public void testSearchSubtreeWithSizeLimit() throws Exception {
+        final Connection connection = getConnection();
+        Collection<SearchResultEntry> entries = new ArrayList<>();
+        try {
+            connection.search(
+                    Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=*)").
+                    setSizeLimit(2), entries);
+            TestCaseUtils.failWasExpected(LdapException.class);
+        } catch (LdapException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED);
+            assertThat(entries).hasSize(2);
+        }
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testSearchSubtreeNotFound() throws Exception {
+        final Connection connection = getConnection();
+        connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                "(uid=missing)");
+    }
+
+    @Test
+    public void testSearchPagedResults() throws Exception {
+        final Connection connection = getConnection();
+        final List<SearchResultEntry> entries = new ArrayList<>();
+        final SearchRequest search =
+                Requests.newSearchRequest("ou=people,dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=*)");
+        final DecodeOptions dc = new DecodeOptions();
+
+        // First page.
+        search.addControl(SimplePagedResultsControl.newControl(true, 2, ByteString.empty()));
+        Result result = connection.search(search, entries);
+        assertThat(entries).hasSize(2);
+        assertThat(entries.get(0).getName().toString()).isEqualTo(
+                "uid=test1,ou=People,dc=example,dc=com");
+        assertThat(entries.get(1).getName().toString()).isEqualTo(
+                "uid=test2,ou=People,dc=example,dc=com");
+        SimplePagedResultsControl control =
+                result.getControl(SimplePagedResultsControl.DECODER, dc);
+        assertThat(control).isNotNull();
+        ByteString cookie = control.getCookie();
+        assertThat(cookie).isNotNull();
+        assertThat(cookie.isEmpty()).isFalse();
+        entries.clear();
+        search.getControls().clear();
+
+        // Second page.
+        search.addControl(SimplePagedResultsControl.newControl(true, 2, cookie));
+        result = connection.search(search, entries);
+        assertThat(entries).hasSize(2);
+        assertThat(entries.get(0).getName().toString()).isEqualTo(
+                "uid=test3,ou=People,dc=example,dc=com");
+        assertThat(entries.get(1).getName().toString()).isEqualTo(
+                "uid=test4,ou=People,dc=example,dc=com");
+        control = result.getControl(SimplePagedResultsControl.DECODER, dc);
+        assertThat(control).isNotNull();
+        cookie = control.getCookie();
+        assertThat(cookie).isNotNull();
+        assertThat(cookie.isEmpty()).isFalse();
+        entries.clear();
+        search.getControls().clear();
+
+        // Final page.
+        search.addControl(SimplePagedResultsControl.newControl(true, 2, cookie));
+        result = connection.search(search, entries);
+        assertThat(entries).hasSize(1);
+        assertThat(entries.get(0).getName().toString()).isEqualTo(
+                "uid=test5,ou=People,dc=example,dc=com");
+        control = result.getControl(SimplePagedResultsControl.DECODER, dc);
+        assertThat(control).isNotNull();
+        cookie = control.getCookie();
+        assertThat(cookie).isNotNull();
+        assertThat(cookie.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void testSimpleBind() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=test1,ou=people,dc=example,dc=com", "password".toCharArray());
+    }
+
+    @Test(expectedExceptions = AuthenticationException.class)
+    public void testSimpleBindBadPassword() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=test1,ou=people,dc=example,dc=com", "bad".toCharArray());
+    }
+
+    @Test(expectedExceptions = AuthenticationException.class)
+    public void testSimpleBindNoSuchUser() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=missing,ou=people,dc=example,dc=com", "password".toCharArray());
+    }
+
+    @Test
+    public void testSimpleBindPostRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.bind(
+                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
+                                "password".toCharArray()).addControl(
+                                PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(getUser1Entry());
+    }
+
+    @Test
+    public void testSimpleBindPreRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.bind(
+                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
+                                "password".toCharArray()).addControl(
+                                PreReadRequestControl.newControl(true))).getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
+                getUser1Entry());
+    }
+
+    private Connection getConnection() throws IOException {
+        // @formatter:off
+        String[] ldifEntries = new String[] {
+            "dn: dc=com",
+            "objectClass: domain",
+            "objectClass: top",
+            "dc: com",
+            "",
+            "dn: dc=example,dc=com",
+            "objectClass: domain",
+            "objectClass: top",
+            "dc: example",
+            "entryDN: dc=example,dc=com",
+            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
+            "",
+            "dn: ou=People,dc=example,dc=com",
+            "objectClass: organizationalunit",
+            "objectClass: top",
+            "ou: People",
+            "",
+            "dn: uid=test1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "uid: test1",
+            "userpassword: password",
+            "cn: test user 1",
+            "sn: user 1",
+            "entryDN: uid=test1,ou=people,dc=example,dc=com",
+            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
+            "",
+            "dn: uid=test2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "uid: test2",
+            "userpassword: password",
+            "cn: test user 2",
+            "sn: user 2",
+            "",
+            "dn: uid=test3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "uid: test3",
+            "userpassword: password",
+            "cn: test user 3",
+            "sn: user 3",
+            "",
+            "dn: uid=test4,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "uid: test4",
+            "userpassword: password",
+            "cn: test user 4",
+            "sn: user 4",
+            "",
+            "dn: uid=test5,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "uid: test5",
+            "userpassword: password",
+            "cn: test user 5",
+            "sn: user 5",
+            "",
+            "dn: dc=xxx,dc=com",
+            "objectClass: domain",
+            "objectClass: top",
+            "dc: xxx"
+        };
+        // @formatter:on
+        numberOfEntriesInBackend = getNumberOfEntries(ldifEntries);
+        final MemoryBackend backend = new MemoryBackend(new LDIFEntryReader(ldifEntries));
+        return newInternalConnection(backend);
+    }
+
+    private int getNumberOfEntries(String[] ldifEntries) {
+        int entries = 0;
+        for (String ldifEntry : ldifEntries) {
+            if (ldifEntry.startsWith("dn: ")) {
+                entries++;
+            }
+        }
+        return entries;
+    }
+
+    private Entry getUser1Entry() {
+        return valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "objectClass: top",
+                "objectClass: person", "uid: test1", "userpassword: password", "cn: test user 1",
+                "sn: user 1");
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockConnectionEventListener.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockConnectionEventListener.java
new file mode 100644
index 0000000..aca05b0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockConnectionEventListener.java
@@ -0,0 +1,93 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+
+/** A connection event listener which records events and signals when it has been notified. */
+@SuppressWarnings("javadoc")
+public final class MockConnectionEventListener implements ConnectionEventListener {
+    private final CountDownLatch closedLatch = new CountDownLatch(1);
+    private final CountDownLatch errorLatch = new CountDownLatch(1);
+    private final CountDownLatch notificationLatch = new CountDownLatch(1);
+    private Boolean isDisconnectNotification;
+    private LdapException error;
+    private ExtendedResult notification;
+    private final AtomicInteger invocationCount = new AtomicInteger();
+
+    @Override
+    public void handleConnectionClosed() {
+        invocationCount.incrementAndGet();
+        closedLatch.countDown();
+    }
+
+    @Override
+    public void handleConnectionError(boolean isDisconnectNotification, LdapException error) {
+        this.isDisconnectNotification = isDisconnectNotification;
+        this.error = error;
+        invocationCount.incrementAndGet();
+        errorLatch.countDown();
+    }
+
+    @Override
+    public void handleUnsolicitedNotification(ExtendedResult notification) {
+        this.notification = notification;
+        invocationCount.incrementAndGet();
+        notificationLatch.countDown();
+    }
+
+    public void awaitClose(long timeout, TimeUnit unit) {
+        await(closedLatch, timeout, unit);
+    }
+
+    public void awaitError(long timeout, TimeUnit unit) {
+        await(errorLatch, timeout, unit);
+    }
+
+    public void awaitNotification(long timeout, TimeUnit unit) {
+        await(notificationLatch, timeout, unit);
+    }
+
+    public Boolean isDisconnectNotification() {
+        return isDisconnectNotification;
+    }
+
+    public LdapException getError() {
+        return error;
+    }
+
+    public ExtendedResult getNotification() {
+        return notification;
+    }
+
+    private void await(CountDownLatch latch, long timeout, TimeUnit unit) {
+        try {
+            assertThat(latch.await(timeout, unit)).isTrue();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    public int getInvocationCount() {
+        return invocationCount.get();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockScheduler.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockScheduler.java
new file mode 100644
index 0000000..1de769b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/MockScheduler.java
@@ -0,0 +1,260 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.util.concurrent.Executors.callable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A mock scheduled executor which allows unit tests to directly invoke
+ * scheduled tasks without needing to wait.
+ */
+final class MockScheduler implements ScheduledExecutorService {
+
+    private final class ScheduledCallableFuture<T> implements ScheduledFuture<T>, Callable<T> {
+        private final Callable<T> callable;
+        private final CountDownLatch isDone = new CountDownLatch(1);
+        private final boolean removeAfterCall;
+        private T result;
+
+        private ScheduledCallableFuture(final Callable<T> callable, final boolean removeAfterCall) {
+            this.callable = callable;
+            this.removeAfterCall = removeAfterCall;
+        }
+
+        @Override
+        public T call() throws Exception {
+            result = callable.call();
+            isDone.countDown();
+            if (removeAfterCall) {
+                tasks.remove(this);
+            }
+            return result;
+        }
+
+        @Override
+        public boolean cancel(final boolean mayInterruptIfRunning) {
+            return tasks.remove(this);
+        }
+
+        @Override
+        public int compareTo(final Delayed o) {
+            // Unused.
+            return 0;
+        }
+
+        @Override
+        public T get() throws InterruptedException, ExecutionException {
+            isDone.await();
+            return result;
+        }
+
+        @Override
+        public T get(final long timeout, final TimeUnit unit) throws InterruptedException,
+                ExecutionException, TimeoutException {
+            if (isDone.await(timeout, unit)) {
+                return result;
+            } else {
+                throw new TimeoutException();
+            }
+        }
+
+        @Override
+        public long getDelay(final TimeUnit unit) {
+            // Unused.
+            return 0;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return tasks.contains(this);
+        }
+
+        @Override
+        public boolean isDone() {
+            return isDone.getCount() == 0;
+        }
+
+    }
+
+    /** Saved scheduled tasks. */
+    private final List<Callable<?>> tasks = new CopyOnWriteArrayList<>();
+
+    MockScheduler() {
+        // Nothing to do.
+    }
+
+    @Override
+    public boolean awaitTermination(final long timeout, final TimeUnit unit)
+            throws InterruptedException {
+        // Unused.
+        return false;
+    }
+
+    @Override
+    public void execute(final Runnable command) {
+        // Unused.
+
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(final Collection<? extends Callable<T>> tasks)
+            throws InterruptedException {
+        // Unused.
+        return null;
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(final Collection<? extends Callable<T>> tasks,
+            final long timeout, final TimeUnit unit) throws InterruptedException {
+        // Unused.
+        return null;
+    }
+
+    @Override
+    public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
+            throws InterruptedException, ExecutionException {
+        // Unused.
+        return null;
+    }
+
+    @Override
+    public <T> T invokeAny(final Collection<? extends Callable<T>> tasks, final long timeout,
+            final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        // Unused.
+        return null;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        // Unused.
+        return false;
+    }
+
+    @Override
+    public boolean isTerminated() {
+        // Unused.
+        return false;
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(final Callable<V> callable, final long delay,
+            final TimeUnit unit) {
+        return onceOnly(callable);
+    }
+
+    @Override
+    public ScheduledFuture<?> schedule(final Runnable command, final long delay, final TimeUnit unit) {
+        return onceOnly(callable(command));
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(final Runnable command, final long initialDelay,
+            final long period, final TimeUnit unit) {
+        return repeated(callable(command));
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(final Runnable command,
+            final long initialDelay, final long delay, final TimeUnit unit) {
+        return repeated(callable(command));
+    }
+
+    @Override
+    public void shutdown() {
+        // Unused.
+    }
+
+    @Override
+    public List<Runnable> shutdownNow() {
+        // Unused.
+        return Collections.emptyList();
+    }
+
+    @Override
+    public <T> Future<T> submit(final Callable<T> task) {
+        return onceOnly(task);
+    }
+
+    @Override
+    public Future<?> submit(final Runnable task) {
+        return onceOnly(callable(task));
+    }
+
+    @Override
+    public <T> Future<T> submit(final Runnable task, final T result) {
+        return onceOnly(callable(task, result));
+    }
+
+    List<Callable<?>> getAllTasks() {
+        return tasks;
+    }
+
+    Callable<?> getFirstTask() {
+        return tasks.get(0);
+    }
+
+    boolean isScheduled() {
+        return !tasks.isEmpty();
+    }
+
+    void runAllTasks() {
+        for (final Callable<?> task : tasks) {
+            runTask0(task);
+        }
+    }
+
+    void runFirstTask() {
+        runTask(0);
+    }
+
+    void runTask(final int i) {
+        runTask0(tasks.get(i));
+    }
+
+    private <T> ScheduledCallableFuture<T> onceOnly(final Callable<T> callable) {
+        final ScheduledCallableFuture<T> wrapped = new ScheduledCallableFuture<>(callable, true);
+        tasks.add(wrapped);
+        return wrapped;
+    }
+
+    private <T> ScheduledCallableFuture<T> repeated(final Callable<T> callable) {
+        final ScheduledCallableFuture<T> wrapped = new ScheduledCallableFuture<>(callable, false);
+        tasks.add(wrapped);
+        return wrapped;
+    }
+
+    private void runTask0(final Callable<?> task) {
+        try {
+            task.call();
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ModificationTypeTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ModificationTypeTestCase.java
new file mode 100644
index 0000000..979e2fd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ModificationTypeTestCase.java
@@ -0,0 +1,46 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Iterator;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+public class ModificationTypeTestCase extends SdkTestCase {
+
+    @DataProvider
+    public Iterator<Object[]> valuesDataProvider() {
+        return new DataProviderIterator(ModificationType.values());
+    }
+
+    @Test(dataProvider = "valuesDataProvider")
+    public void valueOfInt(ModificationType val) throws Exception {
+        assertSame(ModificationType.valueOf(val.intValue()), val);
+    }
+
+    @Test
+    public void valueOfIntUnknown() throws Exception {
+        int intValue = -1;
+        ModificationType unknown = ModificationType.valueOf(intValue);
+        assertSame(unknown.intValue(), intValue);
+        assertSame(unknown.asEnum(), ModificationType.Enum.UNKNOWN);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/PackedLongTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/PackedLongTestCase.java
new file mode 100644
index 0000000..9b593d8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/PackedLongTestCase.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.PackedLong;
+
+/**
+ * Test case for PackedLong.
+ */
+@Test(groups = "unit")
+public class PackedLongTestCase extends SdkTestCase {
+
+    @Test(dataProvider = "unsignedLongValues")
+    public void testCanReadWriteByteArray(int size, long value) throws IOException {
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+        PackedLong.writeCompactUnsigned(os, value);
+        assertThat(os.size()).isEqualTo(size);
+
+        assertThat(PackedLong.getEncodedSize(value)).isEqualTo(size);
+        assertThat(PackedLong.readCompactUnsignedLong(new ByteArrayInputStream(os.toByteArray()))).isEqualTo(value);
+    }
+
+    @DataProvider
+    private static Object[][] unsignedLongValues() {
+        return new Object[][] {
+            { 1, 0 }, { 1, 0x7FL },
+            { 2, 0x80L }, { 2, 0x3FFFL },
+            { 3, 0x4000L }, { 3, 0x1FFFFFL },
+            { 4, 0x200000L }, { 4, 0x0FFFFFFFL },
+            { 5, 0x10000000L }, { 5, 0x7FFFFFFFFL },
+            { 6, 0x800000000L }, { 6, 0x3FFFFFFFFFFL },
+            { 7, 0x40000000000L }, { 7, 0x1FFFFFFFFFFFFL },
+            { 8, 0x2000000000000L }, { 8, 0x00FFFFFFFFFFFFFFL }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RDNTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RDNTestCase.java
new file mode 100644
index 0000000..a6d6547
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RDNTestCase.java
@@ -0,0 +1,512 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This class defines a set of tests for the
+ * {@link org.forgerock.opendj.ldap.RDN} class.
+ */
+@SuppressWarnings("javadoc")
+public final class RDNTestCase extends SdkTestCase {
+
+    /** Domain component attribute type. */
+    private static final AttributeType ATTR_TYPE_DC;
+
+    /** Common name attribute type. */
+    private static final AttributeType ATTR_TYPE_CN;
+
+    /** Test attribute value. */
+    private static final AVA ATTR_VALUE_DC_ORG;
+
+    static {
+        ATTR_TYPE_DC = Schema.getCoreSchema().getAttributeType("dc");
+        ATTR_TYPE_CN = Schema.getCoreSchema().getAttributeType("cn");
+        // Set the avas.
+        ATTR_VALUE_DC_ORG = new AVA(ATTR_TYPE_DC, ByteString.valueOfUtf8("org"));
+    }
+
+    /** "org" bytestring. */
+    private static final ByteString ORG = ByteString.valueOfUtf8("org");
+
+    /**
+     * RDN test data provider.
+     *
+     * @return The array of test RDN strings.
+     */
+    @DataProvider(name = "testRDNs")
+    public Object[][] createData() {
+        return new Object[][] {
+            { "dc=hello world", "dc=hello world", "dc=hello world" },
+            { "dc =hello world", "dc=hello world", "dc=hello world" },
+            { "dc  =hello world", "dc=hello world", "dc=hello world" },
+            { "dc= hello world", "dc=hello world", "dc=hello world" },
+            { "dc=  hello world", "dc=hello world", "dc=hello world" },
+            { "undefined=hello", "undefined=hello", "undefined=hello" },
+            { "DC=HELLO WORLD", "dc=hello world", "DC=HELLO WORLD" },
+            { "dc = hello    world", "dc=hello world", "dc=hello    world" },
+            { "   dc = hello world   ", "dc=hello world", "dc=hello world" },
+            { "givenName=John+cn=Doe", "cn=doe+givenname=john", "givenName=John+cn=Doe" },
+            { "givenName=John\\+cn=Doe", "givenname=john\\+cn\\=doe", "givenName=John\\+cn=Doe" },
+            { "cn=Doe\\, John", "cn=doe\\, john", "cn=Doe\\, John" },
+            { "OU=Sales+CN=J. Smith", "cn=j. smith+ou=sales", "OU=Sales+CN=J. Smith" },
+            { "CN=James \\\"Jim\\\" Smith\\, III", "cn=james \\\"jim\\\" smith\\, iii",
+                "CN=James \\\"Jim\\\" Smith\\, III" },
+            // \0d is a hex representation of Carriage return. It is mapped
+            // to a SPACE as defined in the MAP ( RFC 4518)
+            { "CN=Before\\0dAfter", "cn=before after", "CN=Before\\0dAfter" },
+            { "cn=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "cn=hi", "cn=\\04\\02Hi" },
+            { "CN=Lu\\C4\\8Di\\C4\\87", "cn=lu\u010di\u0107", "CN=Lu\u010di\u0107" },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8", "ou=\u55b6\u696d\u90e8",
+                "ou=\u55b6\u696d\u90e8" },
+            { "photo=\\ john \\ ", "photo=\\ john \\ ", "photo=\\ john \\ " },
+            { "AB-global=", "ab-global=", "AB-global=" },
+            { "cn=John+a=", "a=+cn=john", "cn=John+a=" },
+            { "O=\"Sue, Grabbit and Runn\"", "o=sue\\, grabbit and runn",
+                "O=Sue\\, Grabbit and Runn" }, };
+    }
+
+    /**
+     * Illegal RDN test data provider.
+     *
+     * @return The array of illegal test RDN strings.
+     */
+    @DataProvider(name = "illegalRDNs")
+    public Object[][] createIllegalData() {
+        // @formatter:off
+        return new Object[][] {
+            { null },
+            { "" },
+            { " " },
+            { "=" },
+            { "manager" },
+            { "manager " },
+            { "cn+" },
+            { "cn+Jim" },
+            { "cn=Jim+" },
+            { "cn=Jim +" },
+            { "cn=Jim+ " },
+            { "cn=Jim+cn=John" },
+            { "cn=Jim+sn" },
+            { "cn=Jim+sn " },
+            { "cn=Jim+sn equals" },
+            { "cn=Jim," },
+            { "cn=Jim;" },
+            { "cn=Jim,  " },
+            { "cn=Jim+sn=a," },
+            { "cn=Jim, sn=Jam " },
+            { "cn+uid=Jim" },
+            { "-cn=Jim" },
+            { "/tmp=a" },
+            { "\\tmp=a" },
+            { "cn;lang-en=Jim" },
+            { "@cn=Jim" },
+            { "_name_=Jim" },
+            { "\u03c0=pi" },
+            { "v1.0=buggy" },
+            { "cn=Jim+sn=Bob++" },
+            { "cn=Jim+sn=Bob+," },
+            { "1.3.6.1.4.1.1466..0=#04024869" },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * RDN equality test data provider.
+     *
+     * @return The array of test RDN strings.
+     */
+    @DataProvider(name = "createRDNEqualityData")
+    public Object[][] createRDNEqualityData() {
+        // @formatter:off
+        return new Object[][] {
+            { "cn=hello world", "cn=hello world", 0 },
+            { "cn=hello world", "CN=hello world", 0 },
+            { "cn=hello   world", "cn=hello world", 0 },
+            { "  cn =  hello world  ", "cn=hello world", 0 },
+            { "cn=hello world\\ ", "cn=hello world", 0 },
+            { "cn=HELLO WORLD", "cn=hello world", 0 },
+            { "cn=HELLO+sn=WORLD", "sn=world+cn=hello", 0 },
+            { "cn=HELLO+sn=WORLD", "cn=hello+sn=nurse", 1 },
+            { "cn=HELLO+sn=WORLD", "cn=howdy+sn=yall", -1 },
+            { "cn=hello", "cn=hello+sn=world", -1 },
+            { "cn=hello+sn=world", "cn=hello", 1 },
+            { "cn=hello+sn=world", "cn=hello+description=world", 1 },
+            { "cn=hello", "sn=world", -1 },
+            { "sn=hello", "cn=world", 1 },
+            { "governingStructureRule=10", "governingStructureRule=9", 1 },
+            { "governingStructureRule=999", "governingStructureRule=1000", -1 },
+            { "governingStructureRule=-1", "governingStructureRule=0", -1 },
+            { "governingStructureRule=0", "governingStructureRule=-1", 1 },
+            { "cn=aaa", "cn=aaaa", -1 },
+            { "cn=AAA", "cn=aaaa", -1 },
+            { "cn=aaa", "cn=AAAA", -1 },
+            { "cn=aaaa", "cn=aaa", 1 },
+            { "cn=AAAA", "cn=aaa", 1 },
+            { "cn=aaaa", "cn=AAA", 1 },
+            { "cn=aaab", "cn=aaaa", 1 },
+            { "cn=aaaa", "cn=aaab", -1 },
+            { RDN.maxValue(), RDN.maxValue(), 0 },
+            { RDN.maxValue(), "cn=aaa", 1 },
+            { "cn=aaa", RDN.maxValue(), -1 },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Test RDN compareTo
+     *
+     * @param first
+     *            First RDN to compare.
+     * @param second
+     *            Second RDN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createRDNEqualityData")
+    public void testCompareTo(final Object first, final Object second, final int result)
+            throws Exception {
+        final RDN rdn1 = parseRDN(first);
+        final RDN rdn2 = parseRDN(second);
+
+        int rc = rdn1.compareTo(rdn2);
+
+        // Normalize the result.
+        if (rc < 0) {
+            rc = -1;
+        } else if (rc > 0) {
+            rc = 1;
+        }
+
+        assertEquals(rc, result, "Comparison for <" + first + "> and <" + second + ">.");
+    }
+
+    private RDN parseRDN(final Object value) {
+        return (value instanceof RDN) ? ((RDN) value) : RDN.valueOf(value.toString());
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void ensureRDNIsCreatedWithNonEmptyArguments() {
+        new RDN();
+    }
+
+    /**
+     * Test RDN construction with single AVA.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructor() throws Exception {
+        final RDN rdn = new RDN(ATTR_TYPE_DC, ORG);
+
+        assertEquals(rdn.size(), 1);
+        assertEquals(rdn.isMultiValued(), false);
+        assertEquals(rdn.getFirstAVA().getAttributeType(), ATTR_TYPE_DC);
+        assertEquals(rdn.getFirstAVA().getAttributeType().getNameOrOID(), ATTR_TYPE_DC.getNameOrOID());
+        assertEquals(rdn.getFirstAVA(), ATTR_VALUE_DC_ORG);
+    }
+
+    /**
+     * Test RDN construction with String attribute type and value.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructorWithString() throws Exception {
+        final RDN rdn = new RDN("dc", "org");
+        assertEquals(rdn.size(), 1);
+        assertEquals(rdn.getFirstAVA().getAttributeType(), ATTR_TYPE_DC);
+        assertEquals(rdn.getFirstAVA().getAttributeType().getNameOrOID(), "dc");
+        assertEquals(rdn.getFirstAVA(), ATTR_VALUE_DC_ORG);
+    }
+
+    @Test
+    public void testConstructorWithAVA() throws Exception {
+        final RDN rdn = new RDN(new AVA("dc", "org"));
+        assertEquals(rdn.size(), 1);
+        assertEquals(rdn.getFirstAVA().getAttributeType(), ATTR_TYPE_DC);
+        assertEquals(rdn.getFirstAVA(), ATTR_VALUE_DC_ORG);
+    }
+
+    @Test
+    public void testConstructorWithMultipleAVAs() throws Exception {
+        AVA example = new AVA("dc", "example");
+        AVA user = new AVA("cn", "bjensen");
+
+        final RDN rdn = new RDN(example, user);
+        assertEquals(rdn.size(), 2);
+        Iterator<AVA> rdnIt = rdn.iterator();
+        AVA firstAva = rdnIt.next();
+        assertEquals(firstAva.getAttributeType(), ATTR_TYPE_DC);
+        assertEquals(firstAva, example);
+
+        AVA secondAva = rdnIt.next();
+        assertEquals(secondAva.getAttributeType(), ATTR_TYPE_CN);
+        assertEquals(secondAva, user);
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testConstructorWithDuplicateAVAs() {
+        AVA example = new AVA("dc", "example");
+        AVA org = new AVA("dc", "org");
+        new RDN(example, org);
+    }
+
+    @Test
+    public void testConstructorWithCollectionOfAVAs() throws Exception {
+        AVA example = new AVA("dc", "example");
+        AVA user = new AVA("cn", "bjensen");
+
+        final RDN rdn = new RDN(Arrays.asList(example, user));
+        assertEquals(rdn.size(), 2);
+        Iterator<AVA> rdnIt = rdn.iterator();
+        AVA firstAva = rdnIt.next();
+        assertEquals(firstAva.getAttributeType(), ATTR_TYPE_DC);
+        assertEquals(firstAva, example);
+
+        AVA secondAva = rdnIt.next();
+        assertEquals(secondAva.getAttributeType(), ATTR_TYPE_CN);
+        assertEquals(secondAva, user);
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testConstructorWithCollectionOfDuplicateAVAs() {
+        AVA example = new AVA("dc", "example");
+        AVA org = new AVA("dc", "org");
+        new RDN(Arrays.asList(example, org));
+    }
+
+    /**
+     * Test RDN string decoder against illegal strings.
+     *
+     * @param rawRDN
+     *            Illegal RDN string representation.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "illegalRDNs", expectedExceptions = { NullPointerException.class,
+            LocalizedIllegalArgumentException.class, StringIndexOutOfBoundsException.class })
+    public void testDecodeIllegalString(final String rawRDN) throws Exception {
+        RDN.valueOf(rawRDN);
+
+        fail("Expected exception for value \"" + rawRDN + "\"");
+    }
+
+    /**
+     * Test RDN string decoder.
+     *
+     * @param rawRDN
+     *            Raw RDN string representation.
+     * @param normRDN
+     *            Normalized RDN string representation.
+     * @param stringRDN
+     *            String representation.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    /**
+     * @Test(dataProvider = "testRDNs") public void testToString(String rawRDN,
+     *                    String normRDN, String stringRDN) throws Exception {
+     *                    RDN rdn = RDN.valueOf(rawRDN);
+     *                    assertEquals(rdn.toString(), stringRDN); }
+     **/
+
+    /**
+     * Test RDN string decoder.
+     *
+     * @param rawRDN
+     *            Raw RDN string representation.
+     * @param normRDN
+     *            Normalized RDN string representation.
+     * @param stringRDN
+     *            String representation.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "testRDNs")
+    public void testDecodeString(final String rawRDN, final String normRDN, final String stringRDN)
+            throws Exception {
+        final RDN rdn = RDN.valueOf(rawRDN);
+        final RDN string = RDN.valueOf(stringRDN);
+        assertEquals(rdn, string);
+    }
+
+    /**
+     * Tests the valueof with ctor.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testDuplicateSingle() {
+        final RDN rdn1 = new RDN(ATTR_TYPE_DC, ORG);
+        final RDN rdn2 = RDN.valueOf("dc=org");
+
+        assertFalse(rdn1 == rdn2);
+        assertEquals(rdn1, rdn2);
+    }
+
+    /**
+     * Test RDN equality
+     *
+     * @param first
+     *            First RDN to compare.
+     * @param second
+     *            Second RDN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createRDNEqualityData")
+    public void testEquality(final Object first, final Object second, final int result)
+            throws Exception {
+        final RDN rdn1 = parseRDN(first);
+        final RDN rdn2 = parseRDN(second);
+
+        if (result == 0) {
+            assertTrue(rdn1.equals(rdn2), "RDN equality for <" + first + "> and <" + second + ">");
+        } else {
+            assertFalse(rdn1.equals(rdn2), "RDN equality for <" + first + "> and <" + second + ">");
+        }
+    }
+
+    /**
+     * Tests the equals method with a non-RDN argument.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testEqualityNonRDN() {
+        final RDN rdn = new RDN(ATTR_TYPE_DC, ORG);
+
+        assertFalse(rdn.equals("this isn't an RDN"));
+    }
+
+    /**
+     * Tests the equals method with a null argument.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testEqualityNull() {
+        final RDN rdn = new RDN(ATTR_TYPE_DC, ORG);
+
+        assertFalse(rdn.equals(null));
+    }
+
+    /**
+     * Test getAttributeName.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testGetAttributeName() throws Exception {
+        final RDN rdn = RDN.valueOf("dc=opendj+cn=org");
+        assertTrue(rdn.isMultiValued());
+        assertEquals(rdn.size(), 2);
+        final Iterator<AVA> it = rdn.iterator();
+        assertEquals(it.next().getAttributeType().getNameOrOID(), ATTR_TYPE_DC.getNameOrOID());
+        assertEquals(it.next().getAttributeType().getNameOrOID(), ATTR_TYPE_CN.getNameOrOID());
+    }
+
+    /**
+     * Test escaping of single space values.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testEscaping() {
+        RDN rdn = new RDN(ATTR_TYPE_DC, ByteString.valueOfUtf8(" "));
+        assertEquals(rdn.toString(), "dc=\\ ");
+    }
+
+    /**
+     * Test RDN hashCode
+     *
+     * @param first
+     *            First RDN to compare.
+     * @param second
+     *            Second RDN to compare.
+     * @param result
+     *            Expected comparison result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "createRDNEqualityData")
+    public void testHashCode(final Object first, final Object second, final int result)
+            throws Exception {
+        final RDN rdn1 = parseRDN(first);
+        final RDN rdn2 = parseRDN(second);
+
+        final int h1 = rdn1.hashCode();
+        final int h2 = rdn2.hashCode();
+
+        if (result == 0) {
+            assertThat(h1)
+                    .as("Hash codes for <" + first + "> and <" + second + "> should be the same.")
+                    .isEqualTo(h2);
+        } else {
+            assertThat(h1)
+                    .as("Hash codes for <" + first + "> and <" + second + "> should NOT be the same.")
+                    .isNotEqualTo(h2);
+        }
+    }
+
+    @DataProvider
+    public Object[][] toStringShouldStripOutIllegalWhitespaceDataProvider() {
+        // @formatter:off
+        return new Object[][] {
+            { " dc = hello  world ", "dc=hello  world" },
+            { " dc =\\  hello  world\\  ", "dc=\\  hello  world\\ " },
+            { " uid = cpfc + dc = example ", "uid=cpfc+dc=example" },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "toStringShouldStripOutIllegalWhitespaceDataProvider")
+    public void toStringShouldStripOutIllegalWhitespace(String withWhiteSpace, String withoutWhiteSpace) {
+        assertThat(RDN.valueOf(withWhiteSpace).toString()).isEqualTo(withoutWhiteSpace);
+        assertThat(RDN.valueOf(withWhiteSpace).toNormalizedByteString(new ByteStringBuilder()))
+                .isEqualTo(RDN.valueOf(withoutWhiteSpace).toNormalizedByteString(new ByteStringBuilder()));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RequestLoadBalancerTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RequestLoadBalancerTestCase.java
new file mode 100644
index 0000000..e1e6d0b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/RequestLoadBalancerTestCase.java
@@ -0,0 +1,950 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.ResultCode.CLIENT_SIDE_CONNECT_ERROR;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newGenericExtendedResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNotNull;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.util.ArrayList;
+import java.util.logging.Level;
+
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promises;
+import org.mockito.Mock;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class RequestLoadBalancerTestCase extends SdkTestCase {
+    @Test
+    public void closeLoadBalancerShouldCloseDelegateFactories() throws Exception {
+        configureAllFactoriesOnline();
+        loadBalancer.close();
+        verify(factory1).close();
+        verify(factory2).close();
+        verify(factory3).close();
+    }
+
+    @Test
+    public void getConnectionShouldNotInvokeDelegateFactory() throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnection()) {
+            assertThat(connection).isNotNull();
+            verifyZeroInteractions(factory1, factory2, factory3);
+        }
+        verifyZeroInteractions(factory1, factory2, factory3);
+        loadBalancer.close();
+    }
+
+    @Test
+    public void getConnectionReturnANewConnectionEachTime() throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection1 = loadBalancer.getConnection();
+             Connection connection2 = loadBalancer.getConnection()) {
+            assertThat(connection1).isNotSameAs(connection2);
+        }
+        loadBalancer.close();
+    }
+
+    @Test
+    public void getConnectionAsyncReturnANewConnectionEachTime() throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection1 = loadBalancer.getConnectionAsync().get();
+             Connection connection2 = loadBalancer.getConnectionAsync().get()) {
+            assertThat(connection1).isNotSameAs(connection2);
+        }
+        loadBalancer.close();
+    }
+
+    @Test
+    public void getConnectionAsyncShouldNotInvokeDelegateFactory() throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            assertThat(connection).isNotNull();
+            verifyZeroInteractions(factory1, factory2, factory3);
+        }
+        verifyZeroInteractions(factory1, factory2, factory3);
+    }
+
+    @Test
+    public void connectionEventListenersNotifiedOnClose() throws Exception {
+        configureAllFactoriesOnline();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.addConnectionEventListener(listener);
+        }
+        verify(listener).handleConnectionClosed();
+    }
+
+    @Test
+    public void connectionEventListenersNotifiedOnError() throws Exception {
+        configureAllFactoriesOffline();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.addConnectionEventListener(listener);
+            try {
+                connection.add(addRequest1);
+                fail("add unexpectedly succeeded");
+            } catch (LdapException ignored) {
+                // Ignore.
+            }
+        }
+        verify(listener).handleConnectionError(eq(false), any(LdapException.class));
+        verify(listener).handleConnectionClosed();
+    }
+
+    @Test
+    public void removedConnectionEventListenersShouldNotBeNotified() throws Exception {
+        configureAllFactoriesOnline();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.addConnectionEventListener(listener);
+            connection.removeConnectionEventListener(listener);
+        }
+        verifyZeroInteractions(listener);
+    }
+
+    @Test
+    public void validAndClosedStateShouldBeMaintained() throws Exception {
+        configureAllFactoriesOffline();
+        final Connection connection = loadBalancer.getConnectionAsync().get();
+        assertThat(connection.isValid()).isTrue();
+        assertThat(connection.isClosed()).isFalse();
+
+        try {
+            connection.add(addRequest1);
+            fail("add unexpectedly succeeded");
+        } catch (LdapException ignored) {
+            // Ignore.
+        }
+        assertThat(connection.isValid()).isFalse();
+        assertThat(connection.isClosed()).isFalse();
+
+        connection.close();
+        assertThat(connection.isValid()).isFalse();
+        assertThat(connection.isClosed()).isTrue();
+    }
+
+    @Test
+    public void factoryToStringShouldReturnANonEmptyString() throws Exception {
+        configureAllFactoriesOffline();
+        assertThat(loadBalancer.toString()).isNotEmpty();
+    }
+
+    @Test
+    public void connectionToStringShouldReturnANonEmptyString() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            assertThat(connection.toString()).isNotEmpty();
+        }
+    }
+
+    @Test
+    public void abandonRequestShouldBeIgnored() throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.abandonAsync(mock(AbandonRequest.class));
+        }
+        verifyZeroInteractions(factory1, factory2, factory3);
+    }
+
+    // We can't use a DataProviders here because the mocks will be re-initialized for each test method call.
+
+    // ################## Add Requests ####################
+
+    @Test
+    public void addRequestsShouldBeRoutedCorrectly1() throws Exception {
+        addRequestsShouldBeRoutedCorrectlyImpl(addRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void addRequestsShouldBeRoutedCorrectly2() throws Exception {
+        addRequestsShouldBeRoutedCorrectlyImpl(addRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void addRequestsShouldBeRoutedCorrectly3() throws Exception {
+        addRequestsShouldBeRoutedCorrectlyImpl(addRequest3, factory3, connection3);
+    }
+
+    private void addRequestsShouldBeRoutedCorrectlyImpl(final AddRequest addRequest,
+                                                        final ConnectionFactory expectedFactory,
+                                                        final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.add(addRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).addAsync(same(addRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void addRequestsShouldLinearProbeOnFailure1() throws Exception {
+        addRequestsShouldLinearProbeOnFailureImpl(addRequest1);
+    }
+
+    @Test
+    public void addRequestsShouldLinearProbeOnFailure2() throws Exception {
+        addRequestsShouldLinearProbeOnFailureImpl(addRequest2);
+    }
+
+    @Test
+    public void addRequestsShouldLinearProbeOnFailure3() throws Exception {
+        addRequestsShouldLinearProbeOnFailureImpl(addRequest3);
+    }
+
+    private void addRequestsShouldLinearProbeOnFailureImpl(final AddRequest addRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.add(addRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).addAsync(same(addRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void addRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.add(addRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Bind Requests ####################
+
+    @Test
+    public void bindRequestsShouldBeRoutedCorrectly1() throws Exception {
+        bindRequestsShouldBeRoutedCorrectlyImpl(bindRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void bindRequestsShouldBeRoutedCorrectly2() throws Exception {
+        bindRequestsShouldBeRoutedCorrectlyImpl(bindRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void bindRequestsShouldBeRoutedCorrectly3() throws Exception {
+        bindRequestsShouldBeRoutedCorrectlyImpl(bindRequest3, factory3, connection3);
+    }
+
+    private void bindRequestsShouldBeRoutedCorrectlyImpl(final BindRequest bindRequest,
+                                                         final ConnectionFactory expectedFactory,
+                                                         final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.bind(bindRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).bindAsync(same(bindRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void bindRequestsShouldLinearProbeOnFailure1() throws Exception {
+        bindRequestsShouldLinearProbeOnFailureImpl(bindRequest1);
+    }
+
+    @Test
+    public void bindRequestsShouldLinearProbeOnFailure2() throws Exception {
+        bindRequestsShouldLinearProbeOnFailureImpl(bindRequest2);
+    }
+
+    @Test
+    public void bindRequestsShouldLinearProbeOnFailure3() throws Exception {
+        bindRequestsShouldLinearProbeOnFailureImpl(bindRequest3);
+    }
+
+    private void bindRequestsShouldLinearProbeOnFailureImpl(final BindRequest bindRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.bind(bindRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).bindAsync(same(bindRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void bindRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.bind(bindRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Compare Requests ####################
+
+    @Test
+    public void compareRequestsShouldBeRoutedCorrectly1() throws Exception {
+        compareRequestsShouldBeRoutedCorrectlyImpl(compareRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void compareRequestsShouldBeRoutedCorrectly2() throws Exception {
+        compareRequestsShouldBeRoutedCorrectlyImpl(compareRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void compareRequestsShouldBeRoutedCorrectly3() throws Exception {
+        compareRequestsShouldBeRoutedCorrectlyImpl(compareRequest3, factory3, connection3);
+    }
+
+    private void compareRequestsShouldBeRoutedCorrectlyImpl(final CompareRequest compareRequest,
+                                                            final ConnectionFactory expectedFactory,
+                                                            final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.compare(compareRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).compareAsync(same(compareRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void compareRequestsShouldLinearProbeOnFailure1() throws Exception {
+        compareRequestsShouldLinearProbeOnFailureImpl(compareRequest1);
+    }
+
+    @Test
+    public void compareRequestsShouldLinearProbeOnFailure2() throws Exception {
+        compareRequestsShouldLinearProbeOnFailureImpl(compareRequest2);
+    }
+
+    @Test
+    public void compareRequestsShouldLinearProbeOnFailure3() throws Exception {
+        compareRequestsShouldLinearProbeOnFailureImpl(compareRequest3);
+    }
+
+    private void compareRequestsShouldLinearProbeOnFailureImpl(final CompareRequest compareRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.compare(compareRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).compareAsync(same(compareRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void compareRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.compare(compareRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Delete Requests ####################
+
+    @Test
+    public void deleteRequestsShouldBeRoutedCorrectly1() throws Exception {
+        deleteRequestsShouldBeRoutedCorrectlyImpl(deleteRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void deleteRequestsShouldBeRoutedCorrectly2() throws Exception {
+        deleteRequestsShouldBeRoutedCorrectlyImpl(deleteRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void deleteRequestsShouldBeRoutedCorrectly3() throws Exception {
+        deleteRequestsShouldBeRoutedCorrectlyImpl(deleteRequest3, factory3, connection3);
+    }
+
+    private void deleteRequestsShouldBeRoutedCorrectlyImpl(final DeleteRequest deleteRequest,
+                                                           final ConnectionFactory expectedFactory,
+                                                           final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.delete(deleteRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).deleteAsync(same(deleteRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void deleteRequestsShouldLinearProbeOnFailure1() throws Exception {
+        deleteRequestsShouldLinearProbeOnFailureImpl(deleteRequest1);
+    }
+
+    @Test
+    public void deleteRequestsShouldLinearProbeOnFailure2() throws Exception {
+        deleteRequestsShouldLinearProbeOnFailureImpl(deleteRequest2);
+    }
+
+    @Test
+    public void deleteRequestsShouldLinearProbeOnFailure3() throws Exception {
+        deleteRequestsShouldLinearProbeOnFailureImpl(deleteRequest3);
+    }
+
+    private void deleteRequestsShouldLinearProbeOnFailureImpl(final DeleteRequest deleteRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.delete(deleteRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).deleteAsync(same(deleteRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void deleteRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.delete(deleteRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Extended Requests ####################
+
+    @Test
+    public void extendedRequestsShouldBeRoutedCorrectly1() throws Exception {
+        extendedRequestsShouldBeRoutedCorrectlyImpl(extendedRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void extendedRequestsShouldBeRoutedCorrectly2() throws Exception {
+        extendedRequestsShouldBeRoutedCorrectlyImpl(extendedRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void extendedRequestsShouldBeRoutedCorrectly3() throws Exception {
+        extendedRequestsShouldBeRoutedCorrectlyImpl(extendedRequest3, factory3, connection3);
+    }
+
+    private void extendedRequestsShouldBeRoutedCorrectlyImpl(final ExtendedRequest<?> extendedRequest,
+                                                             final ConnectionFactory expectedFactory,
+                                                             final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.extendedRequest(extendedRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).extendedRequestAsync(same(extendedRequest),
+                                                        isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void extendedRequestsShouldLinearProbeOnFailure1() throws Exception {
+        extendedRequestsShouldLinearProbeOnFailureImpl(extendedRequest1);
+    }
+
+    @Test
+    public void extendedRequestsShouldLinearProbeOnFailure2() throws Exception {
+        extendedRequestsShouldLinearProbeOnFailureImpl(extendedRequest2);
+    }
+
+    @Test
+    public void extendedRequestsShouldLinearProbeOnFailure3() throws Exception {
+        extendedRequestsShouldLinearProbeOnFailureImpl(extendedRequest3);
+    }
+
+    private void extendedRequestsShouldLinearProbeOnFailureImpl(
+            final ExtendedRequest<?> extendedRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.extendedRequest(extendedRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).extendedRequestAsync(same(extendedRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void extendedRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.extendedRequest(extendedRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Modify Requests ####################
+
+    @Test
+    public void modifyRequestsShouldBeRoutedCorrectly1() throws Exception {
+        modifyRequestsShouldBeRoutedCorrectlyImpl(modifyRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void modifyRequestsShouldBeRoutedCorrectly2() throws Exception {
+        modifyRequestsShouldBeRoutedCorrectlyImpl(modifyRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void modifyRequestsShouldBeRoutedCorrectly3() throws Exception {
+        modifyRequestsShouldBeRoutedCorrectlyImpl(modifyRequest3, factory3, connection3);
+    }
+
+    private void modifyRequestsShouldBeRoutedCorrectlyImpl(final ModifyRequest modifyRequest,
+                                                           final ConnectionFactory expectedFactory,
+                                                           final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.modify(modifyRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).modifyAsync(same(modifyRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void modifyRequestsShouldLinearProbeOnFailure1() throws Exception {
+        modifyRequestsShouldLinearProbeOnFailureImpl(modifyRequest1);
+    }
+
+    @Test
+    public void modifyRequestsShouldLinearProbeOnFailure2() throws Exception {
+        modifyRequestsShouldLinearProbeOnFailureImpl(modifyRequest2);
+    }
+
+    @Test
+    public void modifyRequestsShouldLinearProbeOnFailure3() throws Exception {
+        modifyRequestsShouldLinearProbeOnFailureImpl(modifyRequest3);
+    }
+
+    private void modifyRequestsShouldLinearProbeOnFailureImpl(final ModifyRequest modifyRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.modify(modifyRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).modifyAsync(same(modifyRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void modifyRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.modify(modifyRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## ModifyDN Requests ####################
+
+    @Test
+    public void modifyDNRequestsShouldBeRoutedCorrectly1() throws Exception {
+        modifyDNRequestsShouldBeRoutedCorrectlyImpl(modifyDNRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldBeRoutedCorrectly2() throws Exception {
+        modifyDNRequestsShouldBeRoutedCorrectlyImpl(modifyDNRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldBeRoutedCorrectly3() throws Exception {
+        modifyDNRequestsShouldBeRoutedCorrectlyImpl(modifyDNRequest3, factory3, connection3);
+    }
+
+    private void modifyDNRequestsShouldBeRoutedCorrectlyImpl(final ModifyDNRequest modifyDNRequest,
+                                                             final ConnectionFactory expectedFactory,
+                                                             final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.modifyDN(modifyDNRequest);
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).modifyDNAsync(same(modifyDNRequest), isNull(IntermediateResponseHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldLinearProbeOnFailure1() throws Exception {
+        modifyDNRequestsShouldLinearProbeOnFailureImpl(modifyDNRequest1);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldLinearProbeOnFailure2() throws Exception {
+        modifyDNRequestsShouldLinearProbeOnFailureImpl(modifyDNRequest2);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldLinearProbeOnFailure3() throws Exception {
+        modifyDNRequestsShouldLinearProbeOnFailureImpl(modifyDNRequest3);
+    }
+
+    private void modifyDNRequestsShouldLinearProbeOnFailureImpl(
+            final ModifyDNRequest modifyDNRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.modifyDN(modifyDNRequest);
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).modifyDNAsync(same(modifyDNRequest), isNull(IntermediateResponseHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void modifyDNRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.modifyDN(modifyDNRequest1);
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    // ################## Search Requests ####################
+
+    @Test
+    public void searchRequestsShouldBeRoutedCorrectly1() throws Exception {
+        searchRequestsShouldBeRoutedCorrectlyImpl(searchRequest1, factory1, connection1);
+    }
+
+    @Test
+    public void searchRequestsShouldBeRoutedCorrectly2() throws Exception {
+        searchRequestsShouldBeRoutedCorrectlyImpl(searchRequest2, factory2, connection2);
+    }
+
+    @Test
+    public void searchRequestsShouldBeRoutedCorrectly3() throws Exception {
+        searchRequestsShouldBeRoutedCorrectlyImpl(searchRequest3, factory3, connection3);
+    }
+
+    private void searchRequestsShouldBeRoutedCorrectlyImpl(final SearchRequest searchRequest,
+                                                           final ConnectionFactory expectedFactory,
+                                                           final Connection expectedConnection) throws Exception {
+        configureAllFactoriesOnline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.search(searchRequest, new ArrayList<>());
+        }
+        verify(expectedFactory).getConnectionAsync();
+        verify(expectedConnection).searchAsync(same(searchRequest),
+                                               isNull(IntermediateResponseHandler.class),
+                                               isNotNull(SearchResultHandler.class));
+        verify(expectedConnection).close();
+        verifyZeroInteractionsForRemainingFactories(expectedFactory);
+    }
+
+    @Test
+    public void searchRequestsShouldLinearProbeOnFailure1() throws Exception {
+        searchRequestsShouldLinearProbeOnFailureImpl(searchRequest1);
+    }
+
+    @Test
+    public void searchRequestsShouldLinearProbeOnFailure2() throws Exception {
+        searchRequestsShouldLinearProbeOnFailureImpl(searchRequest2);
+    }
+
+    @Test
+    public void searchRequestsShouldLinearProbeOnFailure3() throws Exception {
+        searchRequestsShouldLinearProbeOnFailureImpl(searchRequest3);
+    }
+
+    private void searchRequestsShouldLinearProbeOnFailureImpl(final SearchRequest searchRequest) throws Exception {
+        configureFactoriesOneAndTwoOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            connection.search(searchRequest, new ArrayList<>());
+        }
+        verify(factory3).getConnectionAsync();
+        verify(connection3).searchAsync(same(searchRequest),
+                                        isNull(IntermediateResponseHandler.class),
+                                        isNotNull(SearchResultHandler.class));
+        verify(connection3).close();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+    }
+
+    @Test
+    public void searchRequestsShouldFailWhenAllFactoriesOffline() throws Exception {
+        configureAllFactoriesOffline();
+        try (Connection connection = loadBalancer.getConnectionAsync().get()) {
+            try {
+                connection.search(searchRequest1, new ArrayList<>());
+            } catch (ConnectionException e) {
+                assertThat(e.getResult().getResultCode()).isEqualTo(CLIENT_SIDE_CONNECT_ERROR);
+            }
+        }
+        verify(factory1, atLeastOnce()).getConnectionAsync();
+        verify(factory2, atLeastOnce()).getConnectionAsync();
+        verify(factory3, atLeastOnce()).getConnectionAsync();
+        verifyZeroInteractions(connection1);
+        verifyZeroInteractions(connection2);
+        verifyZeroInteractions(connection3);
+    }
+
+    @Mock private ConnectionFactory factory1;
+    @Mock private ConnectionFactory factory2;
+    @Mock private ConnectionFactory factory3;
+
+    @Mock private AbstractAsynchronousConnection connection1;
+    @Mock private AbstractAsynchronousConnection connection2;
+    @Mock private AbstractAsynchronousConnection connection3;
+
+    @Mock private AddRequest addRequest1;
+    @Mock private AddRequest addRequest2;
+    @Mock private AddRequest addRequest3;
+
+    @Mock private BindRequest bindRequest1;
+    @Mock private BindRequest bindRequest2;
+    @Mock private BindRequest bindRequest3;
+
+    @Mock private CompareRequest compareRequest1;
+    @Mock private CompareRequest compareRequest2;
+    @Mock private CompareRequest compareRequest3;
+
+    @Mock private DeleteRequest deleteRequest1;
+    @Mock private DeleteRequest deleteRequest2;
+    @Mock private DeleteRequest deleteRequest3;
+
+    @Mock private GenericExtendedRequest extendedRequest1;
+    @Mock private GenericExtendedRequest extendedRequest2;
+    @Mock private GenericExtendedRequest extendedRequest3;
+
+    @Mock private ModifyRequest modifyRequest1;
+    @Mock private ModifyRequest modifyRequest2;
+    @Mock private ModifyRequest modifyRequest3;
+
+    @Mock private ModifyDNRequest modifyDNRequest1;
+    @Mock private ModifyDNRequest modifyDNRequest2;
+    @Mock private ModifyDNRequest modifyDNRequest3;
+
+    @Mock private SearchRequest searchRequest1;
+    @Mock private SearchRequest searchRequest2;
+    @Mock private SearchRequest searchRequest3;
+
+    private ConnectionFactory loadBalancer;
+
+    @BeforeMethod
+    public void beforeMethod() {
+        TestCaseUtils.setDefaultLogLevel(Level.SEVERE);
+        initMocks(this);
+        stub(this.connection1);
+        stub(this.connection2);
+        stub(this.connection3);
+        loadBalancer = new RequestLoadBalancer("Test",
+                                               asList(factory1, factory2, factory3),
+                                               defaultOptions(), newNextFactoryFunction());
+    }
+
+    private Function<Request, Integer, NeverThrowsException> newNextFactoryFunction() {
+        return new Function<Request, Integer, NeverThrowsException>() {
+            @Override
+            public Integer apply(final Request request) {
+                if (request == addRequest1 || request == bindRequest1 || request == compareRequest1
+                        || request == deleteRequest1 || request == extendedRequest1 || request == modifyRequest1
+                        || request == modifyDNRequest1 || request == searchRequest1) {
+                    return 0;
+                }
+
+                if (request == addRequest2 || request == bindRequest2 || request == compareRequest2
+                        || request == deleteRequest2 || request == extendedRequest2 || request == modifyRequest2
+                        || request == modifyDNRequest2 || request == searchRequest2) {
+                    return 1;
+                }
+
+                if (request == addRequest3 || request == bindRequest3 || request == compareRequest3
+                        || request == deleteRequest3 || request == extendedRequest3 || request == modifyRequest3
+                        || request == modifyDNRequest3 || request == searchRequest3) {
+                    return 2;
+                }
+
+                fail("Received unexpected request");
+                return -1; // Keep compiler happy.
+            }
+        };
+    }
+
+    private void stub(final Connection connection) {
+        when(connection.addAsync(any(AddRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)));
+        when(connection.bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newBindResult(ResultCode.SUCCESS)));
+        when(connection.compareAsync(any(CompareRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newCompareResult(ResultCode.SUCCESS)));
+        when(connection.deleteAsync(any(DeleteRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)));
+        when(connection.extendedRequestAsync(any(GenericExtendedRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newGenericExtendedResult(ResultCode.SUCCESS)));
+        when(connection.modifyAsync(any(ModifyRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)));
+        when(connection.modifyDNAsync(any(ModifyDNRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)));
+        when(connection.searchAsync(any(SearchRequest.class), any(IntermediateResponseHandler.class),
+                                    any(SearchResultHandler.class)))
+                .thenReturn(newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)));
+    }
+
+    @AfterMethod
+    public void afterMethod() {
+        loadBalancer.close();
+        TestCaseUtils.setDefaultLogLevel(Level.INFO);
+    }
+
+    private void configureAllFactoriesOnline() {
+        when(factory1.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newResultPromise(connection1));
+        when(factory2.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newResultPromise(connection2));
+        when(factory3.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newResultPromise(connection3));
+    }
+
+    private void configureFactoriesOneAndTwoOffline() {
+        final LdapException connectionFailure = newLdapException(CLIENT_SIDE_CONNECT_ERROR);
+        when(factory1.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newExceptionPromise(connectionFailure));
+        when(factory2.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newExceptionPromise(connectionFailure));
+        when(factory3.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newResultPromise(connection3));
+    }
+
+    private void configureAllFactoriesOffline() {
+        final LdapException connectionFailure = newLdapException(CLIENT_SIDE_CONNECT_ERROR);
+        when(factory1.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newExceptionPromise(connectionFailure));
+        when(factory2.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newExceptionPromise(connectionFailure));
+        when(factory3.getConnectionAsync())
+                .thenReturn(Promises.<Connection, LdapException>newExceptionPromise(connectionFailure));
+    }
+
+    private void verifyZeroInteractionsForRemainingFactories(final ConnectionFactory expectedFactory) {
+        if (expectedFactory != factory1) {
+            verifyZeroInteractions(factory1);
+        }
+        if (expectedFactory != factory2) {
+            verifyZeroInteractions(factory2);
+        }
+        if (expectedFactory != factory3) {
+            verifyZeroInteractions(factory3);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ResultCodeTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ResultCodeTestCase.java
new file mode 100644
index 0000000..2d63ebf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/ResultCodeTestCase.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.EnumSet;
+import java.util.Iterator;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.ResultCode.Enum.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+public class ResultCodeTestCase extends SdkTestCase {
+
+    @DataProvider
+    public Iterator<Object[]> valuesDataProvider() {
+        return new DataProviderIterator(ResultCode.values());
+    }
+
+    @Test(dataProvider = "valuesDataProvider")
+    public void valueOfInt(ResultCode val) throws Exception {
+        assertSame(ResultCode.valueOf(val.intValue()), val);
+    }
+
+    @Test
+    public void valueOfIntUnknown() throws Exception {
+        int intValue;
+        ResultCode unknown;
+
+        intValue = -2;
+        unknown = ResultCode.valueOf(intValue);
+        assertEquals(unknown.intValue(), intValue);
+        assertEquals(unknown.asEnum(), ResultCode.Enum.UNKNOWN);
+
+        intValue = Integer.MAX_VALUE;
+        unknown = ResultCode.valueOf(intValue);
+        assertEquals(unknown.intValue(), intValue);
+        assertEquals(unknown.asEnum(), ResultCode.Enum.UNKNOWN);
+    }
+
+    @Test(dataProvider = "valuesDataProvider")
+    public void isExceptional(ResultCode val) {
+        EnumSet<ResultCode.Enum> exceptional = EnumSet.complementOf(EnumSet.of(
+            SUCCESS, COMPARE_FALSE, COMPARE_TRUE, SASL_BIND_IN_PROGRESS, NO_OPERATION));
+        assertEquals(val.isExceptional(), exceptional.contains(val.asEnum()));
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
new file mode 100644
index 0000000..735fda3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
@@ -0,0 +1,29 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all types unit tests should extend. A type represents
+ * the classes found directly under the package org.forgerock.opendj.ldap.
+ */
+@Test(groups = { "precommit", "types", "sdk" })
+public abstract class SdkTestCase extends ForgeRockTestCase {
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SearchScopeTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SearchScopeTestCase.java
new file mode 100644
index 0000000..9829399
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/SearchScopeTestCase.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Iterator;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+public class SearchScopeTestCase extends SdkTestCase {
+
+    @Test
+    public void valueOfInt() {
+        final SearchScope unkown1 = SearchScope.valueOf(-1);
+        assertEquals(unkown1.intValue(), -1);
+        assertEquals(unkown1.asEnum(), SearchScope.Enum.UNKNOWN);
+        final SearchScope unknownMax = SearchScope.valueOf(Integer.MAX_VALUE);
+        assertEquals(unknownMax.intValue(), Integer.MAX_VALUE);
+        assertEquals(unknownMax.asEnum(), SearchScope.Enum.UNKNOWN);
+
+        assertEquals(SearchScope.valueOf(0), SearchScope.BASE_OBJECT);
+        assertEquals(SearchScope.valueOf(1), SearchScope.SINGLE_LEVEL);
+        assertEquals(SearchScope.valueOf(2), SearchScope.WHOLE_SUBTREE);
+        assertEquals(SearchScope.valueOf(3), SearchScope.SUBORDINATES);
+    }
+
+    @Test
+    public void valueOfString() {
+        assertNull(SearchScope.valueOf(null));
+        assertEquals(SearchScope.valueOf("base"), SearchScope.BASE_OBJECT);
+        assertEquals(SearchScope.valueOf("one"), SearchScope.SINGLE_LEVEL);
+        assertEquals(SearchScope.valueOf("sub"), SearchScope.WHOLE_SUBTREE);
+        assertEquals(SearchScope.valueOf("subordinates"), SearchScope.SUBORDINATES);
+    }
+
+    @Test
+    public void values() {
+        assertThat(SearchScope.values()).containsExactly(SearchScope.BASE_OBJECT,
+            SearchScope.SINGLE_LEVEL, SearchScope.WHOLE_SUBTREE,
+            SearchScope.SUBORDINATES);
+    }
+
+    @DataProvider
+    public Iterator<Object[]> valuesDataProvider() {
+        return new DataProviderIterator(SearchScope.values());
+    }
+
+    @Test(dataProvider = "valuesDataProvider")
+    public void valueOfInt(SearchScope val) throws Exception {
+        assertSame(SearchScope.valueOf(val.intValue()), val, val.toString());
+    }
+
+    @Test
+    public void valueOfIntUnknown() throws Exception {
+        int intValue = -1;
+        SearchScope unknown = SearchScope.valueOf(intValue);
+        assertSame(unknown.intValue(), intValue);
+        assertSame(unknown.asEnum(), SearchScope.Enum.UNKNOWN);
+    }
+
+    @Test(dataProvider = "valuesDataProvider")
+    public void valueOfString(SearchScope val) throws Exception {
+        assertSame(SearchScope.valueOf(val.toString()), val);
+    }
+
+    @Test
+    public void valueOfStringUnknown() throws Exception {
+        assertNull(SearchScope.valueOf("unknown"));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtils.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtils.java
new file mode 100644
index 0000000..13134d3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtils.java
@@ -0,0 +1,244 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.forgerock.util.promise.Promise;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.OngoingStubbing;
+
+import org.forgerock.util.time.TimeService;
+
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class defines some utility functions which can be used by test cases.
+ */
+public final class TestCaseUtils {
+    /**
+     * Creates a temporary text file with the specified contents. It will be
+     * marked for automatic deletion when the JVM exits.
+     *
+     * @param lines
+     *            The file contents.
+     * @return The absolute path to the file that was created.
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    public static String createTempFile(final String... lines) throws Exception {
+        final File f = File.createTempFile("LDIFBasedTestCase", ".txt");
+        f.deleteOnExit();
+
+        try (final FileWriter w = new FileWriter(f)) {
+            for (final String s : lines) {
+                w.write(s + System.getProperty("line.separator"));
+            }
+        }
+
+        return f.getAbsolutePath();
+    }
+
+    /**
+     * Return the canonical file path for a test file.
+     * <p>
+     * For example, the path to file "src/test/resources/somedir/somefile" is
+     * obtained with <code>getTestFilePath("somedir/somefile")</code>.
+     *
+     * @param relativePathFromClasspath
+     *            the relative path to any directory that is declared
+     *            in the classpath (typically the src/test/resources
+     *            directory)
+     * @return the canonical path
+     * @throws Exception
+     *             if file is not found or can't be read
+     */
+    public static String getTestFilePath(String relativePathFromClasspath) throws Exception {
+        return new File(TestCaseUtils.class.getClassLoader().getResource(relativePathFromClasspath).toURI())
+                .getCanonicalPath();
+    }
+
+    /**
+     * Finds a free server socket port on the local host.
+     *
+     * @return The free port.
+     */
+    public static InetSocketAddress findFreeSocketAddress() {
+        try (ServerSocket serverLdapSocket = new ServerSocket()) {
+            serverLdapSocket.setReuseAddress(true);
+            serverLdapSocket.bind(new InetSocketAddress("127.0.0.1", 0));
+            return (InetSocketAddress) serverLdapSocket.getLocalSocketAddress();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Returns an internal client connection to the running ldap server.
+     *
+     * @return The internal client connection.
+     * @throws Exception
+     *             When an error occurs.
+     */
+    public static Connection getInternalConnection() throws Exception {
+        startServer();
+        final ConnectionFactory factory =
+                Connections.newInternalConnectionFactory(LDAPServer.getInstance(), null);
+        return factory.getConnection();
+    }
+
+    /**
+     * Starts the test ldap server.
+     *
+     * @throws Exception
+     *             If an error occurs when starting the server.
+     */
+    public static void startServer() throws Exception {
+        LDAPServer.getInstance().start();
+    }
+
+    /**
+     * Stops the test ldap server.
+     */
+    public static void stopServer() {
+        LDAPServer.getInstance().stop();
+    }
+
+    /**
+     * Returns the socket address of the server.
+     *
+     * @return The socket address of the server.
+     */
+    public static InetSocketAddress getServerSocketAddress() {
+        return LDAPServer.getInstance().getSocketAddress();
+    }
+
+    /**
+     * Creates a mock connection factory which will return the provided
+     * connections in order.
+     *
+     * @param first
+     *            The first connection to return.
+     * @param remaining
+     *            The remaining connections to return.
+     * @return The connection factory.
+     */
+    public static ConnectionFactory mockConnectionFactory(final Connection first, final Connection... remaining) {
+        final ConnectionFactory factory = mock(ConnectionFactory.class);
+        try {
+            when(factory.getConnection()).thenReturn(first, remaining);
+        } catch (LdapException ignored) {
+            // Cannot happen.
+        }
+        when(factory.getConnectionAsync()).thenAnswer(new Answer<Promise<Connection, LdapException>>() {
+            @Override
+            public Promise<Connection, LdapException> answer(final InvocationOnMock invocation)
+                    throws Throwable {
+                return newSuccessfulLdapPromise(factory.getConnection());
+            }
+        });
+        return factory;
+    }
+
+    /**
+     * Creates a mock connection which will store connection event listeners in
+     * the provided list.
+     *
+     * @param listeners
+     *            The list which should be used for storing event listeners.
+     * @return The mock connection.
+     */
+    public static Connection mockConnection(final List<ConnectionEventListener> listeners) {
+        final Connection mockConnection = mock(Connection.class);
+
+        // Handle listener registration / deregistration in mock connection.
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.add(listener);
+                return null;
+            }
+        }).when(mockConnection).addConnectionEventListener(any(ConnectionEventListener.class));
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.remove(listener);
+                return null;
+            }
+        }).when(mockConnection).removeConnectionEventListener(any(ConnectionEventListener.class));
+
+        return mockConnection;
+    }
+
+    /**
+     * Returns a mock {@link TimeService} which can be used for injecting fake
+     * time stamps into components.
+     *
+     * @param times
+     *            The times in milli-seconds which should be returned by the
+     *            time source.
+     * @return The mock time source.
+     */
+    public static TimeService mockTimeService(final long... times) {
+        final TimeService mock = mock(TimeService.class);
+        OngoingStubbing<Long> stubbing = when(mock.now());
+        for (long t : times) {
+            stubbing = stubbing.thenReturn(t);
+        }
+        return mock;
+    }
+
+    /**
+     * Fail with precise message giving the exception that was expected.
+     *
+     * @param exceptionClass expected exception
+     */
+    public static void failWasExpected(Class<? extends Throwable> exceptionClass) {
+        fail("should throw an exception " + exceptionClass.getSimpleName());
+    }
+
+    /**
+     * Dynamically change log level using java.util.logging framework.
+     * <p>
+     * slf4j ERROR maps to java.util.logging SEVERE
+     * slf4j INFO maps to java.util.logging INFO
+     * slf4j DEBUG maps to java.util.logging FINE
+     * slf4j TRACE maps to java.util.logging FINEST
+     *
+     * @param level logging level to use
+     */
+    public static void setDefaultLogLevel(Level level) {
+        Logger.getLogger("org.forgerock.opendj.ldap").setLevel(level);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtilsTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtilsTestCase.java
new file mode 100644
index 0000000..ef59a81
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/TestCaseUtilsTestCase.java
@@ -0,0 +1,43 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.TestCaseUtils.mockTimeService;
+
+import org.testng.annotations.Test;
+
+import org.forgerock.util.time.TimeService;
+
+@SuppressWarnings("javadoc")
+public class TestCaseUtilsTestCase extends SdkTestCase {
+
+    /**
+     * Test for {@link #mockTimeSource(long...)}.
+     */
+    @Test
+    public void testMockTimeSource() {
+        final TimeService mock1 = mockTimeService(10);
+        assertThat(mock1.now()).isEqualTo(10);
+        assertThat(mock1.now()).isEqualTo(10);
+
+        final TimeService mock2 = mockTimeService(10, 20, 30);
+        assertThat(mock2.now()).isEqualTo(10);
+        assertThat(mock2.now()).isEqualTo(20);
+        assertThat(mock2.now()).isEqualTo(30);
+        assertThat(mock2.now()).isEqualTo(30);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/controls/ControlsTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/controls/ControlsTestCase.java
new file mode 100644
index 0000000..3d6900c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/controls/ControlsTestCase.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.controls;
+
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all controls unit tests should extend. A control
+ * represents the classes found directly under the package
+ * org.forgerock.opendj.ldap.controls.
+ */
+
+@Test(groups = { "precommit", "controls", "sdk" })
+public abstract class ControlsTestCase extends ForgeRockTestCase {
+    /**
+     * Set up the environment for performing the tests in this suite.
+     *
+     * @throws Exception
+     *             If the environment could not be set up.
+     */
+    @BeforeClass
+    public void setUp() throws Exception {
+        // This test suite depends on having the schema available.
+        TestCaseUtils.startServer();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AbandonRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AbandonRequestTestCase.java
new file mode 100644
index 0000000..80c261a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AbandonRequestTestCase.java
@@ -0,0 +1,77 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests Abandon requests.
+ */
+@SuppressWarnings("javadoc")
+public class AbandonRequestTestCase extends RequestsTestCase {
+    private static final AbandonRequest NEW_ABANDON_REQUEST = Requests.newAbandonRequest(-1);
+    private static final AbandonRequest NEW_ABANDON_REQUEST2 = Requests.newAbandonRequest(0);
+    private static final AbandonRequest NEW_ABANDON_REQUEST3 = Requests.newAbandonRequest(1);
+
+    @DataProvider(name = "abandonRequests")
+    private Object[][] getAbandonRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected AbandonRequest[] newInstance() {
+        return new AbandonRequest[] {
+            NEW_ABANDON_REQUEST,
+            NEW_ABANDON_REQUEST2,
+            NEW_ABANDON_REQUEST3 };
+    }
+
+    @Override
+    protected Request copyOf(final Request original) {
+        return Requests.copyOfAbandonRequest((AbandonRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(final Request original) {
+        return Requests.unmodifiableAbandonRequest((AbandonRequest) original);
+    }
+
+    @Test(dataProvider = "abandonRequests")
+    public void testModifiableRequest(final AbandonRequest original) {
+        final int newReqId = 9999;
+        final AbandonRequest copy = (AbandonRequest) copyOf(original);
+        copy.setRequestID(newReqId);
+        assertThat(copy.getRequestID()).isEqualTo(newReqId);
+        assertThat(original.getRequestID()).isNotEqualTo(newReqId);
+    }
+
+    @Test(dataProvider = "abandonRequests")
+    public void testUnmodifiableRequest(final AbandonRequest original) {
+        final AbandonRequest unmodifiable = (AbandonRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getRequestID()).isEqualTo(original.getRequestID());
+    }
+
+    @Test(dataProvider = "abandonRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetters(final AbandonRequest original) {
+        final AbandonRequest unmodifiable = (AbandonRequest) unmodifiableOf(original);
+        unmodifiable.setRequestID(0);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AddRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AddRequestTestCase.java
new file mode 100644
index 0000000..5098a8d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AddRequestTestCase.java
@@ -0,0 +1,147 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests ADD requests.
+ */
+@SuppressWarnings("javadoc")
+public class AddRequestTestCase extends RequestsTestCase {
+    private static final AddRequest NEW_ADD_REQUEST = Requests.newAddRequest(DN.valueOf("uid=addrequest1"));
+    private static final AddRequest NEW_ADD_REQUEST2 = Requests.newAddRequest("cn=addrequesttestcase");
+    private static final AddRequest NEW_ADD_REQUEST3 = Requests.newAddRequest("dn: ou=People,o=test",
+            "objectClass: top", "objectClass: organizationalUnit", "ou: People");
+
+    @DataProvider(name = "addRequests")
+    private Object[][] getAddRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected AddRequest[] newInstance() {
+        return new AddRequest[] { NEW_ADD_REQUEST, NEW_ADD_REQUEST2, NEW_ADD_REQUEST3 };
+    }
+
+    @Override
+    protected Request copyOf(final Request original) {
+        return Requests.copyOfAddRequest((AddRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(final Request original) {
+        return Requests.unmodifiableAddRequest((AddRequest) original);
+    }
+
+    @Test(dataProvider = "addRequests")
+    public void testModifiableRequest(final AddRequest original) {
+        final String newValue = "uid=newName";
+        final AddRequest copy = (AddRequest) copyOf(original);
+
+        copy.setName(newValue);
+        assertThat(copy.getName().toString()).isEqualTo(newValue);
+        assertThat(original.getName().toString()).isNotEqualTo(newValue);
+
+        copy.addAttribute("cn", "Bob");
+        assertThat(copy.getAttribute("cn")).isNotEmpty();
+        assertThat(original.getAttribute("cn")).isNull();
+
+        copy.clearAttributes();
+        assertThat(copy.getAttribute("cn")).isNull();
+        assertThat(copy.getAttributeCount()).isEqualTo(0);
+
+        copy.addAttribute("sn", "Bobby");
+        assertThat(original.getAttribute("sn")).isNull();
+        assertThat(copy.containsAttribute("sn", "Bobby")).isTrue();
+
+        copy.removeAttribute("sn");
+        assertThat(copy.containsAttribute("sn", "Bobby")).isFalse();
+    }
+
+    @Test(dataProvider = "addRequests")
+    public void testUnmodifiableRequest(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.setName("cn=myexample");
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetDNName(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("cn=mynewexample"));
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddAttribute(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.addAttribute("sn", "Bobby");
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddAttribute2(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.addAttribute(org.forgerock.opendj.ldap.Attributes.emptyAttribute("description"));
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableRemoveAttribute(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.removeAttribute("sn", "Bobby");
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableRemoveAttribute2(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.removeAttribute(AttributeDescription.valueOf("description"));
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableReplaceAttribute(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.replaceAttribute("sn", "cn");
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableReplaceAttribute2(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.replaceAttribute("sn");
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableReplaceAttribute3(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.replaceAttribute(org.forgerock.opendj.ldap.Attributes.emptyAttribute("description"));
+    }
+
+    @Test(dataProvider = "addRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableClearAttribute(final AddRequest original) {
+        final AddRequest unmodifiable = (AddRequest) unmodifiableOf(original);
+        unmodifiable.clearAttributes();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestTestCase.java
new file mode 100644
index 0000000..6d82498
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/AnonymousSASLBindRequestTestCase.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests anonymous SASL bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class AnonymousSASLBindRequestTestCase extends BindRequestTestCase {
+    private static final AnonymousSASLBindRequest NEW_ANONYMOUS_SASL_BIND_REQUEST2 = Requests
+            .newAnonymousSASLBindRequest("test");
+    private static final AnonymousSASLBindRequest NEW_ANONYMOUS_SASL_BIND_REQUEST = Requests
+            .newAnonymousSASLBindRequest("");
+
+    @DataProvider(name = "anonymousSASLBindRequests")
+    private Object[][] getAnonymousSASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected AnonymousSASLBindRequest[] newInstance() {
+        return new AnonymousSASLBindRequest[] { NEW_ANONYMOUS_SASL_BIND_REQUEST, NEW_ANONYMOUS_SASL_BIND_REQUEST2 };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfAnonymousSASLBindRequest((AnonymousSASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableAnonymousSASLBindRequest((AnonymousSASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "anonymousSASLBindRequests")
+    public void testModifiableRequest(final AnonymousSASLBindRequest original) {
+        final String newValue = "MyNewValue";
+        final AnonymousSASLBindRequest copy = (AnonymousSASLBindRequest) copyOf(original);
+        copy.setTraceString(newValue);
+        assertThat(copy.getTraceString()).isEqualTo(newValue);
+        assertThat(copy.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(copy.getName()).isEqualTo(original.getName());
+        assertThat(copy.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+        assertThat(copy.getTraceString()).isNotEqualTo(original.getTraceString());
+    }
+
+    @Test(dataProvider = "anonymousSASLBindRequests")
+    public void testUnmodifiableRequest(final AnonymousSASLBindRequest original) {
+        final AnonymousSASLBindRequest unmodifiable = (AnonymousSASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getName()).isEqualTo(original.getName());
+        assertThat(unmodifiable.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+        assertThat(unmodifiable.getTraceString()).isEqualTo(original.getTraceString());
+    }
+
+    @Test(dataProvider = "anonymousSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableRequestSetter(final AnonymousSASLBindRequest original) {
+        final AnonymousSASLBindRequest unmodifiable = (AnonymousSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setTraceString("the_new_trace_string");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/BindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/BindRequestTestCase.java
new file mode 100644
index 0000000..1302360
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/BindRequestTestCase.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.testng.Assert.assertNotNull;
+
+import org.forgerock.opendj.io.LDAP;
+import org.testng.annotations.Test;
+
+
+/**
+ * Tests the BIND requests.
+ */
+@SuppressWarnings("javadoc")
+public abstract class BindRequestTestCase extends RequestsTestCase {
+    @Test(dataProvider = "createModifiableInstance")
+    public void testAuthType(final BindRequest request) throws Exception {
+        final byte b = request.getAuthenticationType();
+        assertThat(b).isIn(LDAP.TYPE_AUTHENTICATION_SASL, LDAP.TYPE_AUTHENTICATION_SIMPLE);
+    }
+
+    @Test(dataProvider = "createModifiableInstance")
+    public void testBindClient(final BindRequest request) throws Exception {
+        final BindClient client = request.createBindClient("localhost");
+        assertNotNull(client);
+    }
+
+    @Test(dataProvider = "createModifiableInstance")
+    public void testName(final BindRequest request) throws Exception {
+        assertNotNull(request.getName());
+    }
+
+    @Test(dataProvider = "createModifiableInstance")
+    public void testModifiableRequest(final BindRequest original) {
+        final BindRequest copy = (BindRequest) copyOf(original);
+        assertThat(copy.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(copy.getName()).isEqualTo(original.getName());
+    }
+
+    @Test(dataProvider = "createModifiableInstance")
+    public void testUnmodifiableRequest(final BindRequest original) {
+        final BindRequest unmodifiable = (BindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getName()).isEqualTo(original.getName());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestTestCase.java
new file mode 100644
index 0000000..7764565
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CRAMMD5SASLBindRequestTestCase.java
@@ -0,0 +1,108 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests CRAM MD5 SASL bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class CRAMMD5SASLBindRequestTestCase extends BindRequestTestCase {
+
+    private static final CRAMMD5SASLBindRequest NEW_CRAMMD5SASL_BIND_REQUEST = Requests.newCRAMMD5SASLBindRequest(
+            "id1", EMPTY_BYTES);
+    private static final CRAMMD5SASLBindRequest NEW_CRAMMD5SASL_BIND_REQUEST2 = Requests.newCRAMMD5SASLBindRequest(
+            "id2", getBytes("test"));
+
+    @DataProvider(name = "CRAMMD5SASLBindRequests")
+    private Object[][] getCRAMMD5SASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected CRAMMD5SASLBindRequest[] newInstance() {
+        return new CRAMMD5SASLBindRequest[] {
+            NEW_CRAMMD5SASL_BIND_REQUEST,
+            NEW_CRAMMD5SASL_BIND_REQUEST2
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfCRAMMD5SASLBindRequest((CRAMMD5SASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableCRAMMD5SASLBindRequest((CRAMMD5SASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "CRAMMD5SASLBindRequests")
+    public void testModifiableRequest(final CRAMMD5SASLBindRequest original) {
+        final String authId = "newAuthId";
+        final String pwd = "pass";
+        final String pwd2 = "pass2";
+
+        final CRAMMD5SASLBindRequest copy = (CRAMMD5SASLBindRequest) copyOf(original);
+        copy.setAuthenticationID(authId);
+        assertThat(copy.getAuthenticationID()).isEqualTo(authId);
+        assertThat(original.getAuthenticationID()).isNotEqualTo(authId);
+
+        copy.setPassword(pwd.toCharArray());
+        assertThat(copy.getPassword()).isEqualTo(pwd.getBytes());
+        assertThat(original.getPassword()).isNotEqualTo(pwd.getBytes());
+
+        copy.setPassword(pwd2.getBytes());
+        assertThat(copy.getPassword()).isEqualTo(pwd2.getBytes());
+        assertThat(original.getPassword()).isNotEqualTo(pwd2.getBytes());
+    }
+
+    @Test(dataProvider = "CRAMMD5SASLBindRequests")
+    public void testUnmodifiableRequest(final CRAMMD5SASLBindRequest original) {
+        final CRAMMD5SASLBindRequest unmodifiable = (CRAMMD5SASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthenticationID()).isEqualTo(original.getAuthenticationID());
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getName()).isEqualTo(original.getName());
+        assertThat(unmodifiable.getPassword()).isEqualTo(original.getPassword());
+        assertThat(unmodifiable.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+    }
+
+    @Test(dataProvider = "CRAMMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationId(final CRAMMD5SASLBindRequest original) {
+        final CRAMMD5SASLBindRequest unmodifiable = (CRAMMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "CRAMMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword(final CRAMMD5SASLBindRequest original) {
+        final CRAMMD5SASLBindRequest unmodifiable = (CRAMMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".getBytes());
+    }
+
+    @Test(dataProvider = "CRAMMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword2(final CRAMMD5SASLBindRequest original) {
+        final CRAMMD5SASLBindRequest unmodifiable = (CRAMMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".toCharArray());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestTestCase.java
new file mode 100644
index 0000000..5fbd8ee
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CancelExtendedRequestTestCase.java
@@ -0,0 +1,102 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests CANCELEXTENDED requests.
+ */
+@SuppressWarnings("javadoc")
+public class CancelExtendedRequestTestCase extends RequestsTestCase {
+    private static final CancelExtendedRequest NEW_CANCELEXTENDED_REQUEST = Requests.newCancelExtendedRequest(-1);
+    private static final CancelExtendedRequest NEW_CANCELEXTENDED_REQUEST2 = Requests.newCancelExtendedRequest(0);
+    private static final CancelExtendedRequest NEW_CANCELEXTENDED_REQUEST3 = Requests.newCancelExtendedRequest(1);
+
+    @DataProvider(name = "cancelExtendedRequests")
+    private Object[][] getCancelExtendedRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected CancelExtendedRequest[] newInstance() {
+        return new CancelExtendedRequest[] {
+            NEW_CANCELEXTENDED_REQUEST,
+            NEW_CANCELEXTENDED_REQUEST2,
+            NEW_CANCELEXTENDED_REQUEST3 };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfCancelExtendedRequest((CancelExtendedRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableCancelExtendedRequest((CancelExtendedRequest) original);
+    }
+
+
+    @Test(dataProvider = "cancelExtendedRequests")
+    public void testModifiableRequest(final CancelExtendedRequest original) {
+        final int newReqId = 9999;
+        final CancelExtendedRequest copy = (CancelExtendedRequest) copyOf(original);
+        copy.setRequestID(newReqId);
+        assertThat(copy.getRequestID()).isEqualTo(newReqId);
+        assertThat(copy.getOID()).isEqualTo(original.getOID());
+        assertThat(original.getRequestID()).isNotEqualTo(newReqId);
+    }
+
+    @Test(dataProvider = "cancelExtendedRequests")
+    public void testUnmodifiableRequest(final CancelExtendedRequest original) {
+        final CancelExtendedRequest unmodifiable = (CancelExtendedRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getRequestID()).isEqualTo(original.getRequestID());
+        assertThat(unmodifiable.getOID()).isEqualTo(original.getOID());
+        assertThat(unmodifiable.getResultDecoder()).isEqualTo(original.getResultDecoder());
+        assertThat(unmodifiable.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test(dataProvider = "cancelExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetRequestId(final CancelExtendedRequest original) {
+        final CancelExtendedRequest unmodifiable = (CancelExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setRequestID(99);
+    }
+
+    @Test(dataProvider = "cancelExtendedRequests")
+    public void testModifiableRequestDecode(final CancelExtendedRequest original) throws DecodeException {
+        final GenericControl control = GenericControl.newControl("1.2.3".intern());
+
+        final CancelExtendedRequest copy = (CancelExtendedRequest) copyOf(original);
+        copy.setRequestID(99);
+        copy.addControl(control);
+        assertThat(original.getControls().contains(control)).isFalse();
+
+        try {
+            final CancelExtendedRequest decoded = CancelExtendedRequest.DECODER.decodeExtendedRequest(copy,
+                    new DecodeOptions());
+            assertThat(decoded.getControls().contains(control)).isTrue();
+        } catch (DecodeException e) {
+            throw e;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CompareRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CompareRequestTestCase.java
new file mode 100644
index 0000000..f91b712
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/CompareRequestTestCase.java
@@ -0,0 +1,109 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests compare requests.
+ */
+@SuppressWarnings("javadoc")
+public class CompareRequestTestCase extends RequestsTestCase {
+    private static final CompareRequest NEW_COMPARE_REQUEST2 = Requests.newCompareRequest(
+            "uid=user.0,ou=people,o=test", "uid", "user.0");
+    private static final CompareRequest NEW_COMPARE_REQUEST = Requests.newCompareRequest("uid=user.0,ou=people,o=test",
+            "cn", "user.0");
+
+    @DataProvider(name = "compareRequests")
+    private Object[][] getCompareRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected CompareRequest[] newInstance() {
+        return new CompareRequest[] {
+            NEW_COMPARE_REQUEST,
+            NEW_COMPARE_REQUEST2 };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfCompareRequest((CompareRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableCompareRequest((CompareRequest) original);
+    }
+
+    @Test(dataProvider = "compareRequests")
+    public void testModifiableRequest(final CompareRequest original) {
+        final String newValue = "uid=user.0";
+        final String attrDescription = "newattributedescription";
+        final CompareRequest copy = (CompareRequest) copyOf(original);
+        copy.setName(newValue);
+        copy.setAttributeDescription(attrDescription);
+        assertThat(copy.getName().toString()).isEqualTo(newValue);
+        assertThat(original.getName().toString()).isNotEqualTo(newValue);
+        assertThat(copy.getAttributeDescription().toString()).isEqualTo(attrDescription);
+        assertThat(original.getAttributeDescription()).isNotEqualTo(attrDescription);
+    }
+
+    @Test(dataProvider = "compareRequests")
+    public void testUnmodifiableRequest(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAssertionValue()).isEqualTo(original.getAssertionValue());
+        assertThat(unmodifiable.getAttributeDescription()).isEqualTo(original.getAttributeDescription());
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+    }
+
+    @Test(dataProvider = "compareRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAssertionValue(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        unmodifiable.setAssertionValue("newValue");
+    }
+
+    @Test(dataProvider = "compareRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAttributeDescription(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        unmodifiable.setAttributeDescription(AttributeDescription.valueOf("sn"));
+    }
+
+    @Test(dataProvider = "compareRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAttributeDescription2(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        unmodifiable.setAttributeDescription("sn");
+    }
+
+    @Test(dataProvider = "compareRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=user.0");
+    }
+
+    @Test(dataProvider = "compareRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final CompareRequest original) {
+        final CompareRequest unmodifiable = (CompareRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("uid=user.0"));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DeleteRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DeleteRequestTestCase.java
new file mode 100644
index 0000000..acc83ce
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DeleteRequestTestCase.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DN;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the delete request.
+ */
+@SuppressWarnings("javadoc")
+public class DeleteRequestTestCase extends RequestsTestCase {
+
+    private static final DeleteRequest NEW_DELETE_REQUEST = Requests.newDeleteRequest(DN.valueOf("uid=Deleterequest1"));
+    private static final DeleteRequest NEW_DELETE_REQUEST2 = Requests.newDeleteRequest("cn=Deleterequesttestcase");
+    private static final DeleteRequest NEW_DELETE_REQUEST3 = Requests.newDeleteRequest("uid=user.999,ou=people,o=test");
+
+    @DataProvider(name = "deleteRequests")
+    private Object[][] getDeleteRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected DeleteRequest[] newInstance() {
+        return new DeleteRequest[] {
+            NEW_DELETE_REQUEST,
+            NEW_DELETE_REQUEST2,
+            NEW_DELETE_REQUEST3
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfDeleteRequest((DeleteRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableDeleteRequest((DeleteRequest) original);
+    }
+
+    @Test(dataProvider = "deleteRequests")
+    public void testModifiableRequest(final DeleteRequest original) {
+        final String newValue = "uid=newName";
+        final DeleteRequest copy = (DeleteRequest) copyOf(original);
+
+        copy.setName(newValue);
+        assertThat(copy.getName().toString()).isEqualTo(newValue);
+        assertThat(original.getName().toString()).isNotEqualTo(newValue);
+    }
+
+    @Test(dataProvider = "deleteRequests")
+    public void testUnmodifiableRequest(final DeleteRequest original) {
+        final DeleteRequest unmodifiable = (DeleteRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+    }
+
+    @Test(dataProvider = "deleteRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final DeleteRequest original) {
+        final DeleteRequest unmodifiable = (DeleteRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "deleteRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final DeleteRequest original) {
+        final DeleteRequest unmodifiable = (DeleteRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("uid=scarter,ou=people,dc=example,dc=com"));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestTestCase.java
new file mode 100644
index 0000000..2ce6932
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/DigestMD5SASLBindRequestTestCase.java
@@ -0,0 +1,203 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests Digest MD5 SASL requests.
+ */
+@SuppressWarnings("javadoc")
+public class DigestMD5SASLBindRequestTestCase extends BindRequestTestCase {
+    private static final DigestMD5SASLBindRequest NEW_DIGEST_MD5SASL_BIND_REQUEST = Requests
+            .newDigestMD5SASLBindRequest("id1", EMPTY_BYTES);
+    private static final DigestMD5SASLBindRequest NEW_DIGEST_MD5SASL_BIND_REQUEST2 = Requests
+            .newDigestMD5SASLBindRequest("id2", getBytes("password"));
+
+    @DataProvider(name = "DigestMD5SASLBindRequests")
+    private Object[][] getDigestMD5SASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected DigestMD5SASLBindRequest[] newInstance() {
+        return new DigestMD5SASLBindRequest[] {
+            NEW_DIGEST_MD5SASL_BIND_REQUEST,
+            NEW_DIGEST_MD5SASL_BIND_REQUEST2
+        };
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testQOP(DigestMD5SASLBindRequest request) throws Exception {
+        String[] options =
+                new String[] { DigestMD5SASLBindRequest.QOP_AUTH,
+                    DigestMD5SASLBindRequest.QOP_AUTH_INT, DigestMD5SASLBindRequest.QOP_AUTH_CONF };
+        request.addQOP(options);
+        assertEquals(request.getQOPs(), Arrays.asList(options));
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testStrength(DigestMD5SASLBindRequest request) throws Exception {
+        request.setCipher(DigestMD5SASLBindRequest.CIPHER_3DES);
+        assertEquals(request.getCipher(), DigestMD5SASLBindRequest.CIPHER_3DES);
+
+        request.setCipher(DigestMD5SASLBindRequest.CIPHER_MEDIUM);
+        assertEquals(request.getCipher(), DigestMD5SASLBindRequest.CIPHER_MEDIUM);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testServerAuth(DigestMD5SASLBindRequest request) throws Exception {
+        request.setServerAuth(true);
+        assertEquals(request.isServerAuth(), true);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testSendBuffer(DigestMD5SASLBindRequest request) throws Exception {
+        request.setMaxSendBufferSize(1024);
+        assertEquals(request.getMaxSendBufferSize(), 1024);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testRecieveBuffer(DigestMD5SASLBindRequest request) throws Exception {
+        request.setMaxReceiveBufferSize(1024);
+        assertEquals(request.getMaxReceiveBufferSize(), 1024);
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfDigestMD5SASLBindRequest((DigestMD5SASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableDigestMD5SASLBindRequest((DigestMD5SASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testModifiableRequest(final DigestMD5SASLBindRequest original) {
+        final String authID = "u:user.0";
+        final String azID = "dn:user.0,dc=com";
+        final String cipher = DigestMD5SASLBindRequest.CIPHER_LOW;
+        final String password = "pass";
+        final int maxRBufferSize = 1024;
+        final int maxSBufferSize = 2048;
+        final String realm = "my.domain.com";
+
+        final DigestMD5SASLBindRequest copy = (DigestMD5SASLBindRequest) copyOf(original);
+        copy.setAuthenticationID(authID);
+        copy.setAuthorizationID(azID);
+        copy.setCipher(cipher);
+        copy.setPassword(password.toCharArray());
+        copy.setMaxReceiveBufferSize(maxRBufferSize);
+        copy.setMaxSendBufferSize(maxSBufferSize);
+        copy.setRealm(realm);
+        copy.setServerAuth(true);
+
+        assertThat(copy.getAuthenticationID()).isEqualTo(authID);
+        assertThat(copy.getAuthorizationID()).isEqualTo(azID);
+        assertThat(copy.getCipher()).isEqualTo(cipher);
+        assertThat(copy.getMaxReceiveBufferSize()).isEqualTo(maxRBufferSize);
+        assertThat(copy.getMaxSendBufferSize()).isEqualTo(maxSBufferSize);
+        assertThat(copy.getRealm()).isEqualTo(realm);
+        assertThat(original.getRealm()).isNotEqualTo(realm);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests")
+    public void testUnmodifiableRequest(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthorizationID()).isEqualTo(original.getAuthorizationID());
+        assertThat(unmodifiable.getCipher()).isEqualTo(original.getCipher());
+        assertThat(unmodifiable.getMaxReceiveBufferSize()).isEqualTo(original.getMaxReceiveBufferSize());
+        assertThat(unmodifiable.getMaxSendBufferSize()).isEqualTo(original.getMaxSendBufferSize());
+        assertThat(unmodifiable.getRealm()).isEqualTo(original.getRealm());
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddAdditionalAuthParam(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.addAdditionalAuthParam("id2", "value");
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddQOP(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.addQOP(DigestMD5SASLBindRequest.QOP_AUTH, DigestMD5SASLBindRequest.QOP_AUTH_CONF);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationID(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthorizationID(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthorizationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetCipher(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setCipher(DigestMD5SASLBindRequest.CIPHER_LOW);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetMaxReceiveBufferSize(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setMaxReceiveBufferSize(1048);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetMaxSendBufferSize(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setMaxSendBufferSize(1048);
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".getBytes());
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword2(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("passworda".toCharArray());
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetRealm(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setRealm("my.domain");
+    }
+
+    @Test(dataProvider = "DigestMD5SASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetServerAuth(final DigestMD5SASLBindRequest original) {
+        final DigestMD5SASLBindRequest unmodifiable = (DigestMD5SASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setServerAuth(true);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExtendedRequestTestCase.java
new file mode 100644
index 0000000..0ccda7c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExtendedRequestTestCase.java
@@ -0,0 +1,35 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.testng.Assert.assertNotNull;
+
+import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
+import org.testng.annotations.Test;
+
+/**
+ * Tests various extended requests.
+ */
+@SuppressWarnings("javadoc")
+public abstract class ExtendedRequestTestCase extends RequestsTestCase {
+
+    @Test(dataProvider = "ExtendedRequests")
+    public void testDecoder(final ExtendedRequest<?> request) throws Exception {
+        final ExtendedResultDecoder<?> decoder = request.getResultDecoder();
+        assertNotNull(decoder);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestTestCase.java
new file mode 100644
index 0000000..719b86c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ExternalSASLBindRequestTestCase.java
@@ -0,0 +1,75 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the external SASL Bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class ExternalSASLBindRequestTestCase extends BindRequestTestCase {
+    private static final ExternalSASLBindRequest NEW_EXTERNAL_SASL_BIND_REQUEST = Requests.newExternalSASLBindRequest();
+
+    @DataProvider(name = "ExternalSASLBindRequests")
+    private Object[][] getExternalSASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected ExternalSASLBindRequest[] newInstance() {
+        return new ExternalSASLBindRequest[] {
+            NEW_EXTERNAL_SASL_BIND_REQUEST };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfExternalSASLBindRequest((ExternalSASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableExternalSASLBindRequest((ExternalSASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "ExternalSASLBindRequests")
+    public void testModifiableRequest(final ExternalSASLBindRequest original) {
+        final String authID = "u:user.0";
+        final ExternalSASLBindRequest copy = (ExternalSASLBindRequest) copyOf(original);
+        copy.setAuthorizationID(authID);
+        assertThat(copy.getAuthorizationID()).isEqualTo(authID);
+        assertThat(copy.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+    }
+
+    @Test(dataProvider = "ExternalSASLBindRequests")
+    public void testUnmodifiableRequest(final ExternalSASLBindRequest original) {
+        final ExternalSASLBindRequest unmodifiable = (ExternalSASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthorizationID()).isEqualTo(original.getAuthorizationID());
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+    }
+
+    @Test(dataProvider = "ExternalSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthorizationID(final ExternalSASLBindRequest original) {
+        final ExternalSASLBindRequest unmodifiable = (ExternalSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthorizationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestTestCase.java
new file mode 100644
index 0000000..c0b2aae
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GSSAPISASLBindRequestTestCase.java
@@ -0,0 +1,201 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import javax.security.auth.Subject;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests GSSAPI SASL Bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class GSSAPISASLBindRequestTestCase extends BindRequestTestCase {
+
+    private static final GSSAPISASLBindRequest NEW_GSSAPISASL_BIND_REQUEST = Requests.newGSSAPISASLBindRequest("id1",
+            EMPTY_BYTES);
+    private static final GSSAPISASLBindRequest NEW_GSSAPISASL_BIND_REQUEST2 = Requests.newGSSAPISASLBindRequest("id2",
+            getBytes("password"));
+
+    @DataProvider(name = "GSSAPISASLBindRequests")
+    private Object[][] getGSSAPISASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected GSSAPISASLBindRequest[] newInstance() {
+        return new GSSAPISASLBindRequest[] {
+            NEW_GSSAPISASL_BIND_REQUEST,
+            NEW_GSSAPISASL_BIND_REQUEST2
+        };
+    }
+
+    @Override
+    @Test(enabled = false)
+    public void testBindClient(BindRequest request) throws Exception {
+        // Should setup a test krb server...
+        super.testBindClient(request);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testQOP(GSSAPISASLBindRequest request) throws Exception {
+        String[] options =
+                new String[] { GSSAPISASLBindRequest.QOP_AUTH, GSSAPISASLBindRequest.QOP_AUTH_INT,
+                    GSSAPISASLBindRequest.QOP_AUTH_CONF };
+        request.addQOP(options);
+        assertEquals(request.getQOPs(), Arrays.asList(options));
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testServerAuth(GSSAPISASLBindRequest request) throws Exception {
+        request.setServerAuth(true);
+        assertEquals(request.isServerAuth(), true);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testSendBuffer(GSSAPISASLBindRequest request) throws Exception {
+        request.setMaxSendBufferSize(512);
+        assertEquals(request.getMaxSendBufferSize(), 512);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testRecieveBuffer(GSSAPISASLBindRequest request) throws Exception {
+        request.setMaxReceiveBufferSize(512);
+        assertEquals(request.getMaxReceiveBufferSize(), 512);
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfGSSAPISASLBindRequest((GSSAPISASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableGSSAPISASLBindRequest((GSSAPISASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testModifiableRequest(final GSSAPISASLBindRequest original) {
+        final String authID = "u:user.0";
+        final String azID = "dn:user.0,dc=com";
+        final String password = "pass";
+        final int maxRBufferSize = 1024;
+        final int maxSBufferSize = 2048;
+        final String realm = "my.domain.com";
+        final Subject subject = new Subject();
+        subject.setReadOnly();
+
+        final GSSAPISASLBindRequest copy = (GSSAPISASLBindRequest) copyOf(original);
+        copy.setAuthenticationID(authID);
+        copy.setAuthorizationID(azID);
+        copy.setPassword(password.toCharArray());
+        copy.setMaxReceiveBufferSize(maxRBufferSize);
+        copy.setMaxSendBufferSize(maxSBufferSize);
+        copy.setRealm(realm);
+        copy.setServerAuth(true);
+        copy.setSubject(subject);
+
+        assertThat(copy.getAuthenticationID()).isEqualTo(authID);
+        assertThat(copy.getAuthorizationID()).isEqualTo(azID);
+        assertThat(copy.getMaxReceiveBufferSize()).isEqualTo(maxRBufferSize);
+        assertThat(copy.getMaxSendBufferSize()).isEqualTo(maxSBufferSize);
+        assertThat(copy.getRealm()).isEqualTo(realm);
+        assertThat(original.getRealm()).isNotEqualTo(realm);
+        assertThat(copy.getSubject()).isEqualTo(subject);
+        assertThat(original.getSubject()).isNotEqualTo(subject);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests")
+    public void testUnmodifiableRequest(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthorizationID()).isEqualTo(original.getAuthorizationID());
+        assertThat(unmodifiable.getMaxReceiveBufferSize()).isEqualTo(original.getMaxReceiveBufferSize());
+        assertThat(unmodifiable.getMaxSendBufferSize()).isEqualTo(original.getMaxSendBufferSize());
+        assertThat(unmodifiable.getRealm()).isEqualTo(original.getRealm());
+        assertThat(unmodifiable.getSubject()).isEqualTo(original.getSubject());
+        assertThat(unmodifiable.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddAdditionalAuthParam(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.addAdditionalAuthParam("id2", "value");
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddQOP(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.addQOP(GSSAPISASLBindRequest.QOP_AUTH, GSSAPISASLBindRequest.QOP_AUTH_CONF);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationID(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthorizationID(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthorizationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetMaxReceiveBufferSize(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setMaxReceiveBufferSize(1048);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetMaxSendBufferSize(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setMaxSendBufferSize(1048);
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".getBytes());
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword2(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".toCharArray());
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetRealm(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setRealm("my.domain");
+    }
+
+    @Test(dataProvider = "GSSAPISASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetServerAuth(final GSSAPISASLBindRequest original) {
+        final GSSAPISASLBindRequest unmodifiable = (GSSAPISASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setServerAuth(true);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericBindRequestTestCase.java
new file mode 100644
index 0000000..3732dfb
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericBindRequestTestCase.java
@@ -0,0 +1,105 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.io.LDAP;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+
+/**
+ * Tests Generic Bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class GenericBindRequestTestCase extends BindRequestTestCase {
+
+    private static final GenericBindRequest NEW_GENERIC_BIND_REQUEST = Requests.newGenericBindRequest(
+            LDAP.TYPE_AUTHENTICATION_SASL, EMPTY_BYTES);
+    private static final GenericBindRequest NEW_GENERIC_BIND_REQUEST2 = Requests.newGenericBindRequest(
+            LDAP.TYPE_AUTHENTICATION_SIMPLE, getBytes("password"));
+    private static final GenericBindRequest NEW_GENERIC_BIND_REQUEST3 = Requests.newGenericBindRequest("username",
+            LDAP.TYPE_AUTHENTICATION_SIMPLE, getBytes("password"));
+
+    @DataProvider(name = "GenericBindRequests")
+    private Object[][] getGenericBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected GenericBindRequest[] newInstance() {
+        return new GenericBindRequest[] {
+            NEW_GENERIC_BIND_REQUEST,
+            NEW_GENERIC_BIND_REQUEST2,
+            NEW_GENERIC_BIND_REQUEST3
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfGenericBindRequest((GenericBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableGenericBindRequest((GenericBindRequest) original);
+    }
+
+    @Test(dataProvider = "GenericBindRequests")
+    public void testModifiableRequest(final GenericBindRequest original) {
+        final String password = "pass";
+        final String newName = "uid:user.0";
+
+        final GenericBindRequest copy = (GenericBindRequest) copyOf(original);
+        copy.setAuthenticationType((byte) 0);
+        copy.setAuthenticationValue(password.getBytes());
+        copy.setName(newName);
+
+        assertThat(copy.getAuthenticationType()).isEqualTo((byte) 0);
+        assertThat(original.getAuthenticationType()).isNotEqualTo((byte) 0);
+        assertThat(copy.getAuthenticationValue()).isEqualTo(password.getBytes());
+    }
+
+    @Test(dataProvider = "GenericBindRequests")
+    public void testUnmodifiableRequest(final GenericBindRequest original) {
+        final GenericBindRequest unmodifiable = (GenericBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getAuthenticationValue()).isEqualTo(original.getAuthenticationValue());
+    }
+
+    @Test(dataProvider = "GenericBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final GenericBindRequest original) {
+        final GenericBindRequest unmodifiable = (GenericBindRequest) unmodifiableOf(original);
+        unmodifiable.setName("");
+    }
+
+    @Test(dataProvider = "GenericBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationType(final GenericBindRequest original) {
+        final GenericBindRequest unmodifiable = (GenericBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationType((byte) 0xA3);
+    }
+
+    @Test(dataProvider = "GenericBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationValue(final GenericBindRequest original) {
+        final GenericBindRequest unmodifiable = (GenericBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationValue(null);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestTestCase.java
new file mode 100644
index 0000000..7555c35
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/GenericExtendedRequestTestCase.java
@@ -0,0 +1,104 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests GENERICEXTENDED requests.
+ */
+@SuppressWarnings("javadoc")
+public class GenericExtendedRequestTestCase extends RequestsTestCase {
+    private static final GenericExtendedRequest NEW_GENERICEXTENDED_REQUEST = Requests
+            .newGenericExtendedRequest("Generic1");
+    private static final GenericExtendedRequest NEW_GENERICEXTENDED_REQUEST2 = Requests
+            .newGenericExtendedRequest("Generic2");
+    private static final GenericExtendedRequest NEW_GENERICEXTENDED_REQUEST3 = Requests
+            .newGenericExtendedRequest("Generic3");
+
+    @DataProvider(name = "GenericExtendedRequests")
+    private Object[][] getGenericExtendedRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected GenericExtendedRequest[] newInstance() {
+        return new GenericExtendedRequest[] {
+            NEW_GENERICEXTENDED_REQUEST,
+            NEW_GENERICEXTENDED_REQUEST2,
+            NEW_GENERICEXTENDED_REQUEST3 };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfGenericExtendedRequest((GenericExtendedRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableGenericExtendedRequest((GenericExtendedRequest) original);
+    }
+
+    @Test(dataProvider = "GenericExtendedRequests")
+    public void testModifiableRequest(final GenericExtendedRequest original) {
+        final String newOID = "1.2.3.99";
+        final String newValue = "newValue";
+
+        final GenericExtendedRequest copy = (GenericExtendedRequest) copyOf(original);
+        copy.setOID(newOID);
+        copy.setValue(newValue);
+        assertThat(copy.getOID()).isEqualTo(newOID);
+        assertThat(original.getOID()).isNotEqualTo(newOID);
+    }
+
+    @Test(dataProvider = "GenericExtendedRequests")
+    public void testUnmodifiableRequest(final GenericExtendedRequest original) {
+        final GenericExtendedRequest unmodifiable = (GenericExtendedRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getOID()).isEqualTo(original.getOID());
+        assertThat(unmodifiable.getValue()).isEqualTo(original.getValue());
+        assertThat(unmodifiable.getResultDecoder()).isEqualTo(original.getResultDecoder());
+    }
+
+    @Test(dataProvider = "GenericExtendedRequests")
+    public void testModifiableRequestDecode(final GenericExtendedRequest original) throws DecodeException {
+        final String oid = "1.2.3.4";
+        final String value = "myValue";
+        final GenericControl control = GenericControl.newControl("1.2.3".intern());
+
+        final GenericExtendedRequest copy = (GenericExtendedRequest) copyOf(original);
+        copy.setOID(oid);
+        copy.setValue(value);
+        copy.addControl(control);
+
+        try {
+            GenericExtendedRequest decoded = GenericExtendedRequest.DECODER.decodeExtendedRequest(copy,
+                    new DecodeOptions());
+            assertThat(decoded.getOID()).isEqualTo(oid);
+            assertThat(decoded.getValue()).isEqualTo(ByteString.valueOfUtf8(value));
+            assertThat(decoded.getControls().contains(control)).isTrue();
+        } catch (DecodeException e) {
+            throw e;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestTestCase.java
new file mode 100644
index 0000000..abdc7a6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyDNRequestTestCase.java
@@ -0,0 +1,125 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.RDN;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the Modify DN requests.
+ */
+@SuppressWarnings("javadoc")
+public class ModifyDNRequestTestCase extends RequestsTestCase {
+
+    private static final ModifyDNRequest NEW_MODIFY_DN_REQUEST = Requests.newModifyDNRequest(
+            "uid=user.100,ou=people,o=test", "uid=100.user");
+    private static final ModifyDNRequest NEW_MODIFY_DN_REQUEST2 = Requests.newModifyDNRequest(
+            "cn=ModifyDNrequesttestcase", "cn=xyz");
+
+    @DataProvider(name = "ModifyDNRequests")
+    private Object[][] getModifyDNRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected ModifyDNRequest[] newInstance() {
+        return new ModifyDNRequest[] {
+            NEW_MODIFY_DN_REQUEST,
+            NEW_MODIFY_DN_REQUEST2,
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfModifyDNRequest((ModifyDNRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableModifyDNRequest((ModifyDNRequest) original);
+    }
+
+    @Test(dataProvider = "ModifyDNRequests")
+    public void testModifiableRequest(final ModifyDNRequest original) {
+        final String newDN = "cn=Ted,ou=People,dc=example,dc=com";
+        final String superior = "ou=People,dc=example,dc=org";
+
+        final ModifyDNRequest copy = (ModifyDNRequest) copyOf(original);
+        copy.setName(DN.valueOf(newDN));
+        copy.setDeleteOldRDN(true);
+        copy.setNewSuperior(superior);
+        assertThat(copy.getName().toString()).isEqualTo(newDN);
+        assertThat(copy.getNewSuperior().toString()).isEqualTo(superior);
+        assertThat(original.getNewSuperior()).isNull();
+        assertThat(copy.toString()).contains("deleteOldRDN=true");
+    }
+
+    @Test(dataProvider = "ModifyDNRequests")
+    public void testUnmodifiableRequest(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+        assertThat(original.getNewSuperior()).isNull();
+        assertThat(unmodifiable.getNewSuperior()).isNull();
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetDeleteOldRDN(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setDeleteOldRDN(true);
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("uid=scarter,ou=people,dc=example,dc=com"));
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewRDN(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setNewRDN("dc=org");
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewRDN2(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setNewRDN(RDN.valueOf("dc=org"));
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewSuperior(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setNewSuperior("ou=people2,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "ModifyDNRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewSuperior2(final ModifyDNRequest original) {
+        final ModifyDNRequest unmodifiable = (ModifyDNRequest) unmodifiableOf(original);
+        unmodifiable.setNewSuperior(DN.valueOf("ou=people2,dc=example,dc=com"));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyRequestTestCase.java
new file mode 100644
index 0000000..97d2cca
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/ModifyRequestTestCase.java
@@ -0,0 +1,109 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the modify request.
+ */
+@SuppressWarnings("javadoc")
+public class ModifyRequestTestCase extends RequestsTestCase {
+
+    private static final ModifyRequest ADD_MODIFICATION = Requests.newModifyRequest(DN.valueOf("uid=Modifyrequest1"))
+            .addModification(ModificationType.ADD, "userpassword", "password");
+    private static final ModifyRequest ADD_MODIFICATION2 = Requests.newModifyRequest("cn=Modifyrequesttestcase")
+            .addModification(ModificationType.ADD, "userpassword", "password");
+    private static final ModifyRequest NEW_MODIFY_REQUEST = Requests.newModifyRequest("dn: ou=People,o=test",
+            "changetype: modify", "add: userpassword", "userpassword: password");
+
+    @DataProvider(name = "ModifyRequests")
+    private Object[][] getModifyRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected ModifyRequest[] newInstance() {
+        return new ModifyRequest[] {
+            ADD_MODIFICATION,
+            ADD_MODIFICATION2,
+            NEW_MODIFY_REQUEST
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfModifyRequest((ModifyRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableModifyRequest((ModifyRequest) original);
+    }
+
+    @Test(dataProvider = "ModifyRequests")
+    public void testModifiableRequest(final ModifyRequest original) {
+        final String newDN = "cn=Ted,ou=People,dc=example,dc=com";
+
+        final ModifyRequest copy = (ModifyRequest) copyOf(original);
+        copy.setName(DN.valueOf(newDN));
+        assertThat(copy.getName().toString()).isEqualTo(newDN);
+        assertThat(copy.toString()).contains("dn=" + newDN);
+    }
+
+    @Test(dataProvider = "ModifyRequests")
+    public void testUnmodifiableRequest(final ModifyRequest original) {
+        final ModifyRequest unmodifiable = (ModifyRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+        assertThat(unmodifiable.getModifications().size()).isEqualTo(original.getModifications().size());
+
+    }
+
+    @Test(dataProvider = "ModifyRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddModification(final ModifyRequest original) {
+        final ModifyRequest unmodifiable = (ModifyRequest) unmodifiableOf(original);
+        unmodifiable.addModification(ModificationType.ADD, "member",
+                DN.valueOf("uid=scarter,ou=people,dc=example,dc=com"));
+    }
+
+    @Test(dataProvider = "ModifyRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddModification2(final ModifyRequest original) {
+        final ModifyRequest unmodifiable = (ModifyRequest) unmodifiableOf(original);
+        unmodifiable.addModification(new Modification(ModificationType.ADD,
+                new LinkedAttribute("description", "value1")));
+    }
+
+    @Test(dataProvider = "ModifyRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final ModifyRequest original) {
+        final ModifyRequest unmodifiable = (ModifyRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("uid=scarter,ou=people,dc=example,dc=com"));
+    }
+
+    @Test(dataProvider = "ModifyRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final ModifyRequest original) {
+        final ModifyRequest unmodifiable = (ModifyRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=scarter,ou=people,dc=example,dc=com");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestTestCase.java
new file mode 100644
index 0000000..d5c8cd6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PasswordModifyExtendedRequestTestCase.java
@@ -0,0 +1,140 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests PASSWORDMODIFYEXTENDED requests.
+ */
+@SuppressWarnings("javadoc")
+public class PasswordModifyExtendedRequestTestCase extends RequestsTestCase {
+    @DataProvider(name = "passwordModifyExtendedRequests")
+    private Object[][] getPasswordModifyExtendedRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected PasswordModifyExtendedRequest[] newInstance() {
+        return new PasswordModifyExtendedRequest[] { Requests.newPasswordModifyExtendedRequest() };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfPasswordModifyExtendedRequest((PasswordModifyExtendedRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiablePasswordModifyExtendedRequest((PasswordModifyExtendedRequest) original);
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests")
+    public void testModifiableRequest(final PasswordModifyExtendedRequest original) {
+        final String password = "password";
+        final String oldPassword = "oldPassword";
+        final String userIdentity = "user.0";
+
+        final PasswordModifyExtendedRequest copy = (PasswordModifyExtendedRequest) copyOf(original);
+        copy.setNewPassword(password.toCharArray());
+        copy.setOldPassword(oldPassword.toCharArray());
+        copy.setUserIdentity(ByteString.valueOfUtf8(userIdentity));
+
+        assertThat(copy.getNewPassword()).isEqualTo(password.getBytes());
+        assertThat(original.getNewPassword()).isNull();
+        assertThat(copy.getOldPassword()).isEqualTo(oldPassword.getBytes());
+        assertThat(original.getOldPassword()).isNull();
+        assertThat(copy.getUserIdentityAsString()).isEqualTo(userIdentity);
+        assertThat(original.getUserIdentityAsString()).isNull();
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests")
+    public void testModifiableRequestDecode(final PasswordModifyExtendedRequest original) throws DecodeException {
+        final String password = "";
+        final String oldPassword = "old";
+        final String userIdentity = "uid=scarter,ou=people,dc=example,dc=com";
+        final GenericControl control = GenericControl.newControl("1.2.3".intern());
+
+        final PasswordModifyExtendedRequest copy = (PasswordModifyExtendedRequest) copyOf(original);
+        copy.setNewPassword(password.toCharArray());
+        copy.setUserIdentity(userIdentity);
+        copy.setOldPassword(oldPassword.getBytes());
+        copy.addControl(control);
+
+        try {
+            PasswordModifyExtendedRequest decoded = PasswordModifyExtendedRequest.DECODER.decodeExtendedRequest(copy,
+                    new DecodeOptions());
+            assertThat(decoded.getNewPassword()).isEqualTo(password.getBytes());
+            assertThat(decoded.getOldPassword()).isEqualTo(oldPassword.getBytes());
+            assertThat(decoded.getUserIdentity()).isEqualTo(ByteString.valueOfUtf8(userIdentity));
+            assertThat(decoded.getControls().contains(control)).isTrue();
+        } catch (DecodeException e) {
+            throw e;
+        }
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests")
+    public void testUnmodifiableRequest(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        original.setUserIdentity("uid=scarter,ou=people,dc=example,dc=com");
+        original.setOldPassword("old".getBytes());
+        assertThat(unmodifiable.getOID()).isEqualTo(original.getOID());
+        assertThat(unmodifiable.getUserIdentity()).isEqualTo(original.getUserIdentity());
+        assertThat(unmodifiable.getUserIdentityAsString()).isEqualTo(original.getUserIdentityAsString());
+        original.setNewPassword("carter".getBytes());
+        assertThat(unmodifiable.getNewPassword()).isEqualTo(original.getNewPassword());
+        assertThat(unmodifiable.getOldPassword()).isEqualTo(original.getOldPassword());
+
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewPassword(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setNewPassword("password".toCharArray());
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetNewPassword2(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setNewPassword("password".getBytes());
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetOldPassword(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setOldPassword("password".toCharArray());
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetOldPassword2(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setOldPassword("password".getBytes());
+    }
+
+    @Test(dataProvider = "passwordModifyExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetUserIdentity(final PasswordModifyExtendedRequest original) {
+        final PasswordModifyExtendedRequest unmodifiable = (PasswordModifyExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setUserIdentity("uid=scarter,ou=people,dc=example,dc=com");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestTestCase.java
new file mode 100644
index 0000000..cd00e1c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/PlainSASLBindRequestTestCase.java
@@ -0,0 +1,110 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static com.forgerock.opendj.util.StaticUtils.EMPTY_BYTES;
+import static com.forgerock.opendj.util.StaticUtils.getBytes;
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests Plain SASL Bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class PlainSASLBindRequestTestCase extends RequestsTestCase {
+
+    private static final PlainSASLBindRequest NEW_PLAIN_SASL_BIND_REQUEST = Requests.newPlainSASLBindRequest("id1",
+            EMPTY_BYTES);
+    private static final PlainSASLBindRequest NEW_PLAIN_SASL_BIND_REQUEST2 = Requests.newPlainSASLBindRequest("id2",
+            getBytes("password"));
+
+    @DataProvider(name = "plainSASLBindRequests")
+    private Object[][] getPlainSASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected PlainSASLBindRequest[] newInstance() {
+        return new PlainSASLBindRequest[] {
+            NEW_PLAIN_SASL_BIND_REQUEST,
+            NEW_PLAIN_SASL_BIND_REQUEST2
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfPlainSASLBindRequest((PlainSASLBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiablePlainSASLBindRequest((PlainSASLBindRequest) original);
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests")
+    public void testModifiableRequest(final PlainSASLBindRequest original) {
+        final String authID = "u:user.0";
+        final String azID = "dn:user.0,dc=com";
+        final String password = "pass";
+
+        final PlainSASLBindRequest copy = (PlainSASLBindRequest) copyOf(original);
+        copy.setAuthenticationID(authID);
+        copy.setAuthorizationID(azID);
+        copy.setPassword(password.toCharArray());
+
+        assertThat(copy.getAuthenticationID()).isEqualTo(authID);
+        assertThat(copy.getAuthorizationID()).isEqualTo(azID);
+        assertThat(original.getAuthenticationID()).isNotEqualTo(copy.getAuthenticationID());
+        assertThat(original.getAuthorizationID()).isNotEqualTo(copy.getAuthorizationID());
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests")
+    public void testUnmodifiableRequest(final PlainSASLBindRequest original) {
+        final PlainSASLBindRequest unmodifiable = (PlainSASLBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName()).isEqualTo(original.getName());
+        assertThat(unmodifiable.getAuthorizationID()).isEqualTo(original.getAuthorizationID());
+        assertThat(unmodifiable.getAuthenticationID()).isEqualTo(original.getAuthenticationID());
+        assertThat(unmodifiable.getSASLMechanism()).isEqualTo(original.getSASLMechanism());
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationID(final PlainSASLBindRequest original) {
+        final PlainSASLBindRequest unmodifiable = (PlainSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthenticationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthorizationID(final PlainSASLBindRequest original) {
+        final PlainSASLBindRequest unmodifiable = (PlainSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setAuthorizationID("dn: uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword(final PlainSASLBindRequest original) {
+        final PlainSASLBindRequest unmodifiable = (PlainSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".toCharArray());
+    }
+
+    @Test(dataProvider = "plainSASLBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword2(final PlainSASLBindRequest original) {
+        final PlainSASLBindRequest unmodifiable = (PlainSASLBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".getBytes());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/RequestsTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/RequestsTestCase.java
new file mode 100644
index 0000000..03a7891
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/RequestsTestCase.java
@@ -0,0 +1,214 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ControlDecoder;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all requests unit tests should extend. Requests represents the classes found directly under
+ * the package org.forgerock.opendj.ldap.requests.
+ */
+@Test(groups = { "precommit", "requests", "sdk" })
+public abstract class RequestsTestCase extends ForgeRockTestCase {
+
+    private static final GenericControl NEW_CONTROL = GenericControl.newControl("1.2.3".intern());
+    private static final GenericControl NEW_CONTROL2 = GenericControl.newControl("3.4.5".intern());
+    private static final GenericControl NEW_CONTROL3 = GenericControl.newControl("6.7.8".intern());
+    private static final GenericControl NEW_CONTROL4 = GenericControl.newControl("8.9.0".intern());
+
+    /** Dummy decoder which does nothing. */
+    private static class MyDecoder implements ControlDecoder<Control> {
+        @Override
+        public Control decodeControl(final Control control, final DecodeOptions options) throws DecodeException {
+            // do nothing.
+            return control;
+        }
+
+        @Override
+        public String getOID() {
+            return "1.2.3".intern();
+        }
+    }
+
+    /**
+     * Ensures that the LDAP Server is running.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @BeforeClass
+    public void startServer() throws Exception {
+        TestCaseUtils.startServer();
+    }
+
+    /**
+     * Creates the requests.
+     *
+     * @return An array of requests.
+     */
+    protected abstract Request[] newInstance();
+
+    /**
+     * Creates a copy of the request.
+     *
+     * @param original
+     *            The original request to copy.
+     * @return A copy of the request.
+     */
+    protected abstract Request copyOf(final Request original);
+
+    /**
+     * Creates an unmodifiable request.
+     *
+     * @param original
+     *            The original request.
+     * @return A unmodifiable request from the original.
+     */
+    protected abstract Request unmodifiableOf(final Request original);
+
+    @DataProvider(name = "createModifiableInstance")
+    final Object[][] createModifiableInstance() throws Exception {
+        final Request[] requestArray = newInstance();
+        final Object[][] objectArray = new Object[requestArray.length][1];
+
+        for (int i = 0; i < requestArray.length; i++) {
+            objectArray[i][0] = requestArray[i];
+        }
+        return objectArray;
+    }
+
+    @DataProvider(name = "createCopyOfInstance")
+    final Object[][] createCopyOfInstance() throws Exception {
+        final Request[] requestArray = newInstance();
+        final Object[][] objectArray = new Object[requestArray.length][2];
+
+        for (int i = 0; i < requestArray.length; i++) {
+            objectArray[i][0] = requestArray[i];
+            objectArray[i][1] = copyOf(requestArray[i]);
+        }
+        return objectArray;
+    }
+
+    @DataProvider(name = "createUnmodifiableInstance")
+    final Object[][] createUnmodifiableInstance() throws Exception {
+        final Request[] requestArray = newInstance();
+        final Object[][] objectArray = new Object[requestArray.length][2];
+
+        for (int i = 0; i < requestArray.length; i++) {
+            objectArray[i][0] = requestArray[i];
+            objectArray[i][1] = unmodifiableOf(requestArray[i]);
+        }
+        return objectArray;
+    }
+
+
+    /**
+     * Adds a control to a request and make sure it is present.
+     *
+     * @param request
+     *            The request to test.
+     * @throws DecodeException
+     *             If the control cannot be decode.
+     */
+    @Test(dataProvider = "createModifiableInstance")
+    public void testAddControl(final Request request) throws DecodeException {
+        assertThat(request.containsControl(NEW_CONTROL.getOID())).isFalse();
+        request.addControl(NEW_CONTROL);
+        assertThat(request.containsControl(NEW_CONTROL.getOID())).isTrue();
+        assertTrue(request.getControls().size() > 0);
+        final MyDecoder decoder = new MyDecoder();
+        Control control = request.getControl(decoder, new DecodeOptions());
+        assertNotNull(control);
+    }
+
+    /**
+     * Adds a control to the original request and make sure the unmodifiable request is affected. Adding a control to
+     * the unmodifiable throws an exception.
+     *
+     * @param original
+     *            The original request.
+     * @param unmodifiable
+     *            The unmodifiable 'view' request.
+     */
+    @Test(dataProvider = "createUnmodifiableInstance", expectedExceptions = UnsupportedOperationException.class)
+    public void testAddControlUnmodifiable(final Request original, final Request unmodifiable) {
+
+        assertThat(unmodifiable.containsControl(NEW_CONTROL2.getOID())).isFalse();
+        assertThat(original.containsControl(NEW_CONTROL2.getOID())).isFalse();
+        original.addControl(NEW_CONTROL2);
+
+        // Unmodifiable is a view of the original request.
+        assertThat(original.containsControl(NEW_CONTROL2.getOID())).isTrue();
+        assertThat(unmodifiable.containsControl(NEW_CONTROL2.getOID())).isTrue();
+
+        unmodifiable.addControl(NEW_CONTROL3);
+    }
+
+    /**
+     * Tests adding a control to the original request. The Copy should not be affected by the modification.
+     *
+     * @param original
+     *            The original request.
+     * @param copy
+     *            Copy of the original request.
+     */
+    @Test(dataProvider = "createCopyOfInstance")
+    public void testAddControlToCopy(final Request original, final Request copy) {
+        assertThat(original.containsControl(NEW_CONTROL3.getOID())).isFalse();
+        assertThat(copy.containsControl(NEW_CONTROL3.getOID())).isFalse();
+
+        original.addControl(NEW_CONTROL3);
+        assertThat(original.containsControl(NEW_CONTROL3.getOID())).isTrue();
+        assertTrue(original.getControls().size() > 0);
+        assertThat(copy.containsControl(NEW_CONTROL3.getOID())).isFalse();
+
+        copy.addControl(NEW_CONTROL4);
+        assertThat(original.containsControl(NEW_CONTROL4.getOID())).isFalse();
+        assertThat(copy.containsControl(NEW_CONTROL4.getOID())).isTrue();
+    }
+
+    /**
+     * The toString function from the copy should always starts with the class name.
+     * <p>
+     * eg. AbandonRequest(requestID=-1...)
+     *
+     * @param original
+     *            The original request.
+     * @param copy
+     *            The copy request.
+     */
+    @Test(dataProvider = "createCopyOfInstance")
+    public void testCopyToStringShouldContainClassName(final Request original, final Request copy) {
+        final String className = copy.getClass().getSimpleName().replace("Impl", "");
+        assertThat(copy.toString().startsWith(className)).isTrue();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java
new file mode 100644
index 0000000..73d8518
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java
@@ -0,0 +1,176 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.*;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class SearchRequestTestCase extends RequestsTestCase {
+
+    private static final SearchRequest NEW_SEARCH_REQUEST = Requests.newSearchRequest("uid=user.0,ou=people,o=test",
+            SearchScope.BASE_OBJECT, "(uid=user)", "uid", "ou");
+
+    private static final SearchRequest NEW_SEARCH_REQUEST2 = Requests.newSearchRequest("uid=user.0,ou=people,o=test",
+            SearchScope.SINGLE_LEVEL, "(uid=user)", "uid", "ou");
+
+    @DataProvider(name = "SearchRequests")
+    private Object[][] getSearchRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected SearchRequest[] newInstance() {
+        return new SearchRequest[] {
+            NEW_SEARCH_REQUEST,
+            NEW_SEARCH_REQUEST2 };
+    }
+
+    @Test
+    public void createRequestForSingleEntrySearch() throws Exception {
+        SearchRequest request = Requests.newSingleEntrySearchRequest(DN.valueOf("uid=user.0,ou=people,o=test"),
+                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
+
+        assertThat(request.getSizeLimit()).isEqualTo(1);
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+
+    @Test
+    public void createRequestForSingleEntrySearchWithStrings() throws Exception {
+        SearchRequest request = Requests.newSingleEntrySearchRequest("uid=user.0,ou=people,o=test",
+                SearchScope.BASE_OBJECT, "(uid=user)", "uid");
+
+        assertThat(request.getSizeLimit()).isEqualTo(1);
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+
+    @Test
+    public void createRequestWithBaseObjectScope() throws Exception {
+        SearchRequest request = Requests.newSearchRequest(DN.valueOf("uid=user.0,ou=people,o=test"),
+                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
+
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfSearchRequest((SearchRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableSearchRequest((SearchRequest) original);
+    }
+
+    @Test(dataProvider = "SearchRequests")
+    public void testModifiableRequest(final SearchRequest original) {
+        final String name = "uid=bjensen";
+        final Filter filter = Filter.format("(&(uid=%s)(age>=%s))", "bjensen", 21);
+        final SearchScope scope = SearchScope.BASE_OBJECT;
+        final DereferenceAliasesPolicy policy = DereferenceAliasesPolicy.ALWAYS;
+        final int timeLimit = 10;
+        final int sizeLimit = 15;
+
+        final SearchRequest copy = (SearchRequest) copyOf(original);
+        copy.setName(name);
+        copy.setFilter(filter);
+        copy.setScope(scope);
+        copy.setDereferenceAliasesPolicy(policy);
+        copy.setSizeLimit(sizeLimit);
+        copy.setTimeLimit(timeLimit);
+        copy.setTypesOnly(true);
+
+        assertThat(copy.getName().toString()).isEqualTo(name);
+        assertThat(copy.getFilter()).isEqualTo(filter);
+        assertThat(copy.getScope()).isEqualTo(scope);
+        assertThat(copy.getDereferenceAliasesPolicy()).isEqualTo(policy);
+        assertThat(copy.getTimeLimit()).isEqualTo(timeLimit);
+        assertThat(copy.getSizeLimit()).isEqualTo(sizeLimit);
+        assertThat(copy.isTypesOnly()).isTrue();
+    }
+
+    @Test(dataProvider = "SearchRequests")
+    public void testUnmodifiableRequest(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getName().toString()).isEqualTo(original.getName().toString());
+        assertThat(unmodifiable.getFilter()).isEqualTo(original.getFilter());
+        assertThat(unmodifiable.getScope()).isEqualTo(original.getScope());
+        assertThat(unmodifiable.getDereferenceAliasesPolicy()).isEqualTo(original.getDereferenceAliasesPolicy());
+        assertThat(unmodifiable.getTimeLimit()).isEqualTo(original.getTimeLimit());
+        assertThat(unmodifiable.getSizeLimit()).isEqualTo(original.getSizeLimit());
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetDereferenceAliasesPolicy(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setDereferenceAliasesPolicy(DereferenceAliasesPolicy.ALWAYS);
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetFilter(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setFilter(Filter.format("(&(cn=%s)(age>=%s))", "bjensen", 21));
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetFilter2(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setFilter("(&(cn=bjensen)(age>=21))");
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setName(DN.valueOf("uid=scarter,ou=people,dc=example,dc=com"));
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetScope(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setScope(SearchScope.BASE_OBJECT);
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetSizeLimit(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setSizeLimit(10);
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetTimeLimit(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setTimeLimit(200);
+    }
+
+    @Test(dataProvider = "SearchRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetTypesOnly(final SearchRequest original) {
+        final SearchRequest unmodifiable = (SearchRequest) unmodifiableOf(original);
+        unmodifiable.setTypesOnly(false);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestTestCase.java
new file mode 100644
index 0000000..6c4e58f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/SimpleBindRequestTestCase.java
@@ -0,0 +1,98 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests Simple Bind requests.
+ */
+@SuppressWarnings("javadoc")
+public class SimpleBindRequestTestCase extends BindRequestTestCase {
+    private static final SimpleBindRequest NEW_SIMPLE_BIND_REQUEST = Requests.newSimpleBindRequest();
+    private static final SimpleBindRequest NEW_SIMPLE_BIND_REQUEST2 = Requests.newSimpleBindRequest("username",
+            "password".toCharArray());
+
+    @DataProvider(name = "simpleBindRequests")
+    private Object[][] getSimpleBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected SimpleBindRequest[] newInstance() {
+        return new SimpleBindRequest[] {
+            NEW_SIMPLE_BIND_REQUEST, // anonymous;
+            NEW_SIMPLE_BIND_REQUEST2 };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfSimpleBindRequest((SimpleBindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableSimpleBindRequest((SimpleBindRequest) original);
+    }
+
+    @Test(dataProvider = "simpleBindRequests")
+    public void testModifiableRequest(final SimpleBindRequest original) {
+        final String name = "user.0";
+        final String password = "pass99";
+
+        final SimpleBindRequest copy = (SimpleBindRequest) copyOf(original);
+
+        copy.setName(name);
+        copy.setPassword(password.getBytes());
+
+        assertThat(copy.getName()).isEqualTo(name);
+        assertThat(copy.getPassword()).isEqualTo(password.getBytes());
+        assertThat(copy.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(original.getName()).isNotEqualTo(copy.getName());
+        assertThat(original.getPassword()).isNotEqualTo(copy.getPassword());
+    }
+
+    @Test(dataProvider = "simpleBindRequests")
+    public void testUnmodifiableRequest(final SimpleBindRequest original) {
+        final SimpleBindRequest unmodifiable = (SimpleBindRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getAuthenticationType()).isEqualTo(original.getAuthenticationType());
+        assertThat(unmodifiable.getName()).isEqualTo(original.getName());
+        assertThat(unmodifiable.getPassword()).isEqualTo(original.getPassword());
+    }
+
+    @Test(dataProvider = "simpleBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetName2(final SimpleBindRequest original) {
+        final SimpleBindRequest unmodifiable = (SimpleBindRequest) unmodifiableOf(original);
+        unmodifiable.setName("uid=scarter,ou=people,dc=example,dc=com");
+    }
+
+    @Test(dataProvider = "simpleBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword(final SimpleBindRequest original) {
+        final SimpleBindRequest unmodifiable = (SimpleBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".toCharArray());
+    }
+
+    @Test(dataProvider = "simpleBindRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetPassword2(final SimpleBindRequest original) {
+        final SimpleBindRequest unmodifiable = (SimpleBindRequest) unmodifiableOf(original);
+        unmodifiable.setPassword("password".getBytes());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestTestCase.java
new file mode 100644
index 0000000..c958c33
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestTestCase.java
@@ -0,0 +1,125 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests Start TLS Extended Requests.
+ */
+@SuppressWarnings("javadoc")
+public class StartTLSExtendedRequestTestCase extends RequestsTestCase {
+
+    @DataProvider(name = "StartTLSExtendedRequests")
+    private Object[][] getPlainSASLBindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected StartTLSExtendedRequest[] newInstance() {
+        try {
+            return new StartTLSExtendedRequest[] { Requests.newStartTLSExtendedRequest(SSLContext.getDefault()) };
+        } catch (NoSuchAlgorithmException e) {
+            // nothing to do
+        }
+        return null;
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfStartTLSExtendedRequest((StartTLSExtendedRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableStartTLSExtendedRequest((StartTLSExtendedRequest) original);
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests")
+    public void testModifiableRequest(final StartTLSExtendedRequest original) throws NoSuchAlgorithmException {
+
+        final StartTLSExtendedRequest copy = (StartTLSExtendedRequest) copyOf(original);
+        copy.setSSLContext(SSLContext.getInstance("TLS"));
+
+        assertThat(copy.getSSLContext().getProtocol()).isEqualTo("TLS");
+        assertThat(original.getSSLContext().getProtocol()).isEqualTo("Default");
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests")
+    public void testUnmodifiableRequest(final StartTLSExtendedRequest original) {
+        final StartTLSExtendedRequest unmodifiable = (StartTLSExtendedRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getSSLContext()).isEqualTo(original.getSSLContext());
+        assertThat(original.getSSLContext().getProtocol()).isEqualTo("Default");
+        assertThat(unmodifiable.getOID()).isEqualTo(original.getOID());
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddEnabledCipherSuite(final StartTLSExtendedRequest original)
+            throws NoSuchAlgorithmException {
+        final StartTLSExtendedRequest unmodifiable = (StartTLSExtendedRequest) unmodifiableOf(original);
+        unmodifiable.addEnabledCipherSuite("suite");
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableAddEnabledProtocol(final StartTLSExtendedRequest original)
+            throws NoSuchAlgorithmException {
+        final StartTLSExtendedRequest unmodifiable = (StartTLSExtendedRequest) unmodifiableOf(original);
+        unmodifiable.addEnabledProtocol("SSL", "TLS");
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests", expectedExceptions = UnsupportedOperationException.class)
+    public void testUnmodifiableSetAuthenticationID(final StartTLSExtendedRequest original)
+            throws NoSuchAlgorithmException {
+        final StartTLSExtendedRequest unmodifiable = (StartTLSExtendedRequest) unmodifiableOf(original);
+        unmodifiable.setSSLContext(SSLContext.getInstance("SSL"));
+    }
+
+    @Test(dataProvider = "StartTLSExtendedRequests")
+    public void testModifiableRequestDecode(final StartTLSExtendedRequest original) throws DecodeException,
+            NoSuchAlgorithmException {
+        final GenericControl control = GenericControl.newControl("1.2.3".intern());
+
+        final StartTLSExtendedRequest copy = (StartTLSExtendedRequest) copyOf(original);
+        copy.addControl(control);
+        copy.addEnabledCipherSuite("TLSv1");
+        copy.addEnabledProtocol("TLS");
+        copy.setSSLContext(SSLContext.getInstance("TLS"));
+        assertThat(original.getControls().contains(control)).isFalse();
+        assertThat(original.getEnabledCipherSuites().contains("TLSv1")).isFalse();
+        assertThat(original.getSSLContext().getProtocol()).isNotEqualTo("TLS");
+        assertThat(copy.getEnabledCipherSuites().contains("TLSv1")).isTrue();
+        assertThat(copy.getSSLContext().getProtocol()).isEqualTo("TLS");
+
+        try {
+            final StartTLSExtendedRequest decoded = StartTLSExtendedRequest.DECODER.decodeExtendedRequest(copy,
+                    new DecodeOptions());
+            assertThat(decoded.getControls().contains(control)).isTrue();
+        } catch (DecodeException e) {
+            throw e;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/UnbindRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/UnbindRequestTestCase.java
new file mode 100644
index 0000000..b6a88a8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/UnbindRequestTestCase.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the unbind requests.
+ */
+@SuppressWarnings("javadoc")
+public class UnbindRequestTestCase extends RequestsTestCase {
+    private static final UnbindRequest NEW_UNBIND_REQUEST = Requests.newUnbindRequest();
+
+    @DataProvider(name = "UnbindRequests")
+    private Object[][] getUnbindRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected UnbindRequest[] newInstance() {
+        return new UnbindRequest[] { NEW_UNBIND_REQUEST };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfUnbindRequest((UnbindRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableUnbindRequest((UnbindRequest) original);
+    }
+
+    @Test
+    public void testModifiableRequest() {
+        final UnbindRequest copy = (UnbindRequest) copyOf(Requests.newUnbindRequest());
+        assertThat(copy.toString()).isEqualTo("UnbindRequest(controls=[])");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestTestCase.java
new file mode 100644
index 0000000..04bd5d6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/requests/WhoAmIExtendedRequestTestCase.java
@@ -0,0 +1,96 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests WHOAMIEXTENDED requests.
+ */
+@SuppressWarnings("javadoc")
+public class WhoAmIExtendedRequestTestCase extends RequestsTestCase {
+    private static final WhoAmIExtendedRequest NEW_WHOAMIEXTENDED_REQUEST = Requests
+            .newWhoAmIExtendedRequest();
+
+    @DataProvider(name = "whoAmIExtendedRequests")
+    private Object[][] getWhoAmIExtendedRequests() throws Exception {
+        return createModifiableInstance();
+    }
+
+    @Override
+    protected WhoAmIExtendedRequest[] newInstance() {
+        return new WhoAmIExtendedRequest[] {
+            NEW_WHOAMIEXTENDED_REQUEST
+        };
+    }
+
+    @Override
+    protected Request copyOf(Request original) {
+        return Requests.copyOfWhoAmIExtendedRequest((WhoAmIExtendedRequest) original);
+    }
+
+    @Override
+    protected Request unmodifiableOf(Request original) {
+        return Requests.unmodifiableWhoAmIExtendedRequest((WhoAmIExtendedRequest) original);
+    }
+
+    @Test(dataProvider = "whoAmIExtendedRequests")
+    public void testModifiableRequest(final WhoAmIExtendedRequest original) {
+        final WhoAmIExtendedRequest copy = (WhoAmIExtendedRequest) copyOf(original);
+        assertThat(copy.getOID()).isEqualTo(original.getOID());
+        assertThat(copy.getResultDecoder()).isEqualTo(original.getResultDecoder());
+        assertThat(copy.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test(dataProvider = "whoAmIExtendedRequests")
+    public void testUnmodifiableRequest(final WhoAmIExtendedRequest original) {
+        final WhoAmIExtendedRequest unmodifiable = (WhoAmIExtendedRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.getOID()).isEqualTo(original.getOID());
+        assertThat(unmodifiable.getResultDecoder()).isEqualTo(original.getResultDecoder());
+        assertThat(unmodifiable.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test(dataProvider = "whoAmIExtendedRequests")
+    public void testUnmodifiableRequestHasResult(final WhoAmIExtendedRequest original) {
+        final WhoAmIExtendedRequest unmodifiable = (WhoAmIExtendedRequest) unmodifiableOf(original);
+        assertThat(unmodifiable.hasValue()).isFalse();
+    }
+
+
+    @Test(dataProvider = "whoAmIExtendedRequests")
+    public void testModifiableRequestDecode(final WhoAmIExtendedRequest original) throws DecodeException {
+        final GenericControl control = GenericControl.newControl("1.2.3".intern());
+
+        final WhoAmIExtendedRequest copy = (WhoAmIExtendedRequest) copyOf(original);
+        copy.addControl(control);
+        assertThat(original.getControls().contains(control)).isFalse();
+
+        try {
+            final WhoAmIExtendedRequest decoded = WhoAmIExtendedRequest.DECODER.decodeExtendedRequest(copy,
+                    new DecodeOptions());
+            assertThat(decoded.getControls().contains(control)).isTrue();
+        } catch (DecodeException e) {
+            throw e;
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/responses/ResponsesTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/responses/ResponsesTestCase.java
new file mode 100644
index 0000000..8dae00c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/responses/ResponsesTestCase.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.responses;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all responses unit tests should extend. Responses
+ * represents the classes found directly under the package
+ * org.forgerock.opendj.ldap.responses.
+ */
+
+@Test(groups = { "precommit", "responses", "sdk" })
+public abstract class ResponsesTestCase extends ForgeRockTestCase {
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaElementTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaElementTestCase.java
new file mode 100644
index 0000000..f32f9c7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaElementTestCase.java
@@ -0,0 +1,140 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Abstract schema element tests.
+ */
+@SuppressWarnings("javadoc")
+public abstract class AbstractSchemaElementTestCase extends AbstractSchemaTestCase {
+    protected static final Map<String, List<String>> EMPTY_PROPS = Collections.emptyMap();
+    protected static final List<String> EMPTY_NAMES = Collections.emptyList();
+
+    @DataProvider(name = "equalsTestData")
+    public abstract Object[][] createEqualsTestData() throws SchemaException, DecodeException;
+
+    /**
+     * Check that the equals operator works as expected.
+     *
+     * @param e1
+     *            The first element
+     * @param e2
+     *            The second element
+     * @param result
+     *            The expected result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "equalsTestData")
+    public final void testEquals(final SchemaElement e1, final SchemaElement e2,
+            final boolean result) throws Exception {
+        Assert.assertEquals(e1.equals(e2), result);
+        Assert.assertEquals(e2.equals(e1), result);
+    }
+
+    /**
+     * Check that the {@link SchemaElement#getDescription()} method returns a
+     * description.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetDescription() throws Exception {
+        final SchemaElement e = getElement("hello", EMPTY_PROPS);
+        Assert.assertEquals(e.getDescription(), "hello");
+    }
+
+    /**
+     * Check that the {@link SchemaElement#getDescription()} method returns
+     * <code>null</code> when there is no description.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetDescriptionDefault() throws Exception {
+        final SchemaElement e = getElement("", EMPTY_PROPS);
+        Assert.assertEquals(e.getDescription(), "");
+    }
+
+    /**
+     * Check that the {@link SchemaElement#getExtraProperties()} method
+     * returns values.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetExtraProperty() throws Exception {
+        final List<String> values = new ArrayList<>();
+        values.add("one");
+        values.add("two");
+        final Map<String, List<String>> props = Collections.singletonMap("test", values);
+        final SchemaElement e = getElement("", props);
+
+        int i = 0;
+        for (final String value : e.getExtraProperties().get("test")) {
+            Assert.assertEquals(value, values.get(i));
+            i++;
+        }
+    }
+
+    /**
+     * Check that the {@link SchemaElement#getExtraProperties()} method
+     * returns <code>null</code> when there is no property.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetExtraPropertyDefault() throws Exception {
+        final SchemaElement e = getElement("", EMPTY_PROPS);
+        Assert.assertNull(e.getExtraProperties().get("test"));
+    }
+
+    /**
+     * Check that the hasCode method operator works as expected.
+     *
+     * @param e1
+     *            The first element
+     * @param e2
+     *            The second element
+     * @param result
+     *            The expected result.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "equalsTestData")
+    public final void testHashCode(final SchemaElement e1, final SchemaElement e2,
+            final boolean result) throws Exception {
+        Assert.assertEquals(e1.hashCode() == e2.hashCode(), result);
+    }
+
+    protected abstract SchemaElement getElement(String description,
+            Map<String, List<String>> extraProperties) throws SchemaException;
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaTestCase.java
new file mode 100644
index 0000000..98ba71d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSchemaTestCase.java
@@ -0,0 +1,27 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all schema unit test should extend.
+ */
+@Test(groups = { "precommit", "schema", "sdk" })
+public abstract class AbstractSchemaTestCase extends ForgeRockTestCase {
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImplTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImplTest.java
new file mode 100644
index 0000000..faab9af
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSubstringMatchingRuleImplTest.java
@@ -0,0 +1,281 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.TreeSet;
+
+import org.fest.assertions.Assertions;
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
+import org.forgerock.opendj.ldap.spi.Indexer;
+import org.forgerock.opendj.ldap.spi.IndexingOptions;
+import org.forgerock.util.Utils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.ByteString.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+/** Tests all generic code of AbstractSubstringMatchingRuleImpl. */
+@SuppressWarnings("javadoc")
+public class AbstractSubstringMatchingRuleImplTest extends AbstractSchemaTestCase {
+    private int subStringLength = 3;
+
+    private static class FakeSubstringMatchingRuleImpl extends AbstractSubstringMatchingRuleImpl {
+        FakeSubstringMatchingRuleImpl() {
+            super(SMR_CASE_EXACT_OID, EMR_CASE_EXACT_OID);
+        }
+
+        @Override
+        public ByteString normalizeAttributeValue(Schema schema, ByteSequence value) throws DecodeException {
+            return value.toByteString();
+        }
+    }
+
+    static class FakeIndexQueryFactory implements IndexQueryFactory<String> {
+        private final IndexingOptions options;
+        private final boolean normalizedValuesAreReadable;
+
+        public FakeIndexQueryFactory(IndexingOptions options) {
+            this(options, true);
+        }
+
+        public FakeIndexQueryFactory(IndexingOptions options, boolean normalizedValuesAreReadable) {
+            this.options = options;
+            this.normalizedValuesAreReadable = normalizedValuesAreReadable;
+        }
+
+        @Override
+        public String createExactMatchQuery(String indexID, ByteSequence key) {
+            String keyValue = normalizedValuesAreReadable ? key.toString() : key.toByteString().toHexString();
+            return "exactMatch(" + indexID + ", value=='" + keyValue + "')";
+        }
+
+        @Override
+        public String createMatchAllQuery() {
+            return "matchAll()";
+        }
+
+        @Override
+        public String createRangeMatchQuery(String indexID, ByteSequence lower, ByteSequence upper,
+                boolean lowerIncluded, boolean upperIncluded) {
+            final StringBuilder sb = new StringBuilder("rangeMatch");
+            sb.append("(");
+            sb.append(indexID);
+            sb.append(", '");
+            if (normalizedValuesAreReadable) {
+                sb.append(lower);
+            } else if (!lower.isEmpty()) {
+                sb.append(lower.toByteString().toHexString());
+            }
+            sb.append("' <");
+            if (lowerIncluded) {
+                sb.append("=");
+            }
+            sb.append(" value <");
+            if (upperIncluded) {
+                sb.append("=");
+            }
+            sb.append(" '");
+            if (normalizedValuesAreReadable) {
+                sb.append(upper);
+            } else if (!upper.isEmpty()) {
+                sb.append(upper.toByteString().toHexString());
+            }
+            sb.append("')");
+            return sb.toString();
+        }
+
+        @Override
+        public String createIntersectionQuery(Collection<String> subqueries) {
+            return "intersect[" + Utils.joinAsString(", ", subqueries) + "]";
+        }
+
+        @Override
+        public String createUnionQuery(Collection<String> subqueries) {
+            return "union[" + Utils.joinAsString(", ", subqueries) + "]";
+        }
+
+        @Override
+        public IndexingOptions getIndexingOptions() {
+            return options;
+        }
+    }
+
+    private MatchingRuleImpl getRule() {
+        return new FakeSubstringMatchingRuleImpl();
+    }
+
+    static IndexingOptions newIndexingOptions() {
+        return newIndexingOptions(3);
+    }
+
+    static IndexingOptions newIndexingOptions(int subStringLength) {
+        final IndexingOptions options = mock(IndexingOptions.class);
+        when(options.substringKeySize()).thenReturn(subStringLength);
+        return options;
+    }
+
+    @DataProvider
+    public Object[][] invalidAssertions() {
+        return new Object[][] {
+            { "" },
+            { "abc" },
+            { "**" },
+            { "\\g" },
+            { "\\0" },
+            { "\\00" },
+            { "\\0g" },
+            { gen() },
+        };
+    }
+
+    @Test(dataProvider = "invalidAssertions", expectedExceptions = DecodeException.class)
+    public void testInvalidAssertion(String assertionValue) throws Exception {
+        getRule().getAssertion(null, valueOfUtf8(assertionValue));
+    }
+
+    @DataProvider
+    public Object[][] validAssertions() {
+        return new Object[][] {
+            { "this is a string", "*", ConditionResult.TRUE },
+            { "this is a string", "that*", ConditionResult.FALSE },
+            { "this is a string", "*that", ConditionResult.FALSE },
+            { "this is a string", "this*is*a*string", ConditionResult.TRUE },
+            { "this is a string", "this*my*string", ConditionResult.FALSE },
+            { "this is a string", "string*a*is*this", ConditionResult.FALSE },
+            { "this is a string", "string*a*is*this", ConditionResult.FALSE },
+            { "this is a string", "*\\00", ConditionResult.FALSE },
+            { "this is a string", gen() + "*", ConditionResult.FALSE },
+            // initial substring longer than value
+            { "tt", "this*", ConditionResult.FALSE },
+            // final substring longer than value
+            { "tt", "*this", ConditionResult.FALSE },
+        };
+    }
+
+    private String gen() {
+        final char[] array = new char[] {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+        final StringBuilder sb = new StringBuilder();
+        for (char c : array) {
+            sb.append("\\").append(c).append(c);
+        }
+        return sb.toString();
+    }
+
+    private String subStringIndexID(String matchingRule) {
+        return matchingRule + ":" + subStringLength;
+    }
+
+    @Test(dataProvider = "validAssertions")
+    public void testValidAssertions(String attrValue, String assertionValue, ConditionResult expected)
+            throws Exception {
+        final MatchingRuleImpl rule = getRule();
+        final ByteString normValue = rule.normalizeAttributeValue(null, valueOfUtf8(attrValue));
+        Assertion assertion = rule.getAssertion(null, valueOfUtf8(assertionValue));
+        assertEquals(assertion.matches(normValue), expected);
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testSubstringCreateIndexQueryForFinalWithMultipleSubqueries() throws Exception {
+        Assertion assertion = getRule().getSubstringAssertion(
+            null, null, Collections.EMPTY_LIST, valueOfUtf8("this"));
+
+        assertEquals(
+            assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(subStringLength))),
+            "intersect["
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='his'), "
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='thi')"
+                    + "]");
+    }
+
+    @Test
+    public void testSubstringCreateIndexQueryForAllNoSubqueries() throws Exception {
+        Assertion assertion = getRule().getSubstringAssertion(
+            null, valueOfUtf8("abc"), Arrays.asList(toByteStrings("def", "ghi")), valueOfUtf8("jkl"));
+
+        assertEquals(
+            assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(subStringLength))),
+            "intersect["
+                    + "rangeMatch(" + EMR_CASE_EXACT_OID + ", 'abc' <= value < 'abd'), "
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='def'), "
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='ghi'), "
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='jkl'), "
+                    + "exactMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", value=='abc')"
+                    + "]");
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testSubstringCreateIndexQueryWithInitial() throws Exception {
+        Assertion assertion = getRule().getSubstringAssertion(
+            null, valueOfUtf8("aa"), Collections.EMPTY_LIST, null);
+
+        assertEquals(
+            assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(subStringLength))),
+            "intersect["
+                    + "rangeMatch(" + EMR_CASE_EXACT_OID +  ", 'aa' <= value < 'ab'), "
+                    + "rangeMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", 'aa' <= value < 'ab')"
+                    + "]");
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testSubstringCreateIndexQueryWithInitialOverflowsInRange() throws Exception {
+        ByteString lower = wrap(new byte[] { 'a', (byte) 0XFF });
+        Assertion assertion = getRule().getSubstringAssertion(
+            null, lower, Collections.EMPTY_LIST, null);
+
+        assertEquals(
+            assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(subStringLength))),
+            // 0x00 is the nul byte, a.k.a. string terminator
+            // so everything after it is not part of the string
+            "intersect["
+                    + "rangeMatch(" + EMR_CASE_EXACT_OID + ", '" + lower + "' <= value < 'b\u0000'), "
+                    + "rangeMatch(" + subStringIndexID(SMR_CASE_EXACT_OID) + ", '" + lower + "' <= value < 'b\u0000')"
+                    + "]");
+    }
+
+    @Test
+    public void testIndexer() throws Exception {
+        final IndexingOptions options = newIndexingOptions();
+        final Indexer indexer = getRule().createIndexers(options).iterator().next();
+        Assertions.assertThat(indexer.getIndexID()).isEqualTo(SMR_CASE_EXACT_OID + ":" + options.substringKeySize());
+
+        final TreeSet<ByteString> keys = new TreeSet<>();
+        indexer.createKeys(Schema.getCoreSchema(), valueOfUtf8("ABCDE"), keys);
+        Assertions.assertThat(keys).containsOnly((Object[]) toByteStrings("ABC", "BCD", "CDE", "DE", "E"));
+    }
+
+    private ByteString[] toByteStrings(String... strings) {
+        final ByteString[] results = new ByteString[strings.length];
+        for (int i = 0; i < strings.length; i++) {
+            results[i] = valueOfUtf8(strings[i]);
+        }
+        return results;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxTestCase.java
new file mode 100644
index 0000000..f26c283
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AbstractSyntaxTestCase.java
@@ -0,0 +1,58 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.testng.Assert.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Syntax tests. */
+@SuppressWarnings("javadoc")
+public abstract class AbstractSyntaxTestCase extends AbstractSchemaTestCase {
+    /**
+     * Create data for the testAcceptableValues test. This should be a table of
+     * tables with 2 elements. The first one should be the value to test, the
+     * second the expected result of the test.
+     *
+     * @return a table containing data for the testAcceptableValues Test.
+     */
+    @DataProvider(name = "acceptableValues")
+    public abstract Object[][] createAcceptableValues();
+
+    /** Test the normalization and the approximate comparison. */
+    @Test(dataProvider = "acceptableValues")
+    public void testAcceptableValues(final String value, final Boolean result) throws Exception {
+        // Make sure that the specified class can be instantiated as a task.
+        final Syntax syntax = getRule();
+
+        final LocalizableMessageBuilder reason = new LocalizableMessageBuilder();
+        final Boolean liveResult = syntax.valueIsAcceptable(ByteString.valueOfUtf8(value), reason);
+        assertEquals(liveResult, result,
+            syntax + ".valueIsAcceptable gave bad result for " + value + "reason : " + reason);
+    }
+
+    /**
+     * Get an instance of the attribute syntax that must be tested.
+     *
+     * @return An instance of the attribute syntax that must be tested.
+     */
+    protected abstract Syntax getRule() throws SchemaException, DecodeException;
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ApproximateMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ApproximateMatchingRuleTest.java
new file mode 100644
index 0000000..3b7392f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ApproximateMatchingRuleTest.java
@@ -0,0 +1,140 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.AMR_DOUBLE_METAPHONE_NAME;
+import static org.testng.Assert.assertEquals;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Approximate matching rule tests.
+ */
+@SuppressWarnings("javadoc")
+public class ApproximateMatchingRuleTest extends AbstractSchemaTestCase {
+    MatchingRule metaphone = Schema.getCoreSchema().getMatchingRule(AMR_DOUBLE_METAPHONE_NAME);
+
+    /**
+     * Test the normalization and the approximate comparison.
+     */
+    @Test(dataProvider = "approximatematchingrules")
+    public void approximateMatchingRules(final MatchingRule rule, final String value1,
+            final String value2, final ConditionResult result) throws Exception {
+        // normalize the 2 provided values
+        final ByteString normalizedValue1 =
+                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value1));
+
+        // check that the approximatelyMatch return the expected result.
+        final ConditionResult liveResult =
+                rule.getAssertion(ByteString.valueOfUtf8(value2)).matches(normalizedValue1);
+        assertEquals(result, liveResult);
+    }
+
+    /**
+     * Build the data for the approximateMatchingRules test.
+     */
+    @DataProvider(name = "approximatematchingrules")
+    public Object[][] createapproximateMatchingRuleTest() {
+        // fill this table with tables containing :
+        // - the name of the approximate matching rule to test
+        // - 2 values that must be tested for matching
+        // - a boolean indicating if the values match or not
+        return new Object[][] { { metaphone, "celebre", "selebre", ConditionResult.TRUE },
+            { metaphone, "cygale", "sigale", ConditionResult.TRUE },
+            { metaphone, "cigale", "sigale", ConditionResult.TRUE },
+            { metaphone, "accacia", "akacia", ConditionResult.TRUE },
+            { metaphone, "cigale", "sigale", ConditionResult.TRUE },
+            { metaphone, "bertucci", "bertuchi", ConditionResult.TRUE },
+            { metaphone, "manger", "manjer", ConditionResult.TRUE },
+            { metaphone, "gyei", "kei", ConditionResult.TRUE },
+            { metaphone, "agnostique", "aknostic", ConditionResult.TRUE },
+            { metaphone, "ghang", "kang", ConditionResult.TRUE },
+            { metaphone, "affiche", "afiche", ConditionResult.TRUE },
+            { metaphone, "succeed", "sukid", ConditionResult.TRUE },
+            { metaphone, "McCarthur", "macarthur", ConditionResult.TRUE },
+            { metaphone, "czet", "set", ConditionResult.TRUE },
+            { metaphone, "re\u00C7u", "ressu", ConditionResult.TRUE },
+            { metaphone, "ni\u00D1o", "nino", ConditionResult.TRUE },
+            { metaphone, "bateaux", "bateau", ConditionResult.TRUE },
+            { metaphone, "witz", "wits", ConditionResult.TRUE },
+            { metaphone, "barre", "bare", ConditionResult.TRUE },
+            { metaphone, "write", "rite", ConditionResult.TRUE },
+            { metaphone, "the", "ze", ConditionResult.FALSE },
+            { metaphone, "motion", "mochion", ConditionResult.TRUE },
+            { metaphone, "bois", "boi", ConditionResult.TRUE },
+            { metaphone, "schi", "chi", ConditionResult.TRUE },
+            { metaphone, "escalier", "eskalier", ConditionResult.TRUE },
+            { metaphone, "science", "sience", ConditionResult.TRUE },
+            { metaphone, "school", "skool", ConditionResult.TRUE },
+            { metaphone, "swap", "sap", ConditionResult.TRUE },
+            { metaphone, "szize", "size", ConditionResult.TRUE },
+            { metaphone, "shoek", "choek", ConditionResult.FALSE },
+            { metaphone, "sugar", "chugar", ConditionResult.TRUE },
+            { metaphone, "isle", "ile", ConditionResult.TRUE },
+            { metaphone, "yle", "ysle", ConditionResult.TRUE },
+            { metaphone, "focaccia", "focashia", ConditionResult.TRUE },
+            { metaphone, "machine", "mashine", ConditionResult.TRUE },
+            { metaphone, "michael", "mikael", ConditionResult.TRUE },
+            { metaphone, "abba", "aba", ConditionResult.TRUE },
+            { metaphone, "caesar", "saesar", ConditionResult.TRUE },
+            { metaphone, "femme", "fame", ConditionResult.TRUE },
+            { metaphone, "panne", "pane", ConditionResult.TRUE },
+            { metaphone, "josa", "josa", ConditionResult.TRUE },
+            { metaphone, "jose", "hose", ConditionResult.TRUE },
+            { metaphone, "hello", "hello", ConditionResult.TRUE },
+            { metaphone, "hello", "ello", ConditionResult.FALSE },
+            { metaphone, "bag", "bak", ConditionResult.TRUE },
+            { metaphone, "bagg", "bag", ConditionResult.TRUE },
+            { metaphone, "tagliaro", "takliaro", ConditionResult.TRUE },
+            { metaphone, "biaggi", "biaji", ConditionResult.TRUE },
+            { metaphone, "bioggi", "bioji", ConditionResult.TRUE },
+            { metaphone, "rough", "rouf", ConditionResult.TRUE },
+            { metaphone, "ghislane", "jislane", ConditionResult.TRUE },
+            { metaphone, "ghaslane", "kaslane", ConditionResult.TRUE },
+            { metaphone, "odd", "ot", ConditionResult.TRUE },
+            { metaphone, "edgar", "etkar", ConditionResult.TRUE },
+            { metaphone, "edge", "eje", ConditionResult.TRUE },
+            { metaphone, "accord", "akord", ConditionResult.TRUE },
+            { metaphone, "noize", "noise", ConditionResult.TRUE },
+            { metaphone, "orchid", "orkid", ConditionResult.TRUE },
+            { metaphone, "chemistry", "kemistry", ConditionResult.TRUE },
+            { metaphone, "chianti", "kianti", ConditionResult.TRUE },
+            { metaphone, "bacher", "baker", ConditionResult.TRUE },
+            { metaphone, "achtung", "aktung", ConditionResult.TRUE },
+            { metaphone, "Writing", "riting", ConditionResult.TRUE },
+            { metaphone, "xeon", "zeon", ConditionResult.TRUE },
+            { metaphone, "lonely", "loneli", ConditionResult.TRUE },
+            { metaphone, "bellaton", "belatton", ConditionResult.TRUE },
+            { metaphone, "pate", "patte", ConditionResult.TRUE },
+            { metaphone, "voiture", "vouatur", ConditionResult.TRUE },
+            { metaphone, "garbage", "garbedge", ConditionResult.TRUE },
+            { metaphone, "algorithme", "algorizm", ConditionResult.TRUE },
+            { metaphone, "testing", "testng", ConditionResult.TRUE },
+            { metaphone, "announce", "annonce", ConditionResult.TRUE },
+            { metaphone, "automaticly", "automatically", ConditionResult.TRUE },
+            { metaphone, "modifyd", "modified", ConditionResult.TRUE },
+            { metaphone, "bouteille", "butaille", ConditionResult.TRUE },
+            { metaphone, "xeon", "zeon", ConditionResult.TRUE },
+            { metaphone, "achtung", "aktung", ConditionResult.TRUE },
+            { metaphone, "throttle", "throddle", ConditionResult.TRUE },
+            { metaphone, "thimble", "thimblle", ConditionResult.TRUE },
+            { metaphone, "", "", ConditionResult.TRUE }, };
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeBuilderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeBuilderTestCase.java
new file mode 100644
index 0000000..1a3f307
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeBuilderTestCase.java
@@ -0,0 +1,176 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.MapAssert.*;
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.util.List;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class AttributeTypeBuilderTestCase extends AbstractSchemaTestCase {
+
+    @DataProvider
+    Object[][] validAttributeTypes() {
+        // OID, names, description, obsolete, superior type, equalityMR,
+        // orderingMR, substringMR, approximateMR, syntax, singleValue,
+        // collective, noUserModification, attributeUsage, extraPropertyName,
+        // extraPropertyValue
+        return new Object[][] {
+            // Basic attribute type
+            { "1.2.3.4", singletonList("MyAttributeType"), "MyAttributeType description.", false, "2.5.4.0",
+                EMR_CERTIFICATE_EXACT_OID, OMR_UUID_OID, SMR_CASE_IGNORE_LIST_OID, AMR_DOUBLE_METAPHONE_OID, null,
+                false, false, false, AttributeUsage.USER_APPLICATIONS, "New extra property", "New extra value", false },
+            // Allowed overrides existing core schema attribute type name
+            { "2.5.4.41", singletonList("name"), "MyAttributeType description.", false, null,
+                EMR_CERTIFICATE_EXACT_OID, null, SMR_CASE_IGNORE_LIST_OID, AMR_DOUBLE_METAPHONE_OID,
+                SYNTAX_DIRECTORY_STRING_OID, false, false, false, AttributeUsage.USER_APPLICATIONS,
+                "New extra property", "New extra value", true },
+            // No name provided, should be validated
+            { "1.2.3.4", emptyList(), "MyAttributeType description.", false, null, EMR_CASE_IGNORE_LIST_OID,
+                OMR_CASE_IGNORE_OID, null, null, SYNTAX_DIRECTORY_STRING_OID, false, false, false,
+                AttributeUsage.USER_APPLICATIONS, "New extra property", "New extra value", false },
+            // Empty description, should be validated
+            { "1.2.3.4", singletonList("MyAttributeType"), "", false, null, EMR_CASE_IGNORE_LIST_OID, null, null,
+                null, SYNTAX_DIRECTORY_STRING_OID, false, false, true, AttributeUsage.DIRECTORY_OPERATION,
+                "New extra property", "New extra value", false },
+        };
+    }
+
+    @Test(dataProvider = "validAttributeTypes")
+    public void testValidAttributeTypeBuilder(final String oid, final List<String> names, final String description,
+            final boolean obsolete, final String superiorType, final String equalityMatchingRule,
+            final String orderingMatchingRule, final String substringMatchingRule,
+            final String approximateMatchingRule, final String syntax, final boolean singleValue,
+            final boolean collective, final boolean noUserModification, final AttributeUsage attributeUsage,
+            final String extraPropertyName, String extraPropertyValue, final boolean overwrite) throws Exception {
+        AttributeType.Builder atBuilder = new SchemaBuilder(getCoreSchema())
+            .buildAttributeType(oid)
+            .names(names)
+            .description(description)
+            .obsolete(obsolete)
+            .superiorType(superiorType)
+            .equalityMatchingRule(equalityMatchingRule)
+            .orderingMatchingRule(orderingMatchingRule)
+            .substringMatchingRule(substringMatchingRule)
+            .approximateMatchingRule(approximateMatchingRule)
+            .syntax(syntax)
+            .singleValue(singleValue)
+            .collective(collective)
+            .noUserModification(noUserModification)
+            .usage(attributeUsage)
+            .extraProperties(extraPropertyName, extraPropertyValue);
+
+        Schema schema = overwrite ? atBuilder.addToSchemaOverwrite().toSchema()
+                                  : atBuilder.addToSchema().toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final AttributeType at = schema.getAttributeType(oid);
+        assertThat(at).isNotNull();
+        assertThat(at.getOID()).isEqualTo(oid);
+        assertThat(at.getNames()).containsOnly(names.toArray());
+        assertThat(at.getDescription()).isEqualTo(description);
+        assertThat(at.isObsolete()).isEqualTo(obsolete);
+        assertThat(at.getExtraProperties()).includes(entry(extraPropertyName, singletonList(extraPropertyValue)));
+    }
+
+    @Test
+    public void testAttributeTypeBuilderDefaultValues() throws Exception {
+        final String testOID = "1.1.1.42";
+        final SchemaBuilder sb = new SchemaBuilder(getCoreSchema());
+        AttributeType.Builder ocBuilder = sb.buildAttributeType(testOID)
+                                            .names("defaultAttributeType")
+                                            .syntax(SYNTAX_OID_OID)
+                                            .usage(AttributeUsage.USER_APPLICATIONS);
+
+        Schema schema = ocBuilder.addToSchema().toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        AttributeType at = schema.getAttributeType(testOID);
+        assertThat(at).isNotNull();
+        assertThat(at.getOID()).isEqualTo(testOID);
+        assertThat(at.getNames()).containsOnly("defaultAttributeType");
+        assertThat(at.getDescription()).isEqualTo("");
+        assertThat(at.isObsolete()).isFalse();
+        assertThat(at.getSuperiorType()).isNull();
+        assertThat(at.getSyntax().getOID()).isEqualTo(SYNTAX_OID_OID);
+        assertThat(at.isSingleValue()).isFalse();
+        assertThat(at.isCollective()).isFalse();
+        assertThat(at.isNoUserModification()).isFalse();
+        assertThat(at.getExtraProperties()).isEmpty();
+    }
+
+    @Test
+    public void testAttributeTypeBuilderCopyConstructor() throws Exception {
+        SchemaBuilder sb = new SchemaBuilder(getCoreSchema());
+        AttributeType.Builder atBuilder = sb.buildAttributeType("1.1.1.42")
+                                            .names("AttributeTypeToDuplicate")
+                                            .description("Attribute type to duplicate")
+                                            .usage(AttributeUsage.USER_APPLICATIONS)
+                                            .syntax(SYNTAX_OID_OID);
+        Schema schema = atBuilder.addToSchema().toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        sb.buildAttributeType(schema.getAttributeType("AttributeTypeToDuplicate"))
+                .oid("1.1.1.43")
+                .names("Copy")
+                .obsolete(true)
+                .addToSchemaOverwrite();
+        Schema schemaCopy = sb.toSchema();
+        assertThat(schemaCopy.getWarnings()).isEmpty();
+
+        AttributeType atCopy = schemaCopy.getAttributeType("Copy");
+        assertThat(atCopy).isNotNull();
+        assertThat(atCopy.getOID()).isEqualTo("1.1.1.43");
+        assertThat(atCopy.getDescription()).isEqualTo("Attribute type to duplicate");
+        assertThat(atCopy.isObsolete()).isTrue();
+        assertThat(atCopy.getNames()).containsOnly("AttributeTypeToDuplicate", "Copy");
+        assertThat(atCopy.getExtraProperties()).isEmpty();
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testAttributeTypeBuilderDoesNotAllowOverwrite() throws Exception {
+        AttributeType.Builder atBuilder = new SchemaBuilder(getCoreSchema())
+            .buildAttributeType("2.5.4.25")
+            .description("MyAttributeType description")
+            .names("internationalISDNNumber")
+            .syntax(SYNTAX_OID_OID)
+            .usage(AttributeUsage.DSA_OPERATION)
+            .extraProperties("New extra property", "New extra value");
+
+        atBuilder.addToSchema().toSchema();
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testAttributeTypeBuilderDoesNotAllowEmptyOID() throws Exception {
+        AttributeType.Builder atBuilder = new SchemaBuilder(getCoreSchema())
+            .buildAttributeType("")
+            .description("MyAttributeType description")
+            .names("MyAttributeType")
+            .syntax(SYNTAX_OID_DESCRIPTION)
+            .usage(AttributeUsage.DIRECTORY_OPERATION)
+            .extraProperties("New extra property", "New extra value");
+
+        atBuilder.addToSchema().toSchema();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxTest.java
new file mode 100644
index 0000000..5f79e15
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeSyntaxTest.java
@@ -0,0 +1,114 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_ATTRIBUTE_TYPE_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Attribute type syntax tests. */
+@Test
+public class AttributeTypeSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            {
+                "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " USAGE userApplications )", true },
+            {
+                "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " X-APPROX 'equalLengthApproximateMatch'"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " COLLECTIVE USAGE userApplications )", true },
+            {
+                "(1.2.8.5 NAME 'testtype')", true },
+            {
+                "(1.2.8.5 NAME 'testtype' DESC 'full type')", true },
+            {
+                "(1.2.8.5 NAME 'testType' DESC 'full type' EQUALITY caseIgnoreMatch "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)", true},
+            {
+                "(1.2.8.5 NAME 'testType' DESC 'full type' EQUALITY caseIgnoreMatch "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'test' )", true},
+            {   "(1.2.8.5 NAME 'testType' DESC 'full type' EQUALITY caseIgnoreMatch "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'test' "
+                        + " X-SCHEMA-FILE '33-test.ldif' )", true},
+            {
+                "(1.2.8.5 USAGE directoryOperation )", true },
+            {
+                "(1.2.8.5 USAGE directoryOperation)", true },
+            {
+                "(1.2.8.5 USAGE directoryOperation X-SCHEMA-FILE '99-test.ldif')", true },
+
+            // Collective can inherit from non-collective
+            {   "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " COLLECTIVE USAGE userApplications )", true},
+            // Collective can be operational
+            {   "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " COLLECTIVE USAGE directoryOperation )", true},
+
+            // X-NAME is invalid extension (no value)
+            {   "(1.2.8.5 NAME 'testType' DESC 'full type' EQUALITY caseIgnoreMatch "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'test' "
+                        + " X-SCHEMA-FILE '33-test.ldif' X-NAME )", false},
+
+            {
+                "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " COLLECTIVE USAGE badUsage )", false },
+            {
+                "(1.2.8.a.b NAME 'testtype' DESC 'full type' OBSOLETE "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " COLLECTIVE USAGE directoryOperation )", false },
+            {
+                "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " BADTOKEN USAGE directoryOperation )", false },
+
+            // NO-USER-MODIFICATION can't have non-operational usage
+            {
+                "1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE "
+                        + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                        + " SUBSTR caseIgnoreSubstringsMatch"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                        + " NO-USER-MODIFICATION USAGE userApplications", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_ATTRIBUTE_TYPE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeTest.java
new file mode 100644
index 0000000..c261aa0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AttributeTypeTest.java
@@ -0,0 +1,547 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Attribute type tests.
+ */
+@SuppressWarnings("javadoc")
+public class AttributeTypeTest extends AbstractSchemaElementTestCase {
+    private final Schema schema;
+
+    public AttributeTypeTest() throws Exception {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.buildAttributeType("1.2.1")
+               .names(EMPTY_NAMES)
+               .obsolete(true)
+               .syntax("1.3.6.1.4.1.1466.115.121.1.27")
+               .singleValue(true)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .addToSchema();
+
+        builder.addAttributeType(
+                "( 1.2.2 OBSOLETE SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE "
+                        + " COLLECTIVE X-ORIGIN ( 'Sun Java System Identity Management' "
+                        + "'user defined' ) X-SCHEMA-FILE '98sunEmp.ldif')", false);
+
+        builder.buildAttributeType("1.2.3")
+               .names("testType")
+               .superiorType("1.2.2")
+               .syntax("1.3.6.1.4.1.1466.115.121.1.27")
+               .collective(true)
+               .usage(AttributeUsage.USER_APPLICATIONS)
+               .addToSchema();
+
+        builder.addAttributeType("( 1.2.4 NAME 'testType' SUP 1.2.3 SINGLE-VALUE COLLECTIVE )", false);
+
+        builder.buildAttributeType("1.2.5")
+               .names("testType", "testnamealias", "anothernamealias")
+               .equalityMatchingRule(EMR_CASE_IGNORE_LIST_OID)
+               .substringMatchingRule(SMR_CASE_IGNORE_LIST_OID)
+               .approximateMatchingRule(AMR_DOUBLE_METAPHONE_OID)
+               .syntax(SYNTAX_INTEGER_OID)
+               .noUserModification(true)
+               .usage(AttributeUsage.DSA_OPERATION)
+               .addToSchema();
+
+        builder.addAttributeType("( 1.2.6 NAME ( 'testType' 'testnamealias' 'anothernamealias1' ) "
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SUP anothernamealias"
+                + " USAGE dSAOperation NO-USER-MODIFICATION )", false);
+
+        schema = builder.toSchema();
+        if (!schema.getWarnings().isEmpty()) {
+            throw new Exception("Base schema not valid!");
+        }
+    }
+
+    @Override
+    @DataProvider(name = "equalsTestData")
+    public Object[][] createEqualsTestData() throws SchemaException, DecodeException {
+        return new Object[][] {
+            { schema.getAttributeType("1.2.3"), schema.getAttributeType("1.2.3"), true },
+            { schema.getAttributeType("1.2.4"), schema.getAttributeType("1.2.3"), false } };
+    }
+
+    @Test
+    public void testADSyntax() throws Exception {
+        // AD uses single quotes around OIDs
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' " + " SUP '1.2.5' "
+                + " EQUALITY 'caseIgnoreMatch' "
+                + " SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' USAGE dSAOperation )", false);
+        Assert.assertTrue(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testADSyntaxQuoteMismatch() throws Exception {
+        // AD uses single quotes around OIDs
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' " + " SUP '1.2.5 "
+                + " EQUALITY 'caseIgnoreMatch' "
+                + " SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' USAGE dSAOperation )", false);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test
+    public void testCollectiveOperational() throws Exception {
+        // Collective can't be operational
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE "
+                + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                + " SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                + " COLLECTIVE USAGE directoryOperation )", false);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    /**
+     * Check constructor sets the default matching rules correctly.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructorDefaultMatchingRules() throws Exception {
+        final AttributeType type = schema.getAttributeType("1.2.1");
+
+        final Syntax syntax = schema.getSyntax("1.3.6.1.4.1.1466.115.121.1.27");
+        Assert.assertEquals(type.getApproximateMatchingRule(), syntax.getApproximateMatchingRule());
+        Assert.assertEquals(type.getEqualityMatchingRule(), syntax.getEqualityMatchingRule());
+        Assert.assertEquals(type.getOrderingMatchingRule(), syntax.getOrderingMatchingRule());
+        Assert.assertEquals(type.getSubstringMatchingRule(), syntax.getSubstringMatchingRule());
+    }
+
+    /**
+     * Check constructor sets the default usage correctly.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructorDefaultUsage() throws Exception {
+        final AttributeType d = schema.getAttributeType("1.2.2");
+
+        Assert.assertEquals(d.getUsage(), AttributeUsage.USER_APPLICATIONS);
+    }
+
+    /**
+     * Check constructor inherits the matching rules from the parent type when
+     * required.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dependsOnMethods = "testConstructorMatchingRules")
+    public void testConstructorInheritsMatchingRules() throws Exception {
+        final AttributeType parent = schema.getAttributeType("1.2.5");
+
+        final AttributeType child = schema.getAttributeType("1.2.6");
+
+        Assert.assertEquals(parent.getApproximateMatchingRule(), child.getApproximateMatchingRule());
+        Assert.assertEquals(parent.getEqualityMatchingRule(), child.getEqualityMatchingRule());
+        // It should inherit ordering rule from parent's syntax since parent
+        // didn't specify an ordering matching rule.
+        Assert.assertEquals(parent.getSyntax().getOrderingMatchingRule(), child
+                .getOrderingMatchingRule());
+        Assert.assertEquals(parent.getSubstringMatchingRule(), child.getSubstringMatchingRule());
+    }
+
+    /**
+     * Check constructor inherits the syntax from the parent type when required.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dependsOnMethods = "testConstructorSyntax")
+    public void testConstructorInheritsSyntax() throws Exception {
+        AttributeType parent = schema.getAttributeType("1.2.3");
+
+        AttributeType child = schema.getAttributeType("1.2.4");
+
+        Assert.assertEquals(parent.getSyntax(), child.getSyntax());
+
+        parent = schema.getAttributeType("1.2.2");
+
+        child = schema.getAttributeType("1.2.3");
+        Assert.assertFalse(parent.getSyntax().equals(child.getSyntax()));
+
+        // Make sure paren't s syntax was not inherited in this case
+        child = schema.getAttributeType("1.2.6");
+        Assert.assertEquals(child.getSyntax().getOID(), SYNTAX_DIRECTORY_STRING_OID);
+    }
+
+    /**
+     * Check constructor sets the matching rules correctly.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructorMatchingRules() throws Exception {
+        final AttributeType type = schema.getAttributeType("1.2.5");
+
+        Assert.assertEquals(type.getEqualityMatchingRule().getOID(), EMR_CASE_IGNORE_LIST_OID);
+        Assert.assertEquals(type.getOrderingMatchingRule().getOID(), type.getSyntax()
+                .getOrderingMatchingRule().getOID());
+        Assert.assertEquals(type.getSubstringMatchingRule().getOID(), SMR_CASE_IGNORE_LIST_OID);
+        Assert.assertEquals(type.getApproximateMatchingRule().getOID(), AMR_DOUBLE_METAPHONE_OID);
+    }
+
+    /**
+     * Check that the primary name is added to the set of names.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testConstructorPrimaryName() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.3");
+
+        Assert.assertTrue(d.hasName("testType"));
+        Assert.assertFalse(d.hasName("xxx"));
+
+        d = schema.getAttributeType("1.2.4");
+
+        Assert.assertTrue(d.hasName("testType"));
+        Assert.assertFalse(d.hasName("xxx"));
+
+    }
+
+    /**
+     * Check constructor sets the syntax correctly.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testConstructorSyntax() throws Exception {
+        final AttributeType d = schema.getAttributeType("1.2.2");
+
+        Assert.assertEquals(d.getSyntax().getOID(), "1.3.6.1.4.1.1466.115.121.1.15");
+    }
+
+    /**
+     * Check that the type names are accessible.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testConstructorTypeNames() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.5");
+
+        Assert.assertTrue(d.hasName("testType"));
+        Assert.assertTrue(d.hasName("testnamealias"));
+        Assert.assertTrue(d.hasName("anothernamealias"));
+
+        d = schema.getAttributeType("1.2.6");
+
+        Assert.assertTrue(d.hasName("testType"));
+        Assert.assertTrue(d.hasName("testnamealias"));
+        Assert.assertTrue(d.hasName("anothernamealias1"));
+    }
+
+    /**
+     * Check that the {@link AttributeType#getUsage()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testGetAttributeUsage() throws Exception {
+        AttributeType type = schema.getAttributeType("1.2.1");
+        Assert.assertEquals(type.getUsage(), AttributeUsage.USER_APPLICATIONS);
+        type = schema.getAttributeType("1.2.6");
+        Assert.assertEquals(type.getUsage(), AttributeUsage.DSA_OPERATION);
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#getNameOrOID()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetNameOrOIDReturnsOID() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.1");
+
+        Assert.assertEquals(d.getNameOrOID(), "1.2.1");
+
+        d = schema.getAttributeType("1.2.2");
+
+        Assert.assertEquals(d.getNameOrOID(), "1.2.2");
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#getNameOrOID()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetNameOrOIDReturnsPrimaryName() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.3");
+        Assert.assertEquals(d.getNameOrOID(), "testType");
+        d = schema.getAttributeType("1.2.4");
+        Assert.assertEquals(d.getNameOrOID(), "testType");
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#getNormalizedNames()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetNormalizedNames() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.5");
+        Iterator<String> i = d.getNames().iterator();
+        Assert.assertEquals(i.next(), "testType");
+        Assert.assertEquals(i.next(), "testnamealias");
+        Assert.assertEquals(i.next(), "anothernamealias");
+
+        d = schema.getAttributeType("1.2.6");
+        i = d.getNames().iterator();
+        Assert.assertEquals(i.next(), "testType");
+        Assert.assertEquals(i.next(), "testnamealias");
+        Assert.assertEquals(i.next(), "anothernamealias1");
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#getOID()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testGetOID() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.3");
+        Assert.assertEquals(d.getOID(), "1.2.3");
+        d = schema.getAttributeType("1.2.4");
+        Assert.assertEquals(d.getOID(), "1.2.4");
+
+    }
+
+    /**
+     * Check that the {@link AttributeType#getSuperiorType()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testGetSuperiorType() throws Exception {
+        AttributeType type = schema.getAttributeType("1.2.3");
+        Assert.assertEquals(type.getSuperiorType().getOID(), "1.2.2");
+        type = schema.getAttributeType("1.2.4");
+        Assert.assertEquals(type.getSuperiorType().getOID(), "1.2.3");
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#hasNameOrOID(String)} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testHasNameOrOID() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.3");
+
+        Assert.assertTrue(d.hasNameOrOID("testType"));
+        Assert.assertTrue(d.hasNameOrOID("1.2.3"));
+        Assert.assertFalse(d.hasNameOrOID("x.y.z"));
+        d = schema.getAttributeType("1.2.4");
+
+        Assert.assertTrue(d.hasNameOrOID("testType"));
+        Assert.assertTrue(d.hasNameOrOID("1.2.4"));
+        Assert.assertFalse(d.hasNameOrOID("x.y.z"));
+    }
+
+    @Test
+    public void testInheritFromNonCollective() throws Exception {
+        // Collective can't inherit from non-collective
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' "
+                + " OBSOLETE SUP 1.2.5 "
+                + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                + " SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                + " COLLECTIVE USAGE userApplications )", false);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test
+    public void testInheritFromUserAppUsage() throws Exception {
+        // directoryOperation can't inherit from userApplications
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' "
+                + " OBSOLETE SUP 1.2.1 "
+                + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                + " SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                + " NO-USER-MODIFICATION USAGE directoryOperation )", false);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    /**
+     * Check that the {@link AttributeType#isCollective()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testIsCollective() throws Exception {
+        AttributeType type = schema.getAttributeType("1.2.2");
+        Assert.assertTrue(type.isCollective());
+        type = schema.getAttributeType("1.2.3");
+        Assert.assertTrue(type.isCollective());
+        type = schema.getAttributeType("1.2.6");
+        Assert.assertFalse(type.isCollective());
+        type = schema.getAttributeType("1.2.5");
+        Assert.assertFalse(type.isCollective());
+    }
+
+    /**
+     * Check that the {@link AttributeType#isNoUserModification()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testIsNoUserModification() throws Exception {
+        AttributeType type = schema.getAttributeType("1.2.5");
+        Assert.assertTrue(type.isNoUserModification());
+        type = schema.getAttributeType("1.2.6");
+        Assert.assertTrue(type.isNoUserModification());
+        type = schema.getAttributeType("1.2.3");
+        Assert.assertFalse(type.isNoUserModification());
+        type = schema.getAttributeType("1.2.4");
+        Assert.assertFalse(type.isNoUserModification());
+    }
+
+    /**
+     * Check that the {@link CommonSchemaElements#isObsolete()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testIsObsolete() throws Exception {
+        AttributeType d = schema.getAttributeType("1.2.3");
+        Assert.assertFalse(d.isObsolete());
+        d = schema.getAttributeType("1.2.4");
+        Assert.assertFalse(d.isObsolete());
+
+        d = schema.getAttributeType("1.2.1");
+        Assert.assertTrue(d.isObsolete());
+        d = schema.getAttributeType("1.2.2");
+        Assert.assertTrue(d.isObsolete());
+    }
+
+    /**
+     * Check that the {@link AttributeType#isSingleValue()} method.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testIsSingleValue() throws Exception {
+        AttributeType type = schema.getAttributeType("1.2.1");
+        Assert.assertTrue(type.isSingleValue());
+        type = schema.getAttributeType("1.2.2");
+        Assert.assertTrue(type.isSingleValue());
+        type = schema.getAttributeType("1.2.5");
+        Assert.assertFalse(type.isSingleValue());
+        type = schema.getAttributeType("1.2.6");
+        Assert.assertFalse(type.isSingleValue());
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void attributeTypeDefinitionsRequireSyntaxOrSuperiorWhenStrict1() throws Exception {
+        new SchemaBuilder(Schema.getCoreSchema())
+                .setOption(SchemaOptions.ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX, false)
+                .addAttributeType("(1.2.1)", true);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void attributeTypeDefinitionsRequireSyntaxOrSuperiorWhenStrict2() throws Exception {
+        new SchemaBuilder(Schema.getCoreSchema())
+                .setOption(SchemaOptions.ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX, false)
+                .buildAttributeType("1.2.1")
+                .addToSchema();
+    }
+
+    @Test
+    public void attributeTypeDefinitionsDoNotRequireSyntaxOrSuperiorWhenTolerant1() throws Exception {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .addAttributeType("(1.2.1)", true).toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("1.2.1").getSuperiorType()).isNull();
+        assertThat(schema.getAttributeType("1.2.1").getSyntax()).isSameAs(schema.getDefaultSyntax());
+    }
+
+    @Test
+    public void attributeTypeDefinitionsDoNotRequireSyntaxOrSuperiorWhenTolerant2() throws Exception {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildAttributeType("1.2.1")
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("1.2.1").getSuperiorType()).isNull();
+        assertThat(schema.getAttributeType("1.2.1").getSyntax()).isSameAs(schema.getDefaultSyntax());
+    }
+
+    @Test
+    public void testNoUserModNonOperational() throws Exception {
+        // NO-USER-MODIFICATION can't have non-operational usage
+        final SchemaBuilder builder = new SchemaBuilder(schema);
+        builder.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE "
+                + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                + " SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                + " NO-USER-MODIFICATION USAGE userApplications )", false);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Override
+    protected SchemaElement getElement(final String description,
+            final Map<String, List<String>> extraProperties) throws SchemaException {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.buildAttributeType("1.2.3")
+               .names("testType")
+               .description(description)
+               .syntax("1.3.6.1.4.1.1466.115.121.1.27")
+               .usage(AttributeUsage.DSA_OPERATION)
+               .extraProperties(extraProperties)
+               .addToSchema();
+
+        return builder.toSchema().getAttributeType("1.2.3");
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImplTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImplTest.java
new file mode 100644
index 0000000..84b4a5e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/AuthPasswordSyntaxImplTest.java
@@ -0,0 +1,76 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.*;
+
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Tests the AuthPasswordSyntaxImpl. */
+@Test
+@SuppressWarnings("javadoc")
+public class AuthPasswordSyntaxImplTest extends ForgeRockTestCase {
+
+    @DataProvider
+    public Object[][] validEncodedPasswords() {
+        return new Object[][] {
+            { "0$0$0", "0", "0", "0" },
+            { " 0$0$0", "0", "0", "0" },
+            { "0 $0$0", "0", "0", "0" },
+            { "0$ 0$0", "0", "0", "0" },
+            { "0$0 $0", "0", "0", "0" },
+            { "0$0$ 0", "0", "0", "0" },
+            { "0$0$0 ", "0", "0", "0" },
+        };
+    }
+
+    @Test(dataProvider = "validEncodedPasswords")
+    public void decodeValidPassword(String encodedPassword, String expectedScheme, String expectedAuthInfo,
+            String expectedAuthValue) throws Exception {
+        assertThat(AuthPasswordSyntaxImpl.decodeAuthPassword(encodedPassword))
+                .isEqualTo(new String[] {expectedScheme, expectedAuthInfo, expectedAuthValue});
+    }
+
+    @DataProvider
+    public Object[][] invalidEncodedPasswords() {
+        return new Object[][] {
+            { "", "zero-length scheme element" },
+            { "$", "zero-length scheme element" },
+            { "0$$", "zero-length authInfo element" },
+            { "0$0$", "zero-length authValue element" },
+            { "a", "invalid scheme character" },
+            { "0 #", "illegal character between the scheme and authInfo element" },
+            { "0$0#", "invalid authInfo character" },
+            { "0$0 #", "illegal character between the authInfo and authValue element" },
+            { "0$0$\n", "invalid authValue character" },
+            { "0$0$0$", "invalid trailing character" },
+        };
+    }
+
+    @Test(dataProvider = "invalidEncodedPasswords")
+    public void decodeInvalidPassword(String encodedPassword, String errorMsg) throws Exception {
+        try {
+            AuthPasswordSyntaxImpl.decodeAuthPassword(encodedPassword);
+            Assert.fail("Expected DirectoryException");
+        } catch (DecodeException e) {
+            assertThat(e.getMessage()).contains(errorMsg);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..5157b8d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringEqualityMatchingRuleTest.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_BIT_STRING_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the BitStringEqualityMatchingRule. */
+public class BitStringEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            { "\'a\'B" },
+            { "0" },
+            { "010101" },
+            { "\'10101" },
+            { "\'1010\'A" }, };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "\'0\'B", "\'0\'B", ConditionResult.TRUE },
+            { "\'1\'B", "\'1\'B", ConditionResult.TRUE },
+            { "\'0\'B", "\'1\'B", ConditionResult.FALSE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_BIT_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxTest.java
new file mode 100644
index 0000000..9d7a650c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BitStringSyntaxTest.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_BIT_STRING_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Bit string syntax tests. */
+@Test
+public class BitStringSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            { "'0101'B", true },
+            { "'1'B", true },
+            { "'0'B", true },
+            { "invalid", false },
+            { "1", false },
+            { "'010100000111111010101000'B", true },
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_BIT_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..2614b36
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanEqualityMatchingRuleTest.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_BOOLEAN_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the BooleanEqualityMatchingRule. */
+public class BooleanEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { { "garbage" }, };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "TRUE", "true", ConditionResult.TRUE },
+            { "YES", "true", ConditionResult.TRUE },
+            { "ON", "true", ConditionResult.TRUE },
+            { "1", "true", ConditionResult.TRUE },
+            { "FALSE", "false", ConditionResult.TRUE },
+            { "NO", "false", ConditionResult.TRUE },
+            { "OFF", "false", ConditionResult.TRUE },
+            { "0", "false", ConditionResult.TRUE },
+            { "TRUE", "false", ConditionResult.FALSE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_BOOLEAN_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxTest.java
new file mode 100644
index 0000000..c283de3
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/BooleanSyntaxTest.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_BOOLEAN_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Boolean syntax tests. */
+@Test
+public class BooleanSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            { "TRUE", true },
+            { "FALSE", true },
+            { "true", true },
+            { "false", true },
+            { "YES", true },
+            { "NO", true },
+            { "ON", true },
+            { "OFF", true },
+            { "1", true },
+            { "0", true },
+            { "'0'B", false },
+            { "invalidtrue", false },
+            { " true", false },
+            { "NOT", false },
+            { "'010100000111111010101000'B", false }
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_BOOLEAN_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..fb4e0c7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactEqualityMatchingRuleTest.java
@@ -0,0 +1,50 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_EXACT_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactEqualityMatchingRule. */
+public class CaseExactEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "12345678", "12345678", ConditionResult.TRUE },
+            { "12345678\u2163", "12345678\u2163", ConditionResult.TRUE },
+            { "ABC45678", "ABC45678", ConditionResult.TRUE },
+            { "  ABC45678  ", "ABC45678", ConditionResult.TRUE },
+            { "ABC   45678", "ABC 45678", ConditionResult.TRUE },
+            { "   ", " ", ConditionResult.TRUE },
+            { "", "", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.FALSE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_CASE_EXACT_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleTest.java
new file mode 100644
index 0000000..0c10fc1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5EqualityMatchingRuleTest.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_EXACT_IA5_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactIA5EqualityMatchingRule. */
+public class CaseExactIA5EqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            { "12345678\uFFFD" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "12345678", "12345678", ConditionResult.TRUE },
+            { "ABC45678", "ABC45678", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.FALSE },
+            { "\u0020foo\u0020bar\u0020\u0020", "foo bar", ConditionResult.TRUE },
+            { "test\u00AD\u200D", "test", ConditionResult.TRUE },
+            { "foo\u000Bbar", "foo\u0020bar", ConditionResult.TRUE },
+            { "foo\u070Fbar", "foobar", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_CASE_EXACT_IA5_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleTest.java
new file mode 100644
index 0000000..4bb534b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactIA5SubstringMatchingRuleTest.java
@@ -0,0 +1,99 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_EXACT_IA5_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactIA5SubstringMatchingRule. */
+public class CaseExactIA5SubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {
+            { "12345678\uFFFD", new String[0], null },
+            { null, strings("12345678\uFFFD"), null },
+            { null, strings(), "12345678\uFFFD" }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { { "12345678\uFFFD" }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            { "this is a value", "value", ConditionResult.TRUE },
+            { "this is a value", "alue", ConditionResult.TRUE },
+            { "this is a value", "ue", ConditionResult.TRUE },
+            { "this is a value", "e", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "this", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.FALSE },
+            { "this is a VALUE", "value", ConditionResult.FALSE },
+            { "end with space    ", " ", ConditionResult.FALSE },
+            { "end with space    ", "space", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "this is a value", "this", ConditionResult.TRUE },
+            { "this is a value", "th", ConditionResult.TRUE },
+            { "this is a value", "t", ConditionResult.TRUE },
+            { "this is a value", "is", ConditionResult.FALSE },
+            { "this is a value", "a", ConditionResult.FALSE },
+            { "this is a value", "value", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "NOT", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.FALSE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            { "this is a value", strings("this"), ConditionResult.TRUE },
+            { "this is a value", strings("is"), ConditionResult.TRUE },
+            { "this is a value", strings("a"), ConditionResult.TRUE },
+            { "this is a value", strings("value"), ConditionResult.TRUE },
+            { "this is a value", strings(" "), ConditionResult.TRUE },
+            { "this is a value", strings("this", "is", "a", "value"), ConditionResult.TRUE },
+            // The matching rule requires ordered non overlapping substrings
+            // Issue #730 was not valid.
+            { "this is a value", strings("value", "this"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "this is"), ConditionResult.FALSE },
+            { "this is a value", strings("his is", "a val"), ConditionResult.TRUE },
+            { "this is a value", strings("not"), ConditionResult.FALSE },
+            { "this is a value", strings("THIS"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "not"), ConditionResult.FALSE },
+            { "this is a value", strings("    "), ConditionResult.TRUE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(SMR_CASE_EXACT_IA5_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..0b6b7dc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactOrderingMatchingRuleTest.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_EXACT_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactOrderingMatchingRule. */
+public class CaseExactOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            { "12345678", "02345678", 1 },
+            { "abcdef", "bcdefa", -1 },
+            { "abcdef", "abcdef", 0 }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_CASE_EXACT_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleTest.java
new file mode 100644
index 0000000..3ff3625
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseExactSubstringMatchingRuleTest.java
@@ -0,0 +1,97 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_EXACT_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactSubstringMatchingRule class. */
+public class CaseExactSubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            { "this is a value", "value", ConditionResult.TRUE },
+            { "this is a value", "alue", ConditionResult.TRUE },
+            { "this is a value", "ue", ConditionResult.TRUE },
+            { "this is a value", "e", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "this", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.FALSE },
+            { "this is a VALUE", "value", ConditionResult.FALSE },
+            { "end with space    ", " ", ConditionResult.FALSE },
+            { "end with space    ", "space", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "this is a value", "this", ConditionResult.TRUE },
+            { "this is a value", "th", ConditionResult.TRUE },
+            { "this is a value", "t", ConditionResult.TRUE },
+            { "this is a value", "is", ConditionResult.FALSE },
+            { "this is a value", "a", ConditionResult.FALSE },
+            { "this is a value", "value", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "NOT", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.FALSE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            { "this is a value", strings("this"), ConditionResult.TRUE },
+            { "this is a value", strings("is"), ConditionResult.TRUE },
+            { "this is a value", strings("a"), ConditionResult.TRUE },
+            { "this is a value", strings("value"), ConditionResult.TRUE },
+            { "this is a value", strings(" "), ConditionResult.TRUE },
+            { "this is a value", strings("this", "is", "a", "value"), ConditionResult.TRUE },
+            // The matching rule requires ordered non overlapping substrings.
+            // Issue #730 was not valid.
+            { "this is a value", strings("value", "this"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "this is"), ConditionResult.FALSE },
+            { "this is a value", strings("his is", "a val"), ConditionResult.TRUE },
+            { "this is a value", strings("not"), ConditionResult.FALSE },
+            { "this is a value", strings("THIS"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "not"), ConditionResult.FALSE },
+            { "this is a value", strings("    "), ConditionResult.TRUE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(SMR_CASE_EXACT_OID);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..01d112b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreEqualityMatchingRuleTest.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseIgnoreEqualityMatchingRule. */
+public class CaseIgnoreEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "12345678", "12345678", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.TRUE },
+            { " string ", "string", ConditionResult.TRUE },
+            { "string ", "string", ConditionResult.TRUE },
+            { " string", "string", ConditionResult.TRUE },
+            { "    ", " ", ConditionResult.TRUE },
+            { "Z", "z", ConditionResult.TRUE },
+            { "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", "abcdefghijklmnopqrstuvwxyz1234567890",
+                ConditionResult.TRUE },
+            { "foo\u0020bar\u0020\u0020", "foo bar", ConditionResult.TRUE },
+            { "test\u00AD\u200D", "test", ConditionResult.TRUE },
+            { "foo\u070Fbar", "foobar", ConditionResult.TRUE },
+            // Case-folding data below.
+            { "foo\u0149bar", "foo\u02BC\u006Ebar", ConditionResult.TRUE },
+            { "foo\u017Bbar", "foo\u017Cbar", ConditionResult.TRUE },
+            { "foo\u017BBAR", "foo\u017Cbar", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_CASE_IGNORE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleTest.java
new file mode 100644
index 0000000..735859d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5EqualityMatchingRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_CASE_IGNORE_IA5_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseExactIA5EqualityMatchingRule. */
+public class CaseIgnoreIA5EqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            { "12345678\uFFFD" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "12345678", "12345678", ConditionResult.TRUE },
+            { "ABC45678", "ABC45678", ConditionResult.TRUE },
+            { "ABC45678", "abc45678", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_CASE_IGNORE_IA5_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleTest.java
new file mode 100644
index 0000000..1ccf4df
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreIA5SubstringMatchingRuleTest.java
@@ -0,0 +1,112 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_IA5_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseIgnoreIA5SubstringMatchingRule. */
+public class CaseIgnoreIA5SubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] { { "12345678\uFFFD", new String[0], null },
+            { null, strings("12345678\uFFFD"), null },
+            { null, strings(), "12345678\uFFFD" }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { { "12345678\uFFFD" }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            { "this is a value", "value", ConditionResult.TRUE },
+            { "this is a value", "alue", ConditionResult.TRUE },
+            { "this is a value", "ue", ConditionResult.TRUE },
+            { "this is a value", "e", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "this", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.TRUE },
+            { "this is a value", "AlUe", ConditionResult.TRUE },
+            { "this is a value", "UE", ConditionResult.TRUE },
+            { "this is a value", "E", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a VALUE", "value", ConditionResult.TRUE },
+            { "end with space    ", " ", ConditionResult.FALSE },
+            { "end with space    ", "space", ConditionResult.TRUE },
+            { "end with space    ", "SPACE", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "this is a value", "this", ConditionResult.TRUE },
+            { "this is a value", "th", ConditionResult.TRUE },
+            { "this is a value", "t", ConditionResult.TRUE },
+            { "this is a value", "is", ConditionResult.FALSE },
+            { "this is a value", "a", ConditionResult.FALSE },
+            { "this is a value", "TH", ConditionResult.TRUE },
+            { "this is a value", "T", ConditionResult.TRUE },
+            { "this is a value", "IS", ConditionResult.FALSE },
+            { "this is a value", "A", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "NOT", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            { "this is a value", strings("this"), ConditionResult.TRUE },
+            { "this is a value", strings("is"), ConditionResult.TRUE },
+            { "this is a value", strings("a"), ConditionResult.TRUE },
+            { "this is a value", strings("value"), ConditionResult.TRUE },
+            { "this is a value", strings("THIS"), ConditionResult.TRUE },
+            { "this is a value", strings("IS"), ConditionResult.TRUE },
+            { "this is a value", strings("A"), ConditionResult.TRUE },
+            { "this is a value", strings("VALUE"), ConditionResult.TRUE },
+            { "this is a value", strings(" "), ConditionResult.TRUE },
+            { "this is a value", strings("this", "is", "a", "value"), ConditionResult.TRUE },
+            // The matching rule requires ordered non overlapping substrings.
+            // Issue #730 was not valid.
+            { "this is a value", strings("value", "this"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "this is"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "IS", "a", "VALue"), ConditionResult.TRUE },
+            { "this is a value", strings("his IS", "A val"), ConditionResult.TRUE },
+            { "this is a value", strings("not"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "not"), ConditionResult.FALSE },
+            { "this is a value", strings("    "), ConditionResult.TRUE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(SMR_CASE_IGNORE_IA5_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..f104125
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreOrderingMatchingRuleTest.java
@@ -0,0 +1,54 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_CASE_IGNORE_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseIgnoreOrderingMatchingRule. */
+public class CaseIgnoreOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            { "12345678", "02345678", 1 }, { "abcdef", "bcdefa", -1 },
+            { "abcdef", "abcdef", 0 }, { "abcdef", "ABCDEF", 0 },
+            { "abcdef", "aCcdef", -1 },
+            { "aCcdef", "abcdef", 1 },
+            { "foo\u0020bar\u0020\u0020", "foo bar", 0 },
+            { "test\u00AD\u200D", "test", 0 },
+            { "foo\u070Fbar", "foobar", 0 },
+            // Case-folding data below.
+            { "foo\u0149bar", "foo\u02BC\u006Ebar", 0 },
+            { "foo\u017Bbar", "foo\u017Cbar", 0 },
+            { "foo\u017Bbar", "goo\u017Cbar", -1 },
+            // issue# 3583
+            { "a", "\u00f8", -1 }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_CASE_IGNORE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleTest.java
new file mode 100644
index 0000000..d6ff79c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CaseIgnoreSubstringMatchingRuleTest.java
@@ -0,0 +1,110 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the CaseIgnoreSubstringMatchingRule. */
+public class CaseIgnoreSubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            { "this is a value", "value", ConditionResult.TRUE },
+            { "this is a value", "alue", ConditionResult.TRUE },
+            { "this is a value", "ue", ConditionResult.TRUE },
+            { "this is a value", "e", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "this", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.TRUE },
+            { "this is a value", "AlUe", ConditionResult.TRUE },
+            { "this is a value", "UE", ConditionResult.TRUE },
+            { "this is a value", "E", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a VALUE", "value", ConditionResult.TRUE },
+            { "end with space    ", " ", ConditionResult.FALSE },
+            { "end with space    ", "space", ConditionResult.TRUE },
+            { "end with space    ", "SPACE", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "this is a value", "this", ConditionResult.TRUE },
+            { "this is a value", "th", ConditionResult.TRUE },
+            { "this is a value", "t", ConditionResult.TRUE },
+            { "this is a value", "is", ConditionResult.FALSE },
+            { "this is a value", "a", ConditionResult.FALSE },
+            { "this is a value", "TH", ConditionResult.TRUE },
+            { "this is a value", "T", ConditionResult.TRUE },
+            { "this is a value", "IS", ConditionResult.FALSE },
+            { "this is a value", "A", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.FALSE },
+            { "this is a value", "NOT", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.TRUE }, };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            { "this is a value", strings("this"), ConditionResult.TRUE },
+            { "this is a value", strings("is"), ConditionResult.TRUE },
+            { "this is a value", strings("a"), ConditionResult.TRUE },
+            { "this is a value", strings("value"), ConditionResult.TRUE },
+            { "this is a value", strings("THIS"), ConditionResult.TRUE },
+            { "this is a value", strings("IS"), ConditionResult.TRUE },
+            { "this is a value", strings("A"), ConditionResult.TRUE },
+            { "this is a value", strings("VALUE"), ConditionResult.TRUE },
+            { "this is a value", strings(" "), ConditionResult.TRUE },
+            { "this is a value", strings("this", "is", "a", "value"), ConditionResult.TRUE },
+            // The matching rule requires ordered non overlapping substrings.
+            // Issue #730 was not valid.
+            { "this is a value", strings("value", "this"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "this is"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "IS", "a", "VALue"), ConditionResult.TRUE },
+            { "this is a value", strings("his IS", "A val"), ConditionResult.TRUE },
+            { "this is a value", strings("not"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "not"), ConditionResult.FALSE },
+            { "this is a value", strings("    "), ConditionResult.TRUE }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(SMR_CASE_IGNORE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImplTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImplTest.java
new file mode 100644
index 0000000..fedff96
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImplTest.java
@@ -0,0 +1,203 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2008 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2014 Manuel Gaupp
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * This class tests the certificateExactMatch matching rule.
+ */
+@SuppressWarnings("javadoc")
+public class CertificateExactMatchingRuleImplTest extends AbstractSchemaTestCase {
+
+    /**
+     * Generate data for the certificateExactMatch matching rule test.
+     */
+    @DataProvider(name = "certificateExactMatchingRules")
+    public Object[][] createCertificateExactMatchingRuleTest() {
+        String validcert1
+            = "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
+            + "BAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRpb25lMRwwGgYDVQQLExNQcm9kdWN0IERl"
+            + "dmVsb3BtZW50MRQwEgYDVQQDEwtCYWJzIEplbnNlbjAeFw0xMjA1MDIxNjM0MzVa"
+            + "Fw0xMjEyMjExNjM0MzVaMFYxCzAJBgNVBAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRp"
+            + "b25lMRwwGgYDVQQLExNQcm9kdWN0IERldmVsb3BtZW50MRQwEgYDVQQDEwtCYWJz"
+            + "IEplbnNlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApysa0c9qc8FB8gIJ"
+            + "8zAb1pbJ4HzC7iRlVGhRJjFORkGhyvU4P5o2wL0iz/uko6rL9/pFhIlIMbwbV8sm"
+            + "mKeNUPitwiKOjoFDmtimcZ4bx5UTAYLbbHMpEdwSpMC5iF2UioM7qdiwpAfZBd6Z"
+            + "69vqNxuUJ6tP+hxtr/aSgMH2i8ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB"
+            + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE"
+            + "FLlZD3aKDa8jdhzoByOFMAJDs2osMB8GA1UdIwQYMBaAFLlZD3aKDa8jdhzoByOF"
+            + "MAJDs2osMA0GCSqGSIb3DQEBBQUAA4GBAE5vccY8Ydd7by2bbwiDKgQqVyoKrkUg"
+            + "6CD0WRmc2pBeYX2z94/PWO5L3Fx+eIZh2wTxScF+FdRWJzLbUaBuClrxuy0Y5ifj"
+            + "axuJ8LFNbZtsp1ldW3i84+F5+SYT+xI67ZcoAtwx/VFVI9s5I/Gkmu9f9nxjPpK7"
+            + "1AIUXiE3Qcck";
+
+        String incompleteCert = "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV";
+
+        String assertion = "{ serialNumber 13233831500277100508, issuer rdnSequence:\""
+                + "CN=Babs Jensen,OU=Product Development,L=Cupertione,C=US\" }";
+        String assertionWithSpaces = "{    serialNumber     13233831500277100508,  issuer  rdnSequence:\""
+                + "CN=Babs Jensen,OU=Product Development, L=Cupertione,C=US\" }";
+        String assertionDNencoded = "{ serialNumber 13233831500277100508, issuer rdnSequence:\""
+                + "cn=BABS Jensen,ou=Product Development,L=Cupertione,c=#5553\" }";
+        String assertionWrong = "{ serialNumber 13233831511277100508, issuer rdnSequence:\""
+                + "CN=Babs Jensen,OU=Product Development,L=Cupertione,C=US\" }";
+
+        return new Object[][]{
+            {ByteString.valueOfBase64(validcert1), ByteString.valueOfUtf8(assertion), ConditionResult.TRUE},
+            {ByteString.valueOfBase64(validcert1), ByteString.valueOfUtf8(assertionWithSpaces), ConditionResult.TRUE},
+            {ByteString.valueOfBase64(validcert1), ByteString.valueOfUtf8(assertionDNencoded), ConditionResult.TRUE},
+            {ByteString.valueOfBase64(validcert1), ByteString.valueOfUtf8(assertionWrong), ConditionResult.FALSE},
+            {ByteString.valueOfBase64(incompleteCert), ByteString.valueOfBase64(incompleteCert), ConditionResult.TRUE},
+            {ByteString.valueOfBase64(validcert1), ByteString.valueOfBase64(validcert1), ConditionResult.TRUE}
+        };
+    }
+
+    /**
+     * Generate valid assertion values for the certificateExactMatch matching
+     * rule test.
+     */
+    @DataProvider(name = "certificateExactMatchValidAssertionValues")
+    public Object[][] createCertificateExactMatchingRuleValidAssertionValues() {
+        return new Object[][]{
+            {"{serialNumber 123,issuer rdnSequence:\"c=DE\"}"},
+            {"{serialNumber 123,issuer rdnSequence:\"\"}"},
+            {"{serialNumber 0123,issuer rdnSequence:\"cn=issuer\"}"},
+            {"{  serialNumber  123,  issuer  rdnSequence:\"c=DE\"  }"},
+            {"{serialNumber 123,issuer rdnSequence:\"cn=escaped\"\"dquotes\"}"},
+            {"{serialNumber 123,issuer rdnSequence:\"cn=\u00D6\u00C4\"}"}
+        };
+    }
+
+    /**
+     * Generate invalid assertion values for the certificateExactMatch matching
+     * rule test.
+     */
+    @DataProvider(name = "certificateExactMatchInvalidAssertionValues")
+    public Object[][] createCertificateExactMatchingRuleInvalidAssertionValues() {
+        return new Object[][]{
+            {"{serialnumber 123,issuer rdnSequence:\"c=DE\"}"},
+            {"{serialNumber 123,issuer rdnSequence:\"invalid\"}"},
+            {"{serialNumber 0123,issuer rdnSequence: \"cn=issuer\"}"},
+            {"{  serialNumber  123  ,  issuer  rdnSequence:\"c=DE\"  }  trailing"}
+        };
+    }
+
+    /**
+     * Generate invalid atribute values for the certificateExactMatch matching
+     * rule test.
+     */
+    @DataProvider(name = "certificateExactMatchInvalidAttributeValues")
+    public Object[][] createCertificateExactMatchingRuleInvalidAttributeValues()
+            throws Exception {
+        String invalidcert1
+            = "MIICpTCCAg6gAwIBBQIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
+            + "BAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRpb25lMRwwGgYDVQQLExNQcm9kdWN0IERl"
+            + "dmVsb3BtZW50MRQwEgYDVQQDEwtCYWJzIEplbnNlbjAeFw0xMjA1MDIxNjM0MzVa"
+            + "Fw0xMjEyMjExNjM0MzVaMFYxCzAJBgNVBAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRp"
+            + "b25lMRwwGgYDVQQLExNQcm9kdWN0IERldmVsb3BtZW50MRQwEgYDVQQDEwtCYWJz"
+            + "IEplbnNlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApysa0c9qc8FB8gIJ"
+            + "8zAb1pbJ4HzC7iRlVGhRJjFORkGhyvU4P5o2wL0iz/uko6rL9/pFhIlIMbwbV8sm"
+            + "mKeNUPitwiKOjoFDmtimcZ4bx5UTAYLbbHMpEdwSpMC5iF2UioM7qdiwpAfZBd6Z"
+            + "69vqNxuUJ6tP+hxtr/aSgMH2i8ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB"
+            + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE"
+            + "FLlZD3aKDa8jdhzoByOFMAJDs2osMB8GA1UdIwQYMBaAFLlZD3aKDa8jdhzoByOF"
+            + "MAJDs2osMA0GCSqGSIb3DQEBBQUAA4GBAE5vccY8Ydd7by2bbwiDKgQqVyoKrkUg"
+            + "6CD0WRmc2pBeYX2z94/PWO5L3Fx+eIZh2wTxScF+FdRWJzLbUaBuClrxuy0Y5ifj"
+            + "axuJ8LFNbZtsp1ldW3i84+F5+SYT+xI67ZcoAtwx/VFVI9s5I/Gkmu9f9nxjPpK7"
+            + "1AIUXiE3Qcck";
+
+        String brokencert1
+            = "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV";
+
+        return new Object[][]{
+            {ByteString.valueOfBase64(invalidcert1)},
+            {ByteString.valueOfBase64(brokencert1)}
+        };
+    }
+
+    /**
+     * Get an instance of the matching rule.
+     *
+     * @return An instance of the matching rule to test.
+     */
+    protected MatchingRule getRule() {
+        return CoreSchema.getCertificateExactMatchingRule();
+    }
+
+    /**
+     * Test the normalization and the comparison of valid values.
+     */
+    @Test(dataProvider = "certificateExactMatchingRules")
+    public void certificateExactMatchingRules(ByteString attributeValue,
+            ByteString assertionValue, ConditionResult result) throws DecodeException {
+        MatchingRule rule = getRule();
+
+        // normalize the 2 provided values and check that they are equal
+        assertEquals(rule.getAssertion(assertionValue).matches(rule.normalizeAttributeValue(attributeValue)), result);
+    }
+
+    /**
+     * Test that valid assertion values are accepted.
+     */
+    @Test(dataProvider = "certificateExactMatchValidAssertionValues")
+    public void certificateExactMatchingRuleValidAssertionValues(String value)
+            throws DecodeException {
+        // Get the instance of the rule to be tested.
+        MatchingRule rule = getRule();
+
+        // normalize the provided assertion values
+        rule.getAssertion(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Test that invalid assertion values are rejected.
+     */
+    @Test(dataProvider = "certificateExactMatchInvalidAssertionValues",
+            expectedExceptions = { DecodeException.class })
+    public void certificateExactMatchingRuleInvalidAssertionValues(String value)
+            throws DecodeException {
+        // Get the instance of the rule to be tested.
+        MatchingRule rule = getRule();
+
+        // normalize the provided assertion value
+        rule.getAssertion(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Test that invalid attribute values are returned with the original
+     * ByteString.
+     */
+    @Test(dataProvider = "certificateExactMatchInvalidAttributeValues")
+    public void certificateExactMatchingRuleInvalidAttributeValues(ByteString value)
+            throws DecodeException {
+        // Get the instance of the rule to be tested.
+        MatchingRule rule = getRule();
+
+        // normalize the provided assertion value
+        Assertion normalizedAssertionValue = rule.getAssertion(value);
+        assertEquals(normalizedAssertionValue.matches(value), ConditionResult.TRUE);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxTest.java
new file mode 100644
index 0000000..e2f1be8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CertificateSyntaxTest.java
@@ -0,0 +1,143 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteString;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_CERTIFICATE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.testng.Assert.*;
+
+/**
+ * Certificate syntax tests.
+ */
+public class CertificateSyntaxTest extends AbstractSchemaTestCase {
+
+    /**
+     * Create data for the testAcceptableValues test. This should be a table of
+     * tables with 2 elements. The first one should be the value to test, the
+     * second the expected result of the test.
+     *
+     * @return a table containing data for the testAcceptableValues Test.
+     */
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        String validcert1
+            = "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
+            + "BAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRpb25lMRwwGgYDVQQLExNQcm9kdWN0IERl"
+            + "dmVsb3BtZW50MRQwEgYDVQQDEwtCYWJzIEplbnNlbjAeFw0xMjA1MDIxNjM0MzVa"
+            + "Fw0xMjEyMjExNjM0MzVaMFYxCzAJBgNVBAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRp"
+            + "b25lMRwwGgYDVQQLExNQcm9kdWN0IERldmVsb3BtZW50MRQwEgYDVQQDEwtCYWJz"
+            + "IEplbnNlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApysa0c9qc8FB8gIJ"
+            + "8zAb1pbJ4HzC7iRlVGhRJjFORkGhyvU4P5o2wL0iz/uko6rL9/pFhIlIMbwbV8sm"
+            + "mKeNUPitwiKOjoFDmtimcZ4bx5UTAYLbbHMpEdwSpMC5iF2UioM7qdiwpAfZBd6Z"
+            + "69vqNxuUJ6tP+hxtr/aSgMH2i8ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB"
+            + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE"
+            + "FLlZD3aKDa8jdhzoByOFMAJDs2osMB8GA1UdIwQYMBaAFLlZD3aKDa8jdhzoByOF"
+            + "MAJDs2osMA0GCSqGSIb3DQEBBQUAA4GBAE5vccY8Ydd7by2bbwiDKgQqVyoKrkUg"
+            + "6CD0WRmc2pBeYX2z94/PWO5L3Fx+eIZh2wTxScF+FdRWJzLbUaBuClrxuy0Y5ifj"
+            + "axuJ8LFNbZtsp1ldW3i84+F5+SYT+xI67ZcoAtwx/VFVI9s5I/Gkmu9f9nxjPpK7"
+            + "1AIUXiE3Qcck";
+
+        String invalidcert1
+            = "MIICpTCCAg6gAwIBBQIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
+            + "BAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRpb25lMRwwGgYDVQQLExNQcm9kdWN0IERl"
+            + "dmVsb3BtZW50MRQwEgYDVQQDEwtCYWJzIEplbnNlbjAeFw0xMjA1MDIxNjM0MzVa"
+            + "Fw0xMjEyMjExNjM0MzVaMFYxCzAJBgNVBAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRp"
+            + "b25lMRwwGgYDVQQLExNQcm9kdWN0IERldmVsb3BtZW50MRQwEgYDVQQDEwtCYWJz"
+            + "IEplbnNlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApysa0c9qc8FB8gIJ"
+            + "8zAb1pbJ4HzC7iRlVGhRJjFORkGhyvU4P5o2wL0iz/uko6rL9/pFhIlIMbwbV8sm"
+            + "mKeNUPitwiKOjoFDmtimcZ4bx5UTAYLbbHMpEdwSpMC5iF2UioM7qdiwpAfZBd6Z"
+            + "69vqNxuUJ6tP+hxtr/aSgMH2i8ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB"
+            + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE"
+            + "FLlZD3aKDa8jdhzoByOFMAJDs2osMB8GA1UdIwQYMBaAFLlZD3aKDa8jdhzoByOF"
+            + "MAJDs2osMA0GCSqGSIb3DQEBBQUAA4GBAE5vccY8Ydd7by2bbwiDKgQqVyoKrkUg"
+            + "6CD0WRmc2pBeYX2z94/PWO5L3Fx+eIZh2wTxScF+FdRWJzLbUaBuClrxuy0Y5ifj"
+            + "axuJ8LFNbZtsp1ldW3i84+F5+SYT+xI67ZcoAtwx/VFVI9s5I/Gkmu9f9nxjPpK7"
+            + "1AIUXiE3Qcck";
+
+        String brokencert1
+            = "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV";
+
+        return new Object[][]{
+            {ByteString.valueOfBase64(validcert1), true},
+            {ByteString.valueOfUtf8(validcert1), false},
+            {ByteString.valueOfBase64(invalidcert1), false},
+            {ByteString.valueOfBase64(brokencert1), false},
+            {ByteString.valueOfUtf8("invalid"), false}
+        };
+    }
+
+    /**
+     * Test acceptable values for this syntax.
+     *
+     * @param value
+     *            The ByteString containing the value that will be tested
+     * @param result
+     *            The expected result of the test
+     */
+    @Test(dataProvider = "acceptableValues")
+    public void testAcceptableValues(ByteString value, boolean result) {
+        // Make sure that the specified class can be instantiated as a task.
+        final Syntax syntax = getRule();
+
+        final LocalizableMessageBuilder reason = new LocalizableMessageBuilder();
+
+        // test the valueIsAcceptable method
+        final boolean liveResult = syntax.valueIsAcceptable(value, reason);
+        assertEquals(liveResult, result,
+                syntax + ".valueIsAcceptable gave bad result for " + value + "reason : " + reason);
+
+    }
+
+    /**
+     * Test acceptable values for this syntax allowing malformed certificates.
+     *
+     * @param value
+     *            The ByteString containing the value that will be tested
+     * @param result
+     *            Expected result is ignored.
+     */
+    @Test(dataProvider = "acceptableValues")
+    public void testAllowMalformedCertificates(ByteString value, Boolean result) {
+        SchemaBuilder builder = new SchemaBuilder(getCoreSchema()).setOption(ALLOW_MALFORMED_CERTIFICATES, true);
+        final Syntax syntax = builder.toSchema().getSyntax(SYNTAX_CERTIFICATE_OID);
+
+        final LocalizableMessageBuilder reason = new LocalizableMessageBuilder();
+
+        // test the valueIsAcceptable method
+        final boolean liveResult = syntax.valueIsAcceptable(value, reason);
+        assertTrue(liveResult, syntax + ".valueIsAcceptable gave bad result for " + value + "reason : " + reason);
+    }
+
+
+    /**
+     * Get an instance of the attribute syntax that must be tested.
+     *
+     * @return An instance of the attribute syntax that must be tested.
+     */
+    protected Syntax getRule() {
+        SchemaBuilder builder = new SchemaBuilder(getCoreSchema()).setOption(ALLOW_MALFORMED_CERTIFICATES, false);
+
+        return builder.toSchema().getSyntax(SYNTAX_CERTIFICATE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..5fec536
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationEqualityMatchingRuleTest.java
@@ -0,0 +1,106 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "passe", "passe", ConditionResult.TRUE },
+            { "passe ", "passe", ConditionResult.TRUE },
+            { "passE", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passE", ConditionResult.TRUE },
+            { "pass\u00E9", "pass\u00C9", ConditionResult.TRUE },
+            { "passe", "pass\u00E9", ConditionResult.TRUE },
+            { "passE", "pass\u00E9", ConditionResult.TRUE },
+            { "pass\u00C9", "pass\u00E9", ConditionResult.TRUE },
+            { "abc", "abd", ConditionResult.FALSE },
+            { "abc", "acc", ConditionResult.FALSE },
+            { "abc", "bbc", ConditionResult.FALSE },
+            { "abc", "abD", ConditionResult.FALSE },
+            { "def", "d\u00E9g", ConditionResult.FALSE },
+            { "def", "dEg", ConditionResult.FALSE },
+            { "dEf", "deg", ConditionResult.FALSE },
+            { "d\u00E9f", "dEg", ConditionResult.FALSE },
+            { "abd", "abc", ConditionResult.FALSE },
+            { "acc", "abc", ConditionResult.FALSE },
+            { "bbc", "abc", ConditionResult.FALSE },
+            { "abD", "abc", ConditionResult.FALSE },
+            { "d\u00E9g", "def", ConditionResult.FALSE },
+            { "dEg", "def", ConditionResult.FALSE },
+            { "deg", "dEf", ConditionResult.FALSE },
+            { "dEg", "d\u00E9f", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.eq");
+    }
+
+    @Test
+    public void testMatchingRuleNames() throws Exception {
+        // 'fr' and 'fr-FR' share the same oid (they are aliases)
+        MatchingRule rule = Schema.getCoreSchema().getMatchingRule("fr.eq");
+        assertThat(rule.getNames()).containsOnly("fr.eq", "fr.3", "fr-FR.eq", "fr-FR.3");
+
+        MatchingRule rule2 = Schema.getCoreSchema().getMatchingRule("fr-FR.eq");
+        assertThat(rule2.getNames()).containsOnly("fr.eq", "fr.3", "fr-FR.eq", "fr-FR.3");
+
+        // 'ar' does not share oid with another locale
+        MatchingRule rule3 = Schema.getCoreSchema().getMatchingRule("ar.3");
+        assertThat(rule3.getNames()).containsOnly("ar.eq", "ar.3");
+
+        // equality matching rule is also the default matching rule
+        MatchingRule rule4 = Schema.getCoreSchema().getMatchingRule("ar");
+        assertThat(rule4.getNames()).containsOnly("ar");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("abc");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+
+        ByteString normalizedValue = matchingRule.normalizeAttributeValue(value);
+        assertEquals(indexQuery, "exactMatch(fr.shared, value=='" + normalizedValue.toHexString() + "')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanMatchingRuleTest.java
new file mode 100644
index 0000000..0fec596
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanMatchingRuleTest.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationGreaterThanMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "abc", "abd", ConditionResult.FALSE },
+            { "abc", "acc", ConditionResult.FALSE },
+            { "abc", "bbc", ConditionResult.FALSE },
+            { "abc", "abD", ConditionResult.FALSE },
+            { "def", "d\u00E9g", ConditionResult.FALSE },
+            { "def", "dEg", ConditionResult.FALSE },
+            { "dEf", "deg", ConditionResult.FALSE },
+            { "d\u00E9f", "dEg", ConditionResult.FALSE },
+            { "passe", "passe", ConditionResult.FALSE },
+            { "passe ", "passe", ConditionResult.FALSE },
+            { "passE", "passe", ConditionResult.FALSE },
+            { "pass\u00E9", "passe", ConditionResult.FALSE },
+            { "pass\u00E9", "passE", ConditionResult.FALSE },
+            { "pass\u00E9", "pass\u00C9", ConditionResult.FALSE },
+            { "passe", "pass\u00E9", ConditionResult.FALSE },
+            { "passE", "pass\u00E9", ConditionResult.FALSE },
+            { "pass\u00C9", "pass\u00E9", ConditionResult.FALSE },
+            { "abd", "abc", ConditionResult.TRUE },
+            { "acc", "abc", ConditionResult.TRUE },
+            { "bbc", "abc", ConditionResult.TRUE },
+            { "abD", "abc", ConditionResult.TRUE },
+            { "d\u00E9g", "def", ConditionResult.TRUE },
+            { "dEg", "def", ConditionResult.TRUE },
+            { "deg", "dEf", ConditionResult.TRUE },
+            { "dEg", "d\u00E9f", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.gt");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("abc");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+
+        ByteString normalizedValue = matchingRule.normalizeAttributeValue(value);
+        assertEquals(indexQuery, "rangeMatch(fr.shared, '" + normalizedValue.toHexString() + "' < value < '')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanOrEqualMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanOrEqualMatchingRuleTest.java
new file mode 100644
index 0000000..53b8d2d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationGreaterThanOrEqualMatchingRuleTest.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationGreaterThanOrEqualMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "abc", "abd", ConditionResult.FALSE },
+            { "abc", "acc", ConditionResult.FALSE },
+            { "abc", "bbc", ConditionResult.FALSE },
+            { "abc", "abD", ConditionResult.FALSE },
+            { "def", "d\u00E9g", ConditionResult.FALSE },
+            { "def", "dEg", ConditionResult.FALSE },
+            { "dEf", "deg", ConditionResult.FALSE },
+            { "d\u00E9f", "dEg", ConditionResult.FALSE },
+            { "passe", "passe", ConditionResult.TRUE },
+            { "passe ", "passe", ConditionResult.TRUE },
+            { "passE", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passE", ConditionResult.TRUE },
+            { "pass\u00E9", "pass\u00C9", ConditionResult.TRUE },
+            { "passe", "pass\u00E9", ConditionResult.TRUE },
+            { "passE", "pass\u00E9", ConditionResult.TRUE },
+            { "pass\u00C9", "pass\u00E9", ConditionResult.TRUE },
+            { "abd", "abc", ConditionResult.TRUE },
+            { "acc", "abc", ConditionResult.TRUE },
+            { "bbc", "abc", ConditionResult.TRUE },
+            { "abD", "abc", ConditionResult.TRUE },
+            { "d\u00E9g", "def", ConditionResult.TRUE },
+            { "dEg", "def", ConditionResult.TRUE },
+            { "deg", "dEf", ConditionResult.TRUE },
+            { "dEg", "d\u00E9f", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.gte");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("abc");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+
+        ByteString normalizedValue = matchingRule.normalizeAttributeValue(value);
+        assertEquals(indexQuery, "rangeMatch(fr.shared, '" + normalizedValue.toHexString() + "' <= value < '')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanMatchingRuleTest.java
new file mode 100644
index 0000000..3cc18f6
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanMatchingRuleTest.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationLessThanMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "abc", "abd", ConditionResult.TRUE },
+            { "abc", "acc", ConditionResult.TRUE },
+            { "abc", "bbc", ConditionResult.TRUE },
+            { "abc", "abD", ConditionResult.TRUE },
+            { "def", "d\u00E9g", ConditionResult.TRUE },
+            { "def", "dEg", ConditionResult.TRUE },
+            { "dEf", "deg", ConditionResult.TRUE },
+            { "d\u00E9f", "dEg", ConditionResult.TRUE },
+            { "passe", "passe", ConditionResult.FALSE },
+            { "passe ", "passe", ConditionResult.FALSE },
+            { "passE", "passe", ConditionResult.FALSE },
+            { "pass\u00E9", "passe", ConditionResult.FALSE },
+            { "pass\u00E9", "passE", ConditionResult.FALSE },
+            { "pass\u00E9", "pass\u00C9", ConditionResult.FALSE },
+            { "passe", "pass\u00E9", ConditionResult.FALSE },
+            { "passE", "pass\u00E9", ConditionResult.FALSE },
+            { "pass\u00C9", "pass\u00E9", ConditionResult.FALSE },
+            { "abd", "abc", ConditionResult.FALSE },
+            { "acc", "abc", ConditionResult.FALSE },
+            { "bbc", "abc", ConditionResult.FALSE },
+            { "abD", "abc", ConditionResult.FALSE },
+            { "d\u00E9g", "def", ConditionResult.FALSE },
+            { "dEg", "def", ConditionResult.FALSE },
+            { "deg", "dEf", ConditionResult.FALSE },
+            { "dEg", "d\u00E9f", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.lt");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("abc");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+
+        ByteString normalizedValue = matchingRule.normalizeAttributeValue(value);
+        assertEquals(indexQuery, "rangeMatch(fr.shared, '' < value < '" + normalizedValue.toHexString() + "')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanOrEqualMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanOrEqualMatchingRuleTest.java
new file mode 100644
index 0000000..ed8bcae
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationLessThanOrEqualMatchingRuleTest.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationLessThanOrEqualMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "abc", "abd", ConditionResult.TRUE },
+            { "abc", "acc", ConditionResult.TRUE },
+            { "abc", "bbc", ConditionResult.TRUE },
+            { "abc", "abD", ConditionResult.TRUE },
+            { "def", "d\u00E9g", ConditionResult.TRUE },
+            { "def", "dEg", ConditionResult.TRUE },
+            { "dEf", "deg", ConditionResult.TRUE },
+            { "d\u00E9f", "dEg", ConditionResult.TRUE },
+            { "passe", "passe", ConditionResult.TRUE },
+            { "passe ", "passe", ConditionResult.TRUE },
+            { "passE", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passe", ConditionResult.TRUE },
+            { "pass\u00E9", "passE", ConditionResult.TRUE },
+            { "pass\u00E9", "pass\u00C9", ConditionResult.TRUE },
+            { "passe", "pass\u00E9", ConditionResult.TRUE },
+            { "passE", "pass\u00E9", ConditionResult.TRUE },
+            { "pass\u00C9", "pass\u00E9", ConditionResult.TRUE },
+            { "abd", "abc", ConditionResult.FALSE },
+            { "acc", "abc", ConditionResult.FALSE },
+            { "bbc", "abc", ConditionResult.FALSE },
+            { "abD", "abc", ConditionResult.FALSE },
+            { "d\u00E9g", "def", ConditionResult.FALSE },
+            { "dEg", "def", ConditionResult.FALSE },
+            { "deg", "dEf", ConditionResult.FALSE },
+            { "dEg", "d\u00E9f", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.lte");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("abc");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+
+        ByteString normalizedValue = matchingRule.normalizeAttributeValue(value);
+        assertEquals(indexQuery, "rangeMatch(fr.shared, '' < value <= '" + normalizedValue.toHexString() + "')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationSubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationSubstringMatchingRuleTest.java
new file mode 100644
index 0000000..c140bc8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CollationSubstringMatchingRuleTest.java
@@ -0,0 +1,156 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class CollationSubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { };
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            { "this is a value", "value", ConditionResult.TRUE },
+            { "this is a value", "alue", ConditionResult.TRUE },
+            { "this is a value", "ue", ConditionResult.TRUE },
+            { "this is a value", "e", ConditionResult.TRUE },
+            { "this is a value", "valu", ConditionResult.FALSE },
+            { "this is a value", "this", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.TRUE },
+            { "this is a value", "AlUe", ConditionResult.TRUE },
+            { "this is a value", "UE", ConditionResult.TRUE },
+            { "this is a value", "E", ConditionResult.TRUE },
+            { "this is a value", "THIS", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.TRUE },
+            { "this is a VALUE", "value", ConditionResult.TRUE },
+            { "end with space    ", " ", ConditionResult.TRUE },
+            { "end with space    ", "space", ConditionResult.TRUE },
+            { "end with space    ", "SPACE", ConditionResult.TRUE },
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "il est passe", "passE", ConditionResult.TRUE },
+            { "il est passe", "pass\u00E9", ConditionResult.TRUE },
+            { "il est passe", "pass\u00C9", ConditionResult.TRUE },
+            { "il est pass\u00C9", "passe", ConditionResult.TRUE },
+            { "il est pass\u00E9", "passE", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "this is a value", "this", ConditionResult.TRUE },
+            { "this is a value", "th", ConditionResult.TRUE },
+            { "this is a value", "t", ConditionResult.TRUE },
+            { "this is a value", "is", ConditionResult.FALSE },
+            { "this is a value", "a", ConditionResult.FALSE },
+            { "this is a value", "TH", ConditionResult.TRUE },
+            { "this is a value", "T", ConditionResult.TRUE },
+            { "this is a value", "IS", ConditionResult.FALSE },
+            { "this is a value", "A", ConditionResult.FALSE },
+            { "this is a value", "VALUE", ConditionResult.FALSE },
+            { "this is a value", " ", ConditionResult.TRUE },
+            { "this is a value", "NOT", ConditionResult.FALSE },
+            { "this is a value", "THIS", ConditionResult.TRUE },
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "il etait passe", "Il \u00E9", ConditionResult.TRUE },
+            { "il etait passe", "Il \u00C9", ConditionResult.TRUE },
+            { "il etait passe", "Il E", ConditionResult.TRUE },
+            { "il \u00E9tait passe", "IL e", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            { "this is a value", strings("this"), ConditionResult.TRUE },
+            { "this is a value", strings("is"), ConditionResult.TRUE },
+            { "this is a value", strings("a"), ConditionResult.TRUE },
+            { "this is a value", strings("value"), ConditionResult.TRUE },
+            { "this is a value", strings("THIS"), ConditionResult.TRUE },
+            { "this is a value", strings("IS"), ConditionResult.TRUE },
+            { "this is a value", strings("A"), ConditionResult.TRUE },
+            { "this is a value", strings("VALUE"), ConditionResult.TRUE },
+            { "this is a value", strings(" "), ConditionResult.TRUE },
+            { "this is a value", strings("this", "is", "a", "value"), ConditionResult.TRUE },
+            // The matching rule requires ordered non overlapping substrings.
+            // Issue #730 was not valid.
+            { "this is a value", strings("value", "this"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "this is"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "IS", "a", "VALue"), ConditionResult.TRUE },
+            { "this is a value", strings("his IS", "A val"), ConditionResult.TRUE },
+            { "this is a value", strings("not"), ConditionResult.FALSE },
+            { "this is a value", strings("this", "not"), ConditionResult.FALSE },
+            { "this is a value", strings("    "), ConditionResult.TRUE },
+            // \u00E9 = LATIN SMALL LETTER E WITH ACUTE
+            // \u00C9 = LATIN CAPITAL LETTER E WITH ACUTE
+            { "il est passe par la", strings("est", "pass\u00E9"), ConditionResult.TRUE },
+            { "il est passe par la", strings("pass\u00E9", "Par"), ConditionResult.TRUE },
+            { "il est passe par la", strings("est", "pass\u00C9", "PAR", "La"), ConditionResult.TRUE },
+            { "il est pass\u00E9 par la", strings("il", "Est", "pass\u00C9"), ConditionResult.TRUE },
+            { "il est pass\u00C9 par la", strings("est", "passe"), ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule("fr.sub");
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        ByteString value = ByteString.valueOfUtf8("a*c");
+        MatchingRule matchingRule = getRule();
+        Assertion assertion = matchingRule.getAssertion(value);
+
+        int subStringLength = 3;
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(subStringLength),
+            false));
+
+        ByteString binit = matchingRule.normalizeAttributeValue(ByteString.valueOfUtf8("a"));
+        ByteString bfinal = matchingRule.normalizeAttributeValue(ByteString.valueOfUtf8("c"));
+        assertEquals(indexQuery,
+            "intersect["
+            + "rangeMatch(fr.shared, '" + binit.toHexString() + "' <= value < '0054'), "
+            + "rangeMatch(fr.substring:" + subStringLength + ", '" + bfinal.toHexString() + "' <= value < '0056'), "
+            + "rangeMatch(fr.substring:" + subStringLength + ", '" + binit.toHexString() + "' <= value < '0054')]"
+        );
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CoreSchemaTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CoreSchemaTest.java
new file mode 100644
index 0000000..9527a75
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CoreSchemaTest.java
@@ -0,0 +1,31 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Core schema tests.
+ */
+@SuppressWarnings("javadoc")
+public class CoreSchemaTest extends AbstractSchemaTestCase {
+    @Test
+    public final void testCoreSchemaWarnings() {
+        // Make sure core schema doesn't have any warnings.
+        Assert.assertTrue(Schema.getCoreSchema().getWarnings().isEmpty());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxTest.java
new file mode 100644
index 0000000..ddc814d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/CountryStringSyntaxTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2012 Manuel Gaupp
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_COUNTRY_STRING_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Country String syntax tests. */
+@Test
+public class CountryStringSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            // tests for the Country String syntax.
+            { "DE", true },
+            { "de", false },
+            { "SX", true },
+            { "12", false },
+            { "UK", true },
+            { "Xf", false },
+            { "ÖÄ", false }, // "\u00D6\u00C4"
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_COUNTRY_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxTest.java
new file mode 100644
index 0000000..8c5de64
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleSyntaxTest.java
@@ -0,0 +1,65 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_DIT_CONTENT_RULE_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** DIT content rule syntax tests. */
+public class DITContentRuleSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            {
+                "( 2.5.6.4 DESC 'content rule for organization' NOT "
+                        + "( x121Address $ telexNumber ) )", true },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'rule with all possible fields' " + " OBSOLETE"
+                        + " AUX ( posixAccount )" + " MUST ( cn $ sn )" + " MAY ( dc )"
+                        + " NOT ( x121Address $ telexNumber ) )", true },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'ommit parenthesis' " + " OBSOLETE"
+                        + " AUX posixAccount " + " MUST cn " + " MAY dc " + " NOT x121Address )",
+                true },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'use numeric OIDs' " + " OBSOLETE"
+                        + " AUX 1.3.6.1.1.1.2.0" + " MUST cn " + " MAY dc " + " NOT x121Address )",
+                true },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'illegal OIDs' " + " OBSOLETE" + " AUX 2.5.6.."
+                        + " MUST cn " + " MAY dc " + " NOT x121Address )", false },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'illegal OIDs' " + " OBSOLETE" + " AUX 2.5.6.x"
+                        + " MUST cn " + " MAY dc " + " NOT x121Address )", false },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'missing closing parenthesis' " + " OBSOLETE"
+                        + " AUX posixAccount" + " MUST cn " + " MAY dc " + " NOT x121Address",
+                false },
+            {
+                "( 2.5.6.4 NAME 'fullRule' DESC 'extra parameterss' " + " MUST cn "
+                        + " X-name ( 'this is an extra parameter' ) )", true },
+
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_DIT_CONTENT_RULE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleTestCase.java
new file mode 100644
index 0000000..735e375
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITContentRuleTestCase.java
@@ -0,0 +1,161 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+
+import java.util.Collections;
+
+import org.testng.annotations.Test;
+
+public class DITContentRuleTestCase extends AbstractSchemaTestCase {
+
+    /** Adds a DIT content rule on "device" structural object class. */
+    @Test
+    public void testValidDITContentRule() throws Exception {
+        final Schema schema = new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule("2.5.6.14")
+                .names(singletonList("devicecontentrule"))
+                .description("Content rule desc")
+                .auxiliaryObjectClasses(Collections.<String> emptyList())
+                .optionalAttributes(Collections.<String> emptyList())
+                .prohibitedAttributes(singletonList("serialNumber"))
+                .requiredAttributes(singletonList("owner"))
+                .addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final DITContentRule cr = schema.getDITContentRule("devicecontentrule");
+        assertThat(cr).isNotNull();
+        assertThat(cr.getStructuralClassOID()).isEqualTo("2.5.6.14");
+        assertThat(cr.getNames()).containsOnly("devicecontentrule");
+        assertThat(cr.getAuxiliaryClasses()).hasSize(0);
+        assertThat(cr.getOptionalAttributes()).hasSize(0);
+        assertThat(cr.getProhibitedAttributes()).hasSize(1);
+        assertThat(cr.getRequiredAttributes()).hasSize(1);
+        assertThat(cr.getDescription()).isEqualTo("Content rule desc");
+        assertThat(cr.isObsolete()).isFalse();
+    }
+
+    /** Adds a DIT content rule on "organization" object class and then uses it to create another one. **/
+    @Test
+    public void testCopyConstructor() throws Exception {
+        final Schema schema = new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule("2.5.6.4")
+                .names(singletonList("organizationcontentrule"))
+                .description("Content rule desc")
+                .auxiliaryObjectClasses(singletonList("2.5.6.22"))
+                .optionalAttributes(singletonList("searchGuide"))
+                .prohibitedAttributes(singletonList("postOfficeBox"))
+                .requiredAttributes(singletonList("telephoneNumber"))
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final Schema schemaCopy = new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule(schema.getDITContentRule("organizationcontentrule"))
+                .names("organizationcontentrule-copy")
+                .addToSchema()
+                .toSchema();
+        assertThat(schemaCopy.getWarnings()).isEmpty();
+
+        final DITContentRule crCopy = schemaCopy.getDITContentRule("organizationcontentrule-copy");
+        assertThat(crCopy).isNotNull();
+        assertThat(crCopy.getStructuralClassOID()).isEqualTo("2.5.6.4");
+        assertThat(crCopy.getNames()).containsOnly("organizationcontentrule", "organizationcontentrule-copy");
+        assertThat(crCopy.getAuxiliaryClasses()).hasSize(1);
+        assertThat(crCopy.getOptionalAttributes()).hasSize(1);
+        assertThat(crCopy.getProhibitedAttributes()).hasSize(1);
+        assertThat(crCopy.getRequiredAttributes()).hasSize(1);
+        assertThat(crCopy.getDescription()).isEqualTo("Content rule desc");
+        assertThat(crCopy.isObsolete()).isFalse();
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testBuilderDoesNotAllowOverwrite() throws Exception {
+        final SchemaBuilder schemaBuilder = new SchemaBuilder(getCoreSchema())
+                                           .buildDITContentRule("2.5.6.9")
+                                           .addToSchema();
+        schemaBuilder.buildDITContentRule("2.5.6.9")
+                    .addToSchema();
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testBuilderDoesNotAllowNullStructuralOCOID() throws Exception {
+        new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule((String) null)
+                .addToSchema();
+    }
+
+    @Test
+    public void testBuilderRemoveAll() throws Exception {
+        DITContentRule.Builder crBuilder = new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule("2.5.6.1")
+                .names(singletonList("shouldBeRemoved"))
+                .description("My content rule")
+                .auxiliaryObjectClasses(singletonList("shouldBeRemoved"))
+                .optionalAttributes(singletonList("shouldBeRemoved"))
+                .prohibitedAttributes(singletonList("shouldBeRemoved"))
+                .requiredAttributes(singletonList("shouldBeRemoved"));
+
+        Schema schema = crBuilder.removeAllNames()
+                .removeAllAuxiliaryObjectClasses()
+                .removeAllOptionalAttributes()
+                .removeAllProhibitedAttributes()
+                .removeAllRequiredAttributes()
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        DITContentRule cr = schema.getDITContentRule(schema.getObjectClass("2.5.6.1"));
+        assertThat(cr.getNames()).isEmpty();
+        assertThat(cr.getAuxiliaryClasses()).isEmpty();
+        assertThat(cr.getOptionalAttributes()).isEmpty();
+        assertThat(cr.getProhibitedAttributes()).isEmpty();
+        assertThat(cr.getRequiredAttributes()).isEmpty();
+    }
+
+    @Test
+    public void testBuilderRemove() throws Exception {
+        DITContentRule.Builder crBuilder = new SchemaBuilder(getCoreSchema())
+                .buildDITContentRule("2.5.6.1")
+                .names(singletonList("shouldBeRemoved"))
+                .description("My content rule")
+                .auxiliaryObjectClasses(singletonList("shouldBeRemoved"))
+                .optionalAttributes(singletonList("shouldBeRemoved"))
+                .prohibitedAttributes(singletonList("shouldBeRemoved"))
+                .requiredAttributes(singletonList("shouldBeRemoved"));
+
+        Schema schema = crBuilder.removeName("shouldBeRemoved")
+                .removeAuxiliaryObjectClass("shouldBeRemoved")
+                .removeOptionalAttribute("shouldBeRemoved")
+                .removeProhibitedAttribute("shouldBeRemoved")
+                .removeRequiredAttribute("shouldBeRemoved")
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        DITContentRule cr = schema.getDITContentRule(schema.getObjectClass("2.5.6.1"));
+        assertThat(cr.getNames()).isEmpty();
+        assertThat(cr.getAuxiliaryClasses()).isEmpty();
+        assertThat(cr.getOptionalAttributes()).isEmpty();
+        assertThat(cr.getProhibitedAttributes()).isEmpty();
+        assertThat(cr.getRequiredAttributes()).isEmpty();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITStructureRuleTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITStructureRuleTestCase.java
new file mode 100644
index 0000000..933f3d2
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DITStructureRuleTestCase.java
@@ -0,0 +1,171 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.MapAssert.*;
+
+import org.testng.annotations.Test;
+
+public class DITStructureRuleTestCase extends AbstractSchemaTestCase {
+
+    private static final String NAME_FORM_TEST_OID = "1.2.3.4";
+
+    /** Adds a valid DIT structure rule on the "" name form. */
+    @Test
+    public void testValidDITStructureRule() {
+        final SchemaBuilder builder = getTestSchema()
+               .buildDITStructureRule(42)
+               .names("DIT structure rule test", "DIT structure rule for person")
+               .nameForm(NAME_FORM_TEST_OID)
+               .description("My DIT structure rule")
+               .extraProperties("property name", "property value")
+               .addToSchema();
+
+        final Schema schema = builder.buildDITStructureRule(43)
+               .names("DIT structure rule child test")
+               .nameForm(NAME_FORM_TEST_OID)
+               .superiorRules(42)
+               .description("My DIT structure rule child")
+               .extraProperties("property name", "property value")
+               .addToSchema()
+               .toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final DITStructureRule sr = schema.getDITStructureRule(42);
+        assertThat(sr).isNotNull();
+        assertThat(sr.getRuleID()).isEqualTo(42);
+        assertThat(sr.getNames()).containsOnly("DIT structure rule test", "DIT structure rule for person");
+        assertThat(sr.getDescription()).isEqualTo("My DIT structure rule");
+        assertThat(sr.getNameForm().getOID()).isEqualTo(NAME_FORM_TEST_OID);
+        assertThat(sr.getExtraProperties()).includes(entry("property name", singletonList("property value")));
+        assertThat(sr.getSuperiorRules()).isEmpty();
+        assertThat(sr.isObsolete()).isFalse();
+
+        final DITStructureRule srChild = schema.getDITStructureRule(43);
+        assertThat(srChild).isNotNull();
+        assertThat(srChild.getRuleID()).isEqualTo(43);
+        assertThat(srChild.getNames()).containsOnly("DIT structure rule child test");
+        assertThat(srChild.getDescription()).isEqualTo("My DIT structure rule child");
+        assertThat(srChild.getNameForm().getOID()).isEqualTo(NAME_FORM_TEST_OID);
+        assertThat(srChild.getExtraProperties()).includes(entry("property name", singletonList("property value")));
+        assertThat(srChild.getSuperiorRules()).containsOnly(sr);
+        assertThat(srChild.isObsolete()).isFalse();
+    }
+
+    @Test
+    public void testCopyConstructor() throws Exception {
+        final Schema schema = getTestSchema()
+                .buildDITStructureRule(42)
+                .names("DIT structure rule test")
+                .nameForm(NAME_FORM_TEST_OID)
+                .description("My DIT structure rule")
+                .addToSchema()
+                .toSchema();
+
+        final Schema schemaCopy = getTestSchema()
+                .buildDITStructureRule(schema.getDITStructureRule(42))
+                .ruleID(43)
+                .names("DIT structure rule test - copy")
+                .addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schemaCopy.getWarnings()).isEmpty();
+
+        final DITStructureRule srCopy = schemaCopy.getDITStructureRule(43);
+        assertThat(srCopy).isNotNull();
+        assertThat(srCopy.getRuleID()).isEqualTo(43);
+        assertThat(srCopy.getNames()).containsOnly("DIT structure rule test", "DIT structure rule test - copy");
+        assertThat(srCopy.getNameForm().getOID()).isEqualTo(NAME_FORM_TEST_OID);
+        assertThat(srCopy.getDescription()).isEmpty();
+        assertThat(srCopy.isObsolete()).isFalse();
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testBuilderDoesNotAllowOverwrite() throws Exception {
+        final SchemaBuilder builder = getTestSchema()
+                .buildDITStructureRule(42)
+                .nameForm(NAME_FORM_TEST_OID)
+                .addToSchema();
+
+        builder.buildDITStructureRule(42)
+               .nameForm(NAME_FORM_TEST_OID)
+               .addToSchema()
+               .toSchema();
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testBuilderDoesNotAllowNullNameForm() throws Exception {
+        getTestSchema().buildDITStructureRule(42)
+                       .nameForm(null)
+                       .addToSchema();
+    }
+
+    @Test
+    public void testBuilderRemoveAll() throws Exception {
+        DITStructureRule.Builder srBuilder = getTestSchema()
+                .buildDITStructureRule(42)
+                .names("DIT structure rule test", "DIT structure rule for person")
+                .nameForm(NAME_FORM_TEST_OID)
+                .description("My DIT structure rule")
+                .superiorRules(1, 2, 3, 4)
+                .extraProperties("property name", "property value");
+
+        Schema schema = srBuilder.removeAllNames()
+                .removeAllSuperiorRules()
+                .removeAllExtraProperties()
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        DITStructureRule sr = schema.getDITStructureRule(42);
+        assertThat(sr.getNames()).isEmpty();
+        assertThat(sr.getExtraProperties()).isEmpty();
+        assertThat(sr.getSuperiorRules()).isEmpty();
+    }
+
+    @Test
+    public void testBuilderRemove() throws Exception {
+        DITStructureRule.Builder srBuilder = getTestSchema()
+                .buildDITStructureRule(42)
+                .names("DIT structure rule test", "should be removed")
+                .nameForm(NAME_FORM_TEST_OID)
+                .description("My DIT structure rule")
+                .superiorRules(1)
+                .extraProperties("property name", "property value");
+
+        Schema schema = srBuilder.removeName("should be removed")
+                .removeSuperiorRule(1)
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        DITStructureRule sr = schema.getDITStructureRule(42);
+        assertThat(sr.getNames()).containsOnly("DIT structure rule test");
+        assertThat(sr.getSuperiorRules()).isEmpty();
+    }
+
+    private SchemaBuilder getTestSchema() {
+        return new SchemaBuilder(Schema.getCoreSchema())
+                .buildNameForm(NAME_FORM_TEST_OID)
+                .structuralObjectClassOID("person")
+                .requiredAttributes("sn", "cn")
+                .addToSchema();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..dcf5cd8
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DirectoryStringFirstComponentEqualityMatchingRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_DIRECTORY_STRING_FIRST_COMPONENT_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the DirectoryStringFirstComponentEqualityMatchingRule. */
+@Test
+public class DirectoryStringFirstComponentEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {};
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "('full type' 'other value')", "other value", ConditionResult.FALSE },
+            { "('full type' 'other value')", "something", ConditionResult.FALSE },
+            { "('full type' 'other value')", "full type", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_DIRECTORY_STRING_FIRST_COMPONENT_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..5a5a79e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleTest.java
@@ -0,0 +1,208 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2013-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.*;
+import static org.testng.Assert.assertEquals;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test the DistinguishedNameEqualityMatchingRule.
+ */
+@SuppressWarnings("javadoc")
+public class DistinguishedNameEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] { { "manager" }, { "manager " },
+            { "=Jim" },
+            { " =Jim" },
+            { "= Jim" },
+            { " = Jim" },
+            { "cn+Jim" },
+            { "cn + Jim" },
+            { "cn=Jim+" },
+            { "cn=Jim+manager" },
+            { "cn=Jim+manager " },
+            { "cn=Jim+manager," }, // { "cn=Jim," }, { "cn=Jim,  " }, {
+                                   // "c[n]=Jim" },
+            { "_cn=Jim" }, { "c_n=Jim" }, { "cn\"=Jim" }, { "c\"n=Jim" }, { "1cn=Jim" },
+            { "cn+uid=Jim" }, { "-cn=Jim" }, { "/tmp=a" }, { "\\tmp=a" }, { "cn;lang-en=Jim" },
+            { "@cn=Jim" }, { "_name_=Jim" },
+            { "\u03c0=pi" },
+            { "v1.0=buggy" }, // { "1.=buggy" }, { ".1=buggy" },
+            { "oid.1." }, { "1.3.6.1.4.1.1466..0=#04024869" }, { "cn=#a" }, { "cn=#ag" },
+            { "cn=#ga" }, { "cn=#abcdefgh" },
+            { "cn=a\\b" }, // { "cn=a\\bg" }, { "cn=\"hello" },
+            { "cn=+mail=,dc=example,dc=com" }, { "cn=xyz+sn=,dc=example,dc=com" },
+            { "cn=,dc=example,dc=com" } };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "", "", ConditionResult.TRUE },
+            { "   ", "", ConditionResult.TRUE },
+            { "cn=", "cn=", ConditionResult.TRUE },
+            { "cn= ", "cn=", ConditionResult.TRUE },
+            { "cn =", "cn=", ConditionResult.TRUE },
+            { "cn = ", "cn=", ConditionResult.TRUE },
+            { "dc=com", "dc=com", ConditionResult.TRUE },
+            { "dc=com+o=com", "dc=com+o=com", ConditionResult.TRUE },
+            { "DC=COM", "dc=com", ConditionResult.TRUE },
+            { "dc = com", "dc=com", ConditionResult.TRUE },
+            { " dc = com ", "dc=com", ConditionResult.TRUE },
+            { "dc=example,dc=com", "dc=example,dc=com", ConditionResult.TRUE },
+            { "dc=example, dc=com", "dc=example,dc=com", ConditionResult.TRUE },
+            { "dc=example ,dc=com", "dc=example,dc=com", ConditionResult.TRUE },
+            { "dc =example , dc  =   com", "dc=example,dc=com", ConditionResult.TRUE },
+            { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
+                "cn=doe+givenname=john,ou=people,dc=example,dc=com", ConditionResult.TRUE },
+            { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
+                "givenname=john\\+cn\\=doe,ou=people,dc=example,dc=com", ConditionResult.TRUE },
+            { "cn=Doe\\, John,ou=People,dc=example,dc=com",
+                "cn=doe\\, john,ou=people,dc=example,dc=com", ConditionResult.TRUE },
+            { "UID=jsmith,DC=example,DC=net", "uid=jsmith,dc=example,dc=net", ConditionResult.TRUE },
+            { "OU=Sales+CN=J. Smith,DC=example,DC=net", "cn=j. smith+ou=sales,dc=example,dc=net",
+                ConditionResult.TRUE },
+            { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+                "cn=james \\\"jim\\\" smith\\, iii,dc=example,dc=net", ConditionResult.TRUE },
+            { "CN=John Smith\\2C III,DC=example,DC=net", "cn=john smith\\, iii,dc=example,dc=net",
+                ConditionResult.TRUE },
+            { "CN=\\23John Smith\\20,DC=example,DC=net", "cn=\\#john smith,dc=example,dc=net",
+                ConditionResult.TRUE },
+            { "CN=Before\\0dAfter,DC=example,DC=net",
+                // \0d is a hex representation of Carriage return. It is mapped
+                // to a SPACE as defined in the MAP ( RFC 4518)
+                "cn=before after,dc=example,dc=net", ConditionResult.TRUE },
+            { "2.5.4.3=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "cn=hi", ConditionResult.TRUE },
+            { "1.1.1=", "1.1.1=", ConditionResult.TRUE },
+            { "CN=Lu\\C4\\8Di\\C4\\87", "cn=lu\u010di\u0107", ConditionResult.TRUE },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8,o=Airius", "ou=\u55b6\u696d\u90e8,o=airius",
+                ConditionResult.TRUE },
+            { "cn=\\ john \\ ,dc=com", "cn=\\ john \\ ,dc=com", ConditionResult.TRUE },
+            { "caseexact=UPPER,dc=COM", "caseexact=UPPER,dc=com", ConditionResult.TRUE },
+            { "caseexact=upper,dc=COM", "caseexact=UPPER,dc=com", ConditionResult.FALSE },
+            { "caseexact=UPPER,dc=COM", "caseexact=upper,dc=com", ConditionResult.FALSE },
+            { "caseexact=lower,dc=COM", "caseexact=lower,dc=com", ConditionResult.TRUE },
+            { "AB-global=", "ab-global=", ConditionResult.TRUE },
+            { "OU= Sales + CN = J. Smith ,DC=example,DC=net",
+                "cn=j. smith+ou=sales,dc=example,dc=net", ConditionResult.TRUE },
+            { "cn=John+a=Doe", "a=Doe+cn=john", ConditionResult.TRUE },
+            { "O=\"Sue, Grabbit and Runn\",C=US", "o=sue\\, grabbit and runn,c=us",
+                ConditionResult.TRUE }, };
+    }
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider(name = "testDNs")
+    public Object[][] createData() {
+        return new Object[][] {
+            { "", "" },
+            { "   ", "" },
+            { "cn=", "cn=" },
+            { "cn= ", "cn=" },
+            { "cn =", "cn=" },
+            { "cn = ", "cn=" },
+            { "dc=com", "dc=com" },
+            { "dc=com+o=com", "dc=com\u0001o=com" },
+            { "DC=COM", "dc=com" },
+            { "dc = com", "dc=com" },
+            { " dc = com ", "dc=com" },
+            { "dc=example,dc=com", "dc=com\u0000dc=example" },
+            { "dc=example, dc=com", "dc=com\u0000dc=example" },
+            { "dc=example ,dc=com", "dc=com\u0000dc=example" },
+            { "dc =example , dc  =   com", "dc=com\u0000dc=example" },
+            { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
+                "dc=com\u0000dc=example\u0000ou=people\u0000cn=doe\u0001givenname=john" },
+            { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
+                "dc=com\u0000dc=example\u0000ou=people\u0000givenname=john\u002Bcn\u003Ddoe" },
+            { "cn=Doe\\, John,ou=People,dc=example,dc=com",
+                "dc=com\u0000dc=example\u0000ou=people\u0000cn=doe, john" },
+            { "UID=jsmith,DC=example,DC=net", "dc=net\u0000dc=example\u0000uid=jsmith" },
+            { "OU=Sales+CN=J. Smith,DC=example,DC=net",
+                "dc=net\u0000dc=example\u0000cn=j. smith\u0001ou=sales" },
+            // commented due to checkstyle bug : https://github.com/checkstyle/checkstyle/issues/157
+            // uncomment when it is fixed
+            // { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+            //    "dc=net\u0000dc=example\u0000cn=james " + "\u005C\u0022jim\u005C\u0022" + " smith\u002C iii" },
+            { "CN=John Smith\\2C III,DC=example,DC=net",
+                "dc=net\u0000dc=example\u0000cn=john smith\u002C iii" },
+            { "CN=\\23John Smith\\20,DC=example,DC=net",
+                "dc=net\u0000dc=example\u0000cn=\u0023john smith" },
+            { "CN=Before\\0dAfter,DC=example,DC=net",
+                // \0d is a hex representation of Carriage return. It is mapped
+                // to a SPACE as defined in the MAP ( RFC 4518)
+                "dc=net\u0000dc=example\u0000cn=before after" },
+            { "2.5.4.3=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "cn=hi" },
+            { "1.1.1=", "1.1.1=" },
+            { "CN=Lu\\C4\\8Di\\C4\\87", "cn=luc\u030cic\u0301" },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8,o=Airius",
+                "o=airius\u0000ou=\u55b6\u696d\u90e8" },
+            { "cn=\\ john \\ ,dc=com", "dc=com\u0000cn=john" },
+            { "caseexact=UPPER,dc=COM", "dc=com\u0000caseexact=UPPER" },
+            { "caseexact=mIxEd,dc=COM", "dc=com\u0000caseexact=mIxEd" },
+            { "AB-global=", "ab-global=" },
+            { "OU= Sales + CN = J. Smith ,DC=example,DC=net",
+                "dc=net\u0000dc=example\u0000cn=j. smith\u0001ou=sales" },
+            { "cn=John+a=", "a=\u0001cn=john" },
+            { "O=\"Sue, Grabbit and Runn\",C=US", "c=us\u0000o=sue\u002C grabbit and runn" }, };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return CoreSchema.getDistinguishedNameMatchingRule();
+    }
+
+    @Test
+    public void testIsValidated() {
+        assertThat(getRule().isValidated()).isTrue();
+    }
+
+    /**
+     * Test the normalized values.
+     */
+    @Test(dataProvider = "testDNs")
+    public void testNormalization(final String value1, final String value2) throws Exception {
+        final MatchingRule rule = getRule();
+        final ByteString normalizedValue1 =
+                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value1));
+        final ByteString expectedValue = toExpectedNormalizedByteString(value2);
+        assertEquals(normalizedValue1, expectedValue);
+    }
+
+    private ByteString toExpectedNormalizedByteString(final String s) {
+        if (s.isEmpty()) {
+            return ByteString.valueOfUtf8(s);
+        }
+        return new ByteStringBuilder().appendByte(0).appendUtf8(s).toByteString();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EntrySchemaCheckingTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EntrySchemaCheckingTestCase.java
new file mode 100644
index 0000000..5ac5881
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EntrySchemaCheckingTestCase.java
@@ -0,0 +1,1137 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.defaultPolicy;
+import static org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.ignoreAll;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.EntryResolver;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.Action;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.testng.annotations.Test;
+
+/**
+ * Test schema validation using {@link Schema#validateEntry}.
+ */
+@Test
+public class EntrySchemaCheckingTestCase extends AbstractSchemaTestCase {
+
+    /**
+     * Tests schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes allowed by the DIT content rule are allowed even
+     * if not directly allowed by any of the entry's objectclasses.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testAllowAttributeAllowedByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testallowatallowedbydcroc-oid "
+                                + "NAME 'testAllowATAllowedByDCROC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testallowatallowedbydcroc-oid "
+                                        + "NAME 'testAllowATAllowedByDCR' MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testAllowATAllowedByDCROC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes required by the DIT content rule are allowed even
+     * if not directly allowed by any of the entry's objectclasses.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testAllowAttributeRequiredByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testallowatrequiredbydcroc-oid "
+                                + "NAME 'testAllowATRequiredByDCROC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testallowatrequiredbydcroc-oid "
+                                        + "NAME 'testAllowATRequiredByDCR' MUST description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testAllowATRequiredByDCROC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains structural and auxiliary
+     * objectclasses where the auxiliary class is allowed by a DIT content rule.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testAuxiliaryClassAllowedByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testauxiliaryclassallowedbydcroc-oid "
+                                        + "NAME 'testAuxiliaryClassAllowedByDCROC' SUP top STRUCTURAL "
+                                        + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addObjectClass(
+                                "( testauxiliaryclassallowedbydcrocaux-oid "
+                                        + "NAME 'testAuxiliaryClassAllowedByDCROCAux' SUP top AUXILIARY "
+                                        + "MAY description X-ORIGIN 'EntrySchemaCheckingTestCase')",
+                                false).addDITContentRule(
+                                "( testauxiliaryclassallowedbydcroc-oid "
+                                        + "NAME 'testAuxiliaryClassAllowedByDCR' "
+                                        + "AUX testAuxiliaryClassAllowedByDCROCAux "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testAuxiliaryClassAllowedByDCROC",
+            "objectClass: testAuxiliaryClassAllowedByDCROCAux",
+            "cn: test");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains structural and auxiliary
+     * objectclasses where the auxiliary class is not allowed by the associated
+     * DIT content rule.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testAuxiliaryClassNotAllowedByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testauxiliaryclassnotallowedbydcroc-oid "
+                                + "NAME 'testAuxiliaryClassNotAllowedByDCROC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addObjectClass(
+                                "( testauxiliaryclassnotallowedbydcrocaux-oid "
+                                        + "NAME 'testAuxiliaryClassNotAllowedByDCROCAux' SUP top "
+                                        + "AUXILIARY MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testauxiliaryclassnotallowedbydcroc-oid "
+                                        + "NAME 'testAuxiliaryClassNotAllowedByDCR' "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testAuxiliaryClassNotAllowedByDCROC",
+            "objectClass: testAuxiliaryClassNotAllowedByDCROCAux",
+            "cn: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.removeAttribute("objectClass", "testAuxiliaryClassNotAllowedByDCROCAux");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that includes an attribute type that
+     * is not allowed by any of its object classes.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDisallowedAttributeType() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testdisallowedattributetypeoc-oid "
+                                + "NAME 'testDisallowedAttributeTypeOC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testDisallowedAttributeTypeOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.removeAttribute("description", "foo");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Performs various tests to ensure that the server appropriately enforces
+     * DIT structure rule constraints.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDITStructureRuleConstraints() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testditstructureruleconstraintssupoc-oid "
+                                + "NAME 'testDITStructureRuleConstraintsSupOC' SUP top "
+                                + "STRUCTURAL MUST ou X-ORIGIN 'SchemaBackendTestCase')", false)
+                        .addObjectClass(
+                                "( testditstructureruleconstraintssuboc-oid "
+                                        + "NAME 'testDITStructureRuleConstraintsSubOC' SUP top "
+                                        + "STRUCTURAL MUST cn X-ORIGIN 'SchemaBackendTestCase')",
+                                false).addNameForm(
+                                "( testditstructureruleconstraintsupsnf-oid "
+                                        + "NAME 'testDITStructureRuleConstraintsSupNF' "
+                                        + "OC testDITStructureRuleConstraintsSupOC MUST ou "
+                                        + "X-ORIGIN 'SchemaBackendTestCase' )", false).addNameForm(
+                                "( testditstructureruleconstraintsubsnf-oid "
+                                        + "NAME 'testDITStructureRuleConstraintsSubNF' "
+                                        + "OC testDITStructureRuleConstraintsSubOC MUST cn "
+                                        + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                        .addDITStructureRule(
+                                "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                                        + "FORM testDITStructureRuleConstraintsSupNF "
+                                        + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                        .addDITStructureRule(
+                                "( 999015 " + "NAME 'testDITStructureRuleConstraintsSub' "
+                                        + "FORM testDITStructureRuleConstraintsSubNF SUP 999014 "
+                                        + "X-ORIGIN 'SchemaBackendTestCase' )", false).toSchema();
+
+        // @formatter:off
+        Entry e = newEntry(schema,
+            "dn: cn=child,ou=parent,o=test",
+            "objectClass: top",
+            "objectClass: testDITStructureRuleConstraintsSubOC",
+            "cn: child");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy().checkDITStructureRules(
+                Action.REJECT, newResolver(null)));
+
+        // @formatter:off
+        final Entry p = newEntry(schema,
+            "dn: ou=parent,o=test",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: testDITStructureRuleConstraintsSupOC",
+            "ou: parent");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy().checkDITStructureRules(Action.REJECT,
+                newResolver(p)));
+
+        // @formatter:off
+        e = newEntry(schema,
+            "dn: cn=not below valid parent,o=test",
+            "objectClass: top",
+            "objectClass: testDITStructureRuleConstraintsSubOC",
+            "cn: not below valid parent");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy().checkDITStructureRules(
+                Action.REJECT, newResolver(null)));
+
+        // @formatter:off
+        e = newEntry(schema,
+            "dn: o=invalid entry below parent covered by DSR,ou=parent,o=test",
+            "objectClass: top",
+            "objectClass: organization",
+            "o: invalid entry below parent covered by DSR");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy().checkDITStructureRules(
+                Action.REJECT, newResolver(p)));
+    }
+
+    /**
+     * Tests schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes prohibited by the DIT content rule are not allowed
+     * even if they are allowed by the associated object classes.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDontAllowAttributeProhibitedByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testdontallowattributeprohibitedbydcroc-oid "
+                                        + "NAME 'testDontAllowAttributeProhibitedByDCROC' SUP top "
+                                        + "STRUCTURAL MUST cn MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testdontallowattributeprohibitedbydcroc-oid "
+                                        + "NAME 'testDontAllowAttributeProhibitedByDCR' NOT description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testDontAllowAttributeProhibitedByDCROC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.removeAttribute("description", "foo");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes allowed by the DIT content rule are allowed but
+     * not required if they are not required by any of the associated object
+     * classes.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDontRequireAttributeAllowedByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testdontrequireatallowedbydcroc-oid "
+                                + "NAME 'testDontRequireATAllowedByDCROC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testdontrequireatallowedbydcroc-oid "
+                                        + "NAME 'testDontRequireATAllowedByDCR' MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testDontRequireATAllowedByDCROC",
+            "cn: test");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry for which there is a DIT content rule
+     * covering the structural objectclass but that DIT content rule is marked
+     * OBSOLETE. In this case, any attribute types required by the DIT content
+     * rule should not be required for the entry.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDontRequireAttributeRequiredByObsoleteDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testdontrequireatrequiredbyobsoletedcroc-oid "
+                                + "NAME 'testDontRequireATRequiredByObsoleteDCROC' SUP top "
+                                + "STRUCTURAL MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')",
+                        false).addDITContentRule(
+                        "( testdontrequireatrequiredbyobsoletedcroc-oid "
+                                + "NAME 'testDontRequireATRequiredByObsoleteDCR' OBSOLETE "
+                                + "MUST description X-ORIGIN 'EntrySchemaCheckingTestCase' )",
+                        false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testDontRequireATRequiredByObsoleteDCROC",
+            "cn: test");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that includes an attribute not defined
+     * in any objectClasses but the subtypes of the attribute are.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testInvalidSuperiorAttribute() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: uid=test.user,o=test",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalPerson",
+            "sn: User",
+            "cn: Test User",
+            "name: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, defaultPolicy());
+
+        e.removeAttribute("name", "foo");
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that is missing an attribute required
+     * by an auxiliary object class.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMissingAttributeRequiredByAuxiliaryClass() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testmissingatrequiredbyauxiliaryoc-oid "
+                                + "NAME 'testMissingATRequiredByAuxiliaryOC' SUP top AUXILIARY "
+                                + "MUST ( cn $ description ) "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: device",
+            "objectClass: testMissingATRequiredByAuxiliaryOC",
+            "cn: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.addAttribute("description", "xxx");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that is missing an attribute required
+     * by its structural object class.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMissingAttributeRequiredByStructuralClass() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testmissingatrequiredbystructuraloc-oid "
+                                + "NAME 'testMissingATRequiredByStructuralOC' SUP top STRUCTURAL "
+                                + "MUST ( cn $ description ) "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testMissingATRequiredByStructuralOC",
+            "cn: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.addAttribute("description", "xxx");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains multiple structural
+     * objectclasses.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMultipleStructuralClasses() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: uid=test.user,o=test",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organization",
+            "o: test",
+            "sn: User",
+            "cn: Test User");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, defaultPolicy());
+
+        e.removeAttribute("objectClass", "organization");
+        e.removeAttribute("o", "test");
+
+        assertConformsToSchema(e, ignoreAll().checkAttributeValues(Action.REJECT));
+    }
+
+    /**
+     * Tests schema checking for an entry that includes multiple values for a
+     * multivalued attribute.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMultipleValuesForMultiValuedAttribute() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: o=test",
+            "objectClass: top",
+            "objectClass: organization",
+            "o: test",
+            "o: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that includes multiple values for a
+     * single-valued attribute.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMultipleValuesForSingleValuedAttribute() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "dc: example",
+            "dc: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, ignoreAll().checkAttributeValues(Action.REJECT));
+
+        e.removeAttribute("dc", "foo");
+        assertConformsToSchema(e, ignoreAll().checkAttributeValues(Action.REJECT));
+    }
+
+    /**
+     * Tests schema checking for an entry that includes multiple values for a
+     * single-valued operational attribute.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMultipleValuesForSingleValuedOperationalAttribute() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: domain",
+            "dc: example",
+            "creatorsName: cn=Directory Manager",
+            "creatorsName: cn=Another Manager");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, ignoreAll().checkAttributeValues(Action.REJECT));
+
+        e.removeAttribute("creatorsName", "cn=Another Manager");
+        assertConformsToSchema(e, ignoreAll().checkAttributeValues(Action.REJECT));
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * multivalued RDN component is compliant with that name form which requires
+     * one value but allows other values.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMVSatisfiesOptionalMultiValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testmvsatisfiesoptionalmultivaluednameformoc-oid "
+                                        + "NAME 'testMVSatisfiesOptionalMultiValuedNameFormOC' SUP top "
+                                        + "STRUCTURAL MUST cn MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addNameForm(
+                                "( testmvsatisfiesoptionalmultivaluednameform-oid "
+                                        + "NAME 'testMVSatisfiesOptionalMultiValuedNameForm' "
+                                        + "OC testMVSatisfiesOptionalMultiValuedNameFormOC MUST cn "
+                                        + "MAY description X-ORIGIN 'EntrySchemaCheckingTestCase' )",
+                                false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test+description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testMVSatisfiesOptionalMultiValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be rejected if its
+     * multivalued RDN component violates that name form which only allows a
+     * single value.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testMVViolatesSingleValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testmvviolatessinglevaluednameformoc-oid "
+                                        + "NAME 'testMVViolatesSingleValuedNameFormOC' SUP top STRUCTURAL "
+                                        + "MUST cn MAY description X-ORIGIN 'EntrySchemaCheckingTestCase')",
+                                false).addNameForm(
+                                "( testmvviolatessinglevaluednameform-oid "
+                                        + "NAME 'testMVViolatesSingleValuedNameForm' "
+                                        + "OC testMVViolatesSingleValuedNameFormOC MUST cn "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test+description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testMVViolatesSingleValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.setName("cn=test,o=test");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that does not contain a structural
+     * objectclass.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testNoStructuralClass() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( domain-oid " + "NAME 'domain' SUP top STRUCTURAL "
+                                + "MUST dc X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: dcObject",
+            "dc: example");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.addAttribute("objectClass", "domain");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes required by the DIT content rule are required even
+     * if not directly allowed by any of the entry's objectclasses.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testRequireAttributeRequiredByDCR() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testrequireatrequiredbydcroc-oid "
+                                + "NAME 'testRequireATRequiredByDCROC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addDITContentRule(
+                                "( testrequireatrequiredbydcroc-oid "
+                                        + "NAME 'testRequireATRequiredByDCR' MUST description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testRequireATRequiredByDCROC",
+            "cn: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.addAttribute("description", "foo");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * multivalued RDN component is compliant with that name form which requires
+     * multiple values.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSatisfiesRequiredMultiValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testsatisfiesrequiredmultivaluednameformoc-oid "
+                                + "NAME 'testSatisfiesRequiredMultiValuedNameFormOC' SUP top "
+                                + "STRUCTURAL MUST cn MAY description "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false).addNameForm(
+                        "( testsatisfiesrequiredmultivaluednameform-oid "
+                                + "NAME 'testSatisfiesRequiredMultiValuedNameForm' "
+                                + "OC testSatisfiesRequiredMultiValuedNameFormOC "
+                                + "MUST ( cn $ description ) "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test+description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testSatisfiesRequiredMultiValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * single-valued RDN component is compliant with that name form.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSatisfiesSingleValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testsatisfiessinglevaluednameformoc-oid "
+                                + "NAME 'testSatisfiesSingleValuedNameFormOC' SUP top STRUCTURAL "
+                                + "MUST cn X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addNameForm(
+                                "( testsatisfiessinglevaluednameform-oid "
+                                        + "NAME 'testSatisfiesSingleValuedNameForm' "
+                                        + "OC testSatisfiesSingleValuedNameFormOC MUST cn "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testSatisfiesSingleValuedNameFormOC",
+            "cn: test");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * single-valued RDN component is compliant with that name form which
+     * requires one value but allows other values.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSVSatisfiesOptionalMultiValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testsvsatisfiesoptionalmultivaluednameformoc-oid "
+                                        + "NAME 'testSVSatisfiesOptionalMultiValuedNameFormOC' SUP top "
+                                        + "STRUCTURAL MUST cn MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addNameForm(
+                                "( testsvsatisfiesoptionalmultivaluednameform-oid "
+                                        + "NAME 'testSVSatisfiesOptionalMultiValuedNameForm' "
+                                        + "OC testSVSatisfiesOptionalMultiValuedNameFormOC MUST cn "
+                                        + "MAY description X-ORIGIN 'EntrySchemaCheckingTestCase' )",
+                                false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testSVSatisfiesOptionalMultiValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * single-valued RDN component violates that name form which requires one
+     * value but allows other values.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testSVViolatesOptionalMultiValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testsvviolatesoptionalmultivaluednameformoc-oid "
+                                        + "NAME 'testSVViolatesOptionalMultiValuedNameFormOC' SUP top "
+                                        + "STRUCTURAL MUST cn MAY description "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+                        .addNameForm(
+                                "( testsvviolatesoptionalmultivaluednameform-oid "
+                                        + "NAME 'testSVViolatesOptionalMultiValuedNameForm' "
+                                        + "OC testSVViolatesOptionalMultiValuedNameFormOC MUST cn "
+                                        + "MAY description X-ORIGIN 'EntrySchemaCheckingTestCase' )",
+                                false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testSVViolatesOptionalMultiValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.setName("cn=test+description=foo,o=test");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains an undefined objectclass
+     * as well as a valid structural class.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testUndefinedAuxiliaryObjectClass() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: xxxundefinedauxiliaryxxx",
+            "cn: test",
+            "sn: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, defaultPolicy());
+
+        e.removeAttribute("objectClass", "xxxundefinedauxiliaryxxx");
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains an undefined objectclass
+     * with no other structural class.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testUndefinedStructuralObjectClass() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: xxxundefinedstructuralxxx",
+            "o: test");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, defaultPolicy());
+
+        e.removeAttribute("objectClass", "xxxundefinedstructuralxxx");
+        e.addAttribute("objectClass", "organization");
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry that contains an undefined objectclass
+     * when there is no structural objectclass and no structural objectclass checking.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testUndefinedObjectClassNoStructural() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+                "dn: o=test",
+                "objectClass: top",
+                "objectClass: xxxundefinedxxx");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, defaultPolicy().requireSingleStructuralObjectClass(Action.IGNORE));
+
+        e.removeAttribute("objectClass", "xxxundefinedxxx");
+        assertConformsToSchema(e, defaultPolicy().requireSingleStructuralObjectClass(Action.IGNORE));
+    }
+
+    /**
+     * Tests schema checking for an entry with a valid single structural
+     * objectclass.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testValidSingleStructuralClass() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organization",
+            "o: example");
+        // @formatter:on
+
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests schema checking for an entry (not covered by a DIT content rule)
+     * with a valid single structural objectclass as well as an auxiliary
+     * objectclass.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testValidSingleStructuralClassAndAuxiliaryClass() throws Exception {
+        // @formatter:off
+        final Entry e = newEntry(
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organization",
+            "objectClass: dcObject",
+            "dc: example",
+            "o: Example Org");
+        // @formatter:on
+
+        assertConformsToSchema(e, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be accepted if its
+     * single-valued RDN component only contains one of the multiple required
+     * attribute types.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testViolatesRequiredMultiValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testviolatesrequiredmultivaluednameformoc-oid "
+                                + "NAME 'testViolatesRequiredMultiValuedNameFormOC' SUP top "
+                                + "STRUCTURAL MUST cn MAY description "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false).addNameForm(
+                        "( testviolatesrequiredmultivaluednameform-oid "
+                                + "NAME 'testViolatesRequiredMultiValuedNameForm' "
+                                + "OC testViolatesRequiredMultiValuedNameFormOC "
+                                + "MUST ( cn $ description ) "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: cn=test,o=test",
+            "objectClass: top",
+            "objectClass: testViolatesRequiredMultiValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.setName("cn=test+description=foo,o=test");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will be rejected if its
+     * single-valued RDN component violates that name form.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testViolatesSingleValuedNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema())
+                        .addObjectClass(
+                                "( testviolatessinglevaluednameformoc-oid "
+                                        + "NAME 'testViolatesSingleValuedNameFormOC' SUP top STRUCTURAL "
+                                        + "MUST cn MAY description X-ORIGIN 'EntrySchemaCheckingTestCase')",
+                                false).addNameForm(
+                                "( testviolatessinglevaluednameform-oid "
+                                        + "NAME 'testViolatesSingleValuedNameForm' "
+                                        + "OC testViolatesSingleValuedNameFormOC MUST cn "
+                                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+                        .toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testViolatesSingleValuedNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertDoesNotConformToSchema(e, schema, defaultPolicy());
+
+        e.setName("cn=test,o=test");
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    /**
+     * Tests that an entry covered by a name form will not be rejected if its
+     * single-valued RDN component violates that name form but the name form is
+     * declared OBSOLETE.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testViolatesSingleValuedObsoleteNameForm() throws Exception {
+        final Schema schema =
+                new SchemaBuilder(Schema.getDefaultSchema()).addObjectClass(
+                        "( testviolatessinglevaluedobsoletenameformoc-oid "
+                                + "NAME 'testViolatesSingleValuedObsoleteNameFormOC' SUP top "
+                                + "STRUCTURAL MUST cn MAY description "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase')", false).addNameForm(
+                        "( testviolatessinglevaluedobsoletenameform-oid "
+                                + "NAME 'testViolatesSingleValuedObsoleteNameForm' OBSOLETE "
+                                + "OC testViolatesSingleValuedObsoleteNameFormOC MUST cn "
+                                + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false).toSchema();
+
+        // @formatter:off
+        final Entry e = newEntry(schema,
+            "dn: description=foo,o=test",
+            "objectClass: top",
+            "objectClass: testViolatesSingleValuedObsoleteNameFormOC",
+            "cn: test",
+            "description: foo");
+        // @formatter:on
+
+        assertConformsToSchema(e, schema, defaultPolicy());
+    }
+
+    private void assertConformsToSchema(final Entry entry, final Schema schema,
+            final SchemaValidationPolicy policy) {
+        final List<LocalizableMessage> errorMessages = new LinkedList<>();
+        assertThat(schema.validateEntry(entry, policy, errorMessages)).as(errorMessages.toString())
+                .isTrue();
+    }
+
+    private void assertConformsToSchema(final Entry entry, final SchemaValidationPolicy policy) {
+        assertConformsToSchema(entry, Schema.getDefaultSchema(), policy);
+    }
+
+    private void assertDoesNotConformToSchema(final Entry entry, final Schema schema,
+            final SchemaValidationPolicy policy) {
+        final List<LocalizableMessage> errorMessages = new LinkedList<>();
+        assertThat(schema.validateEntry(entry, policy, errorMessages)).as(errorMessages.toString())
+                .isFalse();
+    }
+
+    private void assertDoesNotConformToSchema(final Entry entry, final SchemaValidationPolicy policy) {
+        assertDoesNotConformToSchema(entry, Schema.getDefaultSchema(), policy);
+    }
+
+    private Entry newEntry(final Schema schema, final String... ldif) {
+        try {
+            return new LDIFEntryReader(ldif).setSchema(schema).readEntry();
+        } catch (final Exception e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    private Entry newEntry(final String... ldif) {
+        return newEntry(Schema.getDefaultSchema(), ldif);
+    }
+
+    private EntryResolver newResolver(final Entry e) {
+        return new EntryResolver() {
+
+            @Override
+            public Entry getEntry(final DN dn) throws LdapException {
+                if (e == null) {
+                    throw newLdapException(ResultCode.NO_SUCH_OBJECT, "no such entry " + dn);
+                }
+
+                assertThat((Object) dn).isEqualTo(e.getName());
+                return e;
+            }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EnumSyntaxTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EnumSyntaxTestCase.java
new file mode 100644
index 0000000..7ce2b00
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/EnumSyntaxTestCase.java
@@ -0,0 +1,80 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.ConditionResult.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.testng.Assert.*;
+
+/** Enum syntax tests. */
+@SuppressWarnings("javadoc")
+public class EnumSyntaxTestCase extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "arbit-day", false }, { "wednesday", true }, };
+    }
+
+    @Test
+    public void testDecode() throws SchemaException, DecodeException {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 3.3.3  DESC 'Day Of The Week' "
+                + " X-ENUM  ( 'monday' 'tuesday'   'wednesday'  'thursday'  'friday' "
+                + " 'saturday' 'sunday') )", true);
+        final Schema schema = builder.toSchema();
+        final Syntax syntax = schema.getSyntax("3.3.3");
+        final MatchingRule rule = syntax.getOrderingMatchingRule();
+        final ByteString monday = ByteString.valueOfUtf8("monday");
+        final ByteString normMonday = rule.normalizeAttributeValue(monday);
+        final ByteString tuesday = ByteString.valueOfUtf8("tuesday");
+        final ByteString normTuesday = rule.normalizeAttributeValue(tuesday);
+        final ByteString normThursday = rule.normalizeAttributeValue(ByteString.valueOfUtf8("thursday"));
+        assertEquals(rule.getGreaterOrEqualAssertion(monday).matches(normThursday), TRUE);
+        assertEquals(rule.getLessOrEqualAssertion(monday).matches(normThursday), FALSE);
+        assertEquals(rule.getGreaterOrEqualAssertion(tuesday).matches(normMonday), FALSE);
+        assertEquals(rule.getLessOrEqualAssertion(tuesday).matches(normMonday), TRUE);
+        assertEquals(rule.getGreaterOrEqualAssertion(tuesday).matches(normTuesday), TRUE);
+        assertEquals(rule.getLessOrEqualAssertion(tuesday).matches(normTuesday), TRUE);
+        assertEquals(rule.getAssertion(tuesday).matches(normMonday), TRUE);
+        assertEquals(rule.getAssertion(monday).matches(normThursday), FALSE);
+        assertEquals(rule.getAssertion(tuesday).matches(normTuesday), FALSE);
+        assertNotNull(schema.getMatchingRule(OMR_OID_GENERIC_ENUM + ".3.3.3"));
+    }
+
+    @Test
+    public void testDuplicateEnum() throws SchemaException, DecodeException {
+        // This should be handled silently.
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 3.3.3  DESC 'Day Of The Week' "
+                + " X-ENUM  ( 'monday' 'tuesday'   'wednesday'  'thursday'  'friday' "
+                + " 'saturday' 'monday') )", true);
+        builder.toSchema();
+    }
+
+    @Override
+    protected Syntax getRule() throws SchemaException, DecodeException {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addEnumerationSyntax("3.3.3", "Day Of The Week", false, "monday", "tuesday",
+                "wednesday", "thursday", "friday", "saturday", "sunday");
+        return builder.toSchema().getSyntax("3.3.3");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..2a95e90
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleTest.java
@@ -0,0 +1,76 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_GENERALIZED_TIME_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the GeneralizedTimeEqualityMatchingRule. */
+public class GeneralizedTimeEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            {"2006september061Z"},
+            {"2006"},
+            {"200609061Z"},
+            {"20060906135Z"},
+            {"200609061350G"},
+            {"2006090613mmZ"},
+            {"20060906135030.011"},
+            {"20060906135030Zx"},
+            {"20060906135030.Z"},
+            {"20060906135030.aZ"},
+            {"20060906135030"},
+            {"20060906135030.123"},
+            {"20060906135030-2500"},
+            {"20060906135030-2070"},
+            // Following values do not pass - they passed in server
+            //{"20060931135030Z"},
+            //{"20060229135030Z"},
+            //{"20060230135030Z"},
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            {"2006090613Z",             "20060906130000.000Z", ConditionResult.TRUE },
+            {"200609061350Z",           "20060906135000.000Z", ConditionResult.TRUE },
+            {"200609061351Z",           "20060906135000.000Z", ConditionResult.FALSE },
+            {"20060906135030Z",         "20060906135030.000Z", ConditionResult.TRUE },
+            {"20060906135030.3Z",       "20060906135030.300Z", ConditionResult.TRUE },
+            {"20060906135030.30Z",      "20060906135030.300Z", ConditionResult.TRUE },
+            {"20060906135030Z",         "20060906135030.000Z", ConditionResult.TRUE },
+            {"20060906135030.0Z",       "20060906135030.000Z", ConditionResult.TRUE },
+            {"20060906135030.0118Z",    "20060906135030.012Z", ConditionResult.TRUE },
+            {"20060906135030+01",       "20060906125030.000Z", ConditionResult.TRUE },
+            {"20060906135030+0101",     "20060906124930.000Z", ConditionResult.TRUE },
+            {"20070417055812.318-0500", "20070417105812.318Z", ConditionResult.TRUE },
+            // Following values do not pass - they passed in server
+            //{"2007041705.5Z",           "20070417053000.000Z", ConditionResult.TRUE },
+            //{"200704170558.5Z",         "20070417055830.000Z", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_GENERALIZED_TIME_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..c90aea4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleTest.java
@@ -0,0 +1,72 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_GENERALIZED_TIME_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Test the GeneralizedTimeOrderingMatchingRule. */
+public class GeneralizedTimeOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        return new Object[][] {
+            { "20060912180130"},
+            {"2006123123595aZ"},
+            {"200a1231235959Z"},
+            {"2006j231235959Z"},
+            {"20061231#35959Z"},
+            {"20060912180a30Z"},
+            {"20060912180030Z.01"},
+            {"200609121800"},
+            {"20060912180129.hhZ"},
+            {"20060912180129.1hZ"},
+            {"20060906135030+aa01"},
+            {"2006"},
+            {"20060906135030+3359"},
+            {"20060906135030+2389"},
+            {"20060906135030+2361"},
+            {"20060906135030+"},
+            {"20060906135030+0"},
+            {"20060906135030+010"},
+        };
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            {"20060906135030+0101", "20060906135030+2359",  1},
+            {"20060912180130Z",     "20060912180130Z",      0},
+            {"20060912180130z",     "20060912180130Z",      0},
+            {"20060912180130Z",     "20060912180129Z",      1},
+            {"20060912180129Z",     "20060912180130Z",     -1},
+            {"20060912180129.000Z", "20060912180130.001Z", -1},
+            {"20060912180129.1Z",   "20060912180130.2Z",   -1},
+            {"20060912180129.11Z",  "20060912180130.12Z",  -1},
+            // OPENDJ-2397 - dates before 1970 have negative ms.
+            {"19000101010203Z",     "20000101010203Z",     -1},
+            {"20000101010203Z",     "19000101010203Z",      1},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_GENERALIZED_TIME_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxTest.java
new file mode 100644
index 0000000..b7ec520
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxTest.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GENERALIZED_TIME_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Generalized time syntax tests. */
+public class GeneralizedTimeSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "2006090613Z", true }, { "20060906135030+01", true }, { "200609061350Z", true },
+            { "20060906135030Z", true }, { "20061116135030Z", true }, { "20061126135030Z", true },
+            { "20061231235959Z", true }, { "20060906135030+0101", true }, { "20060906135030+2359", true },
+            { "20060906135030+3359", false }, { "20060906135030+2389", false }, { "20060906135030+2361", false },
+            { "20060906135030+", false }, { "20060906135030+0", false }, { "20060906135030+010", false },
+            { "20061200235959Z", false }, { "2006121a235959Z", false }, { "2006122a235959Z", false },
+            { "20060031235959Z", false }, { "20061331235959Z", false }, { "20062231235959Z", false },
+            { "20061232235959Z", false }, { "2006123123595aZ", false }, { "200a1231235959Z", false },
+            { "2006j231235959Z", false }, { "200612-1235959Z", false }, { "20061231#35959Z", false },
+            { "2006", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_GENERALIZED_TIME_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GuideSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GuideSyntaxTest.java
new file mode 100644
index 0000000..a6106a1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/GuideSyntaxTest.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GUIDE_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Guide syntax tests. */
+public class GuideSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "sn$EQ|!(sn$EQ)", true }, { "!(sn$EQ)", true },
+            { "person#sn$EQ", true }, { "(sn$EQ)", true }, { "sn$EQ", true },
+            { "sn$SUBSTR", true }, { "sn$GE", true }, { "sn$LE", true }, { "sn$ME", false },
+            { "?true", true }, { "?false", true }, { "true|sn$GE", false }, { "sn$APPROX", true },
+            { "sn$EQ|(sn$EQ)", true }, { "sn$EQ|(sn$EQ", false }, { "sn$EQ|(sn$EQ)|sn$EQ", true },
+            { "sn$EQ|(cn$APPROX&?false)", true }, { "sn$EQ|(cn$APPROX&|?false)", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_GUIDE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxTest.java
new file mode 100644
index 0000000..bb0fe1c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IA5StringSyntaxTest.java
@@ -0,0 +1,35 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_IA5_STRING_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** IA5 string syntax tests. */
+public class IA5StringSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "12345678", true }, { "12345678\u2163", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_IA5_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..b0bfb70
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerEqualityMatchingRuleTest.java
@@ -0,0 +1,68 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_INTEGER_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the IntegerEqualityMatchingRule. */
+public class IntegerEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        /*
+         * The JDK8 BigInteger parser is quite tolerant and allows leading zeros
+         * and + characters. It's ok if the matching rule is more tolerant than
+         * the syntax itself (see commented data).
+         */
+        return new Object[][] {
+            //{"01"},
+            //{"00"},
+            //{"-01"},
+            {"1-2"},
+            {"b2"},
+            {"-"},
+            {""},
+            {" 63 "},
+            {"- 63"},
+            //{"+63" },
+            {"AB"  },
+            {"0xAB"},
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            {"1234567890",  "1234567890",   ConditionResult.TRUE},
+            {"-1",          "-1",           ConditionResult.TRUE},
+            {"-9876543210", "-9876543210",  ConditionResult.TRUE},
+            {"1",           "-1",           ConditionResult.FALSE},
+            // Values which are greater than 64 bits.
+            { "-987654321987654321987654321", "-987654321987654321987654322", ConditionResult.FALSE },
+            {"987654321987654321987654321", "987654321987654321987654322", ConditionResult.FALSE },
+            { "987654321987654321987654321", "987654321987654321987654321", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_INTEGER_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..38116bd
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerOrderingMatchingRuleTest.java
@@ -0,0 +1,145 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.schema.IntegerOrderingMatchingRuleImpl.SIGN_MASK_NEGATIVE;
+import static org.forgerock.opendj.ldap.schema.IntegerOrderingMatchingRuleImpl.SIGN_MASK_POSITIVE;
+import static org.forgerock.opendj.ldap.schema.IntegerOrderingMatchingRuleImpl.encodeHeader;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_INTEGER_OID;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the IntegerOrderingMatchingRule. */
+@SuppressWarnings("javadoc")
+public class IntegerOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        /*
+         * The JDK8 BigInteger parser is quite tolerant and allows leading zeros
+         * and + characters. It's ok if the matching rule is more tolerant than
+         * the syntax itself (see commented data).
+         */
+        return new Object[][] {
+            //{"01"},
+            //{"00"},
+            //{"-01"},
+            { "1-2" },
+            { "b2" },
+            { "-" },
+            { "" },
+            {" 63 "},
+            {"- 63"},
+            //{"+63" },
+            {"AB"  },
+            {"0xAB"},
+        };
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            {"1",   "0",   1},
+            {"1",   "1",   0},
+            {"45",  "54", -1},
+            {"-63", "63", -1},
+            {"-63", "0",  -1},
+            {"63",  "0",   1},
+            {"0",   "-63", 1},
+            // Values which are greater than 64 bits.
+            { "-987654321987654321987654321", "-987654321987654321987654322", 1 },
+            {"987654321987654321987654321", "987654321987654321987654322", -1},
+            { "987654321987654321987654321", "987654321987654321987654321", 0 },
+            // Values which have very different encoded lengths.
+            { "-987654321987654321987654321", "-1", -1 },
+            { "-987654321987654321987654321", "1", -1 },
+            { "987654321987654321987654321", "-1", 1 },
+            { "987654321987654321987654321", "1", 1 },
+            { "-1", "-987654321987654321987654321", 1 },
+            { "1", "-987654321987654321987654321", 1 },
+            {"-1", "987654321987654321987654322", -1},
+            {"1", "987654321987654321987654322", -1},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_INTEGER_OID);
+    }
+
+    private enum Sign {
+        POSITIVE(SIGN_MASK_POSITIVE), NEGATIVE(SIGN_MASK_NEGATIVE);
+        private final byte mask;
+        private Sign(byte mask) {
+            this.mask = mask;
+        }
+    }
+
+    private int length(int i) {
+        return i;
+    }
+
+    private String expected(int... bytes) {
+        ByteStringBuilder builder = new ByteStringBuilder();
+        for (int b : bytes) {
+            builder.appendByte(b);
+        }
+        return builder.toByteString().toHexString();
+    }
+
+    @DataProvider
+    private Object[][] headerEncoding() {
+        return new Object[][] {
+            // @formatter:off
+            { length(1 << 0),      Sign.POSITIVE, expected(0x81) },
+            { length(1 << 4) - 1,  Sign.POSITIVE, expected(0x8f) },
+            { length(1 << 4),      Sign.POSITIVE, expected(0x90, 0x10) },
+            { length(1 << 12) - 1, Sign.POSITIVE, expected(0x9f, 0xff) },
+            { length(1 << 12),     Sign.POSITIVE, expected(0xa0, 0x10, 0x00) },
+            { length(1 << 20) - 1, Sign.POSITIVE, expected(0xaf, 0xff, 0xff) },
+            { length(1 << 20),     Sign.POSITIVE, expected(0xb0, 0x10, 0x00, 0x00) },
+            { length(1 << 28) - 1, Sign.POSITIVE, expected(0xbf, 0xff, 0xff, 0xff) },
+            { length(1 << 28),     Sign.POSITIVE, expected(0xc1, 0x00, 0x00, 0x00, 0x00) },
+            { length(1 << 31) - 1, Sign.POSITIVE, expected(0xc7, 0xff, 0xff, 0xff, 0xf0) },
+
+            { length(1 << 0),      Sign.NEGATIVE, expected(0x7e) },
+            { length(1 << 4) - 1,  Sign.NEGATIVE, expected(0x70) },
+            { length(1 << 4),      Sign.NEGATIVE, expected(0x6f, 0xef) },
+            { length(1 << 12) - 1, Sign.NEGATIVE, expected(0x60, 0x00) },
+            { length(1 << 12),     Sign.NEGATIVE, expected(0x5f, 0xef, 0xff) },
+            { length(1 << 20) - 1, Sign.NEGATIVE, expected(0x50, 0x00, 0x00) },
+            { length(1 << 20),     Sign.NEGATIVE, expected(0x4f, 0xef, 0xff, 0xff) },
+            { length(1 << 28) - 1, Sign.NEGATIVE, expected(0x40, 0x00, 0x00, 0x00) },
+            { length(1 << 28),     Sign.NEGATIVE, expected(0x3e, 0xff, 0xff, 0xff, 0xff) },
+            { length(1 << 31) - 1, Sign.NEGATIVE, expected(0x38, 0x00, 0x00, 0x00, 0x0f) },
+            // @formatter:on
+        };
+    }
+
+    @Test(dataProvider = "headerEncoding")
+    public void testHeaderEncoding(int length, Sign sign, String expectedHexString) {
+        ByteStringBuilder builder = new ByteStringBuilder();
+        encodeHeader(builder, length, sign.mask);
+        ByteString actual = builder.toByteString();
+        assertThat(actual.toHexString()).isEqualTo(expectedHexString);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxTest.java
new file mode 100644
index 0000000..92b7083
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/IntegerSyntaxTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_INTEGER_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Integer syntax tests. */
+@Test
+public class IntegerSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object [][] {
+            {"123", true},
+            {"987654321", true},
+            {"-1", true},
+            {"10001", true},
+            {"001", false},
+            {"-01", false},
+            {"12345678\u2163", false},
+            {" 123", false},
+            {"123 ", false}
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_INTEGER_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxTest.java
new file mode 100644
index 0000000..7c5616b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/JPEGSyntaxTest.java
@@ -0,0 +1,116 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.testng.Assert.*;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ByteString;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * JPEG syntax tests.
+ */
+@SuppressWarnings("javadoc")
+@Test
+public class JPEGSyntaxTest extends AbstractSchemaTestCase {
+
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        String nonImage =
+            "AAECAwQFBgcICQ==";
+        String jfifImage =
+            "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACMYGh4aFiMeHB4nJSMpNFc4NDAwNGpM"
+            + "UD9Xfm+EgnxveneLnMipi5S9lnd6ru2wvc7V4OLgh6f1//PZ/8jb4Nf/2wBDASUn"
+            + "JzQuNGY4OGbXj3qP19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX"
+            + "19fX19fX19fX19fX19f/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEA"
+            + "AAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh"
+            + "MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6"
+            + "Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZ"
+            + "mqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx"
+            + "8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA"
+            + "AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV"
+            + "YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp"
+            + "anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPE"
+            + "xcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCC"
+            + "iiiuM9g//9k=";
+        String exifImage =
+            "/9j/4QD2RXhpZgAATU0AKgAAAAgACQESAAMAAAABAAEAAAEaAAUAAAABAAAAegEb"
+            + "AAUAAAABAAAAggEoAAMAAAABAAIAAAExAAIAAAAQAAAAigEyAAIAAAAUAAAAmgE8"
+            + "AAIAAAAOAAAArgITAAMAAAABAAEAAIdpAAQAAAABAAAAvAAAAAAASAAAAAEAAABI"
+            + "AAAAAQAAUXVpY2tUaW1lIDcuNy4xADIwMTI6MDg6MjAgMTI6MTE6MTIATWFjIE9T"
+            + "IFggMTAuOAAAApAAAAcAAAAEMDIyMJADAAIAAAAUAAAA2gAAAAAyMDEyOjA4OjIw"
+            + "IDEyOjEwOjA2AP/+AAxBcHBsZU1hcmsK/9sAQwAjGBoeGhYjHhweJyUjKTRXODQw"
+            + "MDRqTFA/V35vhIJ8b3p3i5zIqYuUvZZ3eq7tsL3O1eDi4Ien9f/z2f/I2+DX/9sA"
+            + "QwElJyc0LjRmODhm1496j9fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX"
+            + "19fX19fX19fX19fX19fX19fX/8AAEQgAAQABAwEiAAIRAQMRAf/EAB8AAAEFAQEB"
+            + "AQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAE"
+            + "EQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2"
+            + "Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SV"
+            + "lpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn"
+            + "6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//E"
+            + "ALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkj"
+            + "M1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2Rl"
+            + "ZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5"
+            + "usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMR"
+            + "AD8AgooorjPYP//Z";
+        String pngImage =
+            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAABlBMVEX/JgAAAAAP"
+            + "IsinAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==";
+
+        return new Object[][] {
+            { ByteString.valueOfBase64(nonImage), false },
+            { ByteString.valueOfBase64(jfifImage), true },
+            { ByteString.valueOfBase64(exifImage), true },
+            { ByteString.valueOfBase64(pngImage), false }
+        };
+    }
+
+    /** Test acceptable values for this syntax when not allowing malformed JPEG photos. */
+    @Test(dataProvider = "acceptableValues")
+    public void testAcceptableValues(ByteString value, boolean expectedResult) {
+        final Syntax syntax = getSyntax(false);
+        final LocalizableMessageBuilder reason = new LocalizableMessageBuilder();
+
+        final boolean result = syntax.valueIsAcceptable(value, reason);
+        assertEquals(result, expectedResult,
+                syntax + ".valueIsAcceptable gave bad result for " + value + "reason : " + reason);
+
+    }
+
+    /** Test acceptable values for this syntax when allowing malformed JPEG photos. */
+    @Test(dataProvider = "acceptableValues")
+    public void testAcceptableValuesWhenAllowingMalformedJPEG(ByteString value, boolean notUsedForThisTest) {
+        final Syntax syntax = getSyntax(true);
+        final LocalizableMessageBuilder reason = new LocalizableMessageBuilder();
+
+        final boolean liveResult = syntax.valueIsAcceptable(value, reason);
+        // should always be true
+        assertTrue(liveResult, syntax + ".valueIsAcceptable gave bad result for " + value + "reason : " + reason);
+    }
+
+    private Syntax getSyntax(boolean allowMalformedJpegPhotos) {
+        SchemaBuilder builder =
+            new SchemaBuilder(getCoreSchema()).setOption(ALLOW_MALFORMED_JPEG_PHOTOS, allowMalformedJpegPhotos);
+        return builder.toSchema().getSyntax(SYNTAX_JPEG_OID);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxTest.java
new file mode 100644
index 0000000..0996218
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/LDAPSyntaxTest.java
@@ -0,0 +1,77 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_LDAP_SYNTAX_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** LDAP syntax tests. */
+@Test
+public class LDAPSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-9EN ('this' 'is' 'a' 'test'))", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "(X-name 'this", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "(X-name 'this'", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "Y-name 'this')", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-name 'this' 'is')", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-name )", false },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X- ('this' 'is' 'a' 'test'))", false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this')",
+                false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'", false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'))))",
+                false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a  X-name-b ('this'))))", false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a  'X-name-b' ('this'))))", false },
+            {
+                "( 2.5.4.3 DESC 'full syntax description' "
+                        + "X-name ('this' 'is' 'a' 'test') X-name-a 'this' X-name-b ('this'))", true },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-a-_eN_- ('this' 'is' 'a' 'test'))",  true },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-name ('this'))", true },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-name 'this')", true },
+            { "( 2.5.4.3 DESC 'full syntax description' " + "X-name 'this' X-name-a 'test')", true },
+            { "( 2.5.4.3 DESC 'full syntax description' )", true },
+            { "   (    2.5.4.3    DESC  ' syntax description'    )", true },
+            { "( 2.5.4.3 DESC syntax description )", false },
+            { "($%^*&!@ DESC 'syntax description' )", false },
+            { "(temp-oid DESC 'syntax description' )", true },
+            { "2.5.4.3 DESC 'syntax description' )", false },
+            { "(2.5.4.3 DESC 'syntax description' ", false },
+            { "( 1.1.1 DESC 'Host and Port in the format of HOST:PORT' X-PATTERN '^[a-z-A-Z]+:[0-9.]+\\d$' )", true },
+        };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_LDAP_SYNTAX_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxTest.java
new file mode 100644
index 0000000..9525595
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleSyntaxTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_MATCHING_RULE_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Matching rule syntax tests. */
+@Test
+public class MatchingRuleSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            {
+                "( 1.2.3.4 NAME 'fullMatchingRule' "
+                        + " DESC 'description of matching rule' OBSOLETE "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 "
+                        + " X-name ( 'this is an extension' ) )", true },
+            {
+                "( 1.2.3.4 NAME 'missingClosingParenthesis' "
+                        + " DESC 'description of matching rule' "
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 "
+                        + " X-name ( 'this is an extension' ) ", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_MATCHING_RULE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTest.java
new file mode 100644
index 0000000..538652d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTest.java
@@ -0,0 +1,106 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.testng.Assert.assertEquals;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test The equality matching rules and the equality matching rule api.
+ */
+@SuppressWarnings("javadoc")
+public abstract class MatchingRuleTest extends AbstractSchemaTestCase {
+    /**
+     * Generate invalid assertion values for the Matching Rule test.
+     *
+     * @return the data for the EqualityMatchingRulesInvalidValuestest.
+     */
+    @DataProvider(name = "matchingRuleInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return createMatchingRuleInvalidAttributeValues();
+    }
+
+    /**
+     * Generate invalid attribute values for the Matching Rule test.
+     *
+     * @return the data for the EqualityMatchingRulesInvalidValuestest.
+     */
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public abstract Object[][] createMatchingRuleInvalidAttributeValues();
+
+    /**
+     * Generate data for the Matching Rule test.
+     *
+     * @return the data for the equality matching rule test.
+     */
+    @DataProvider(name = "matchingrules")
+    public abstract Object[][] createMatchingRuleTest();
+
+    /**
+     * Test the normalization and the comparison of valid values.
+     */
+    @Test(dataProvider = "matchingrules")
+    public void matchingRules(final String value1, final String value2, final ConditionResult result)
+            throws Exception {
+        final MatchingRule rule = getRule();
+
+        // normalize the 2 provided values and check that they are equals
+        final ByteString normalizedValue1 =
+                rule.normalizeAttributeValue(ByteString.valueOfUtf8(value1));
+        final Assertion assertion = rule.getAssertion(ByteString.valueOfUtf8(value2));
+
+        final ConditionResult liveResult = assertion.matches(normalizedValue1);
+        assertEquals(result, liveResult);
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "matchingRuleInvalidAssertionValues")
+    public void matchingRulesInvalidAssertionValues(final String value) throws Exception {
+        // Get the instance of the rule to be tested.
+        final MatchingRule rule = getRule();
+
+        rule.getAssertion(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "matchingRuleInvalidAttributeValues")
+    public void matchingRulesInvalidAttributeValues(final String value) throws Exception {
+        // Get the instance of the rule to be tested.
+        final MatchingRule rule = getRule();
+
+        rule.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Get an instance of the matching rule.
+     *
+     * @return An instance of the matching rule to test.
+     */
+    protected abstract MatchingRule getRule();
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTestCase.java
new file mode 100644
index 0000000..5e7829e
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleTestCase.java
@@ -0,0 +1,744 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_AUTH_PASSWORD_EXACT_DESCRIPTION;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_AUTH_PASSWORD_EXACT_NAME;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_AUTH_PASSWORD_EXACT_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_AUTH_PASSWORD_OID;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.testng.annotations.Test;
+
+/**
+ * This class tests the MatchingRule class. The matching rule builder can be only used with the schema builder.
+ */
+@SuppressWarnings("javadoc")
+public class MatchingRuleTestCase extends AbstractSchemaTestCase {
+
+    @Test
+    public final void testCreatesBasicMatchingRule() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getDescription()).isEqualTo("Directory String");
+        assertThat(mr.getDescription()).isEqualTo("An example of matching rule");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(mr.isObsolete()).isFalse();
+    }
+
+    @Test
+    public final void testCreatesOverrideBasicMatchingRule() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule(EMR_AUTH_PASSWORD_EXACT_OID)
+            .names(Collections.singletonList(EMR_AUTH_PASSWORD_EXACT_NAME))
+            .description(EMR_AUTH_PASSWORD_EXACT_DESCRIPTION)
+            .syntaxOID(SYNTAX_AUTH_PASSWORD_OID)
+            .extraProperties("New extra propertie")
+            .implementation(new AuthPasswordExactEqualityMatchingRuleImpl())
+            .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule(EMR_AUTH_PASSWORD_EXACT_OID);
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isTrue();
+        assertThat(mr.isObsolete()).isFalse();
+    }
+
+    /**
+     * The builder requires an OID or throw an exception.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowEmptyOid() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule("")
+            .names("ExampleMatch")
+            .description("An example of matching rule")
+            .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+            .extraProperties("LDAP Schema Update Procedures")
+            .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+            .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * The builder requires an OID or throw an exception.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowNullOid() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule((String) null)
+            .names("ExampleMatch")
+            .description("An example of matching rule")
+            .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+            .extraProperties("LDAP Schema Update Procedures")
+            .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+            .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * When syntax is missing, the builder sends exception.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowNullSyntax() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule("1.1.4.1")
+            .names("ExampleMatch")
+            .description("An example of matching rule")
+            .extraProperties("LDAP Schema Update Procedures")
+            .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+            .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * Matching rule name is optional.
+     */
+    @Test
+    public final void testBuilderAllowsEmptyName() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule("1.1.4.1")
+            .description("An example of matching rule")
+            .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+            .extraProperties("LDAP Schema Update Procedures")
+            .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+            .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("1.1.4.1");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(mr.getNames().size()).isEqualTo(0);
+        assertThat(mr.getNameOrOID()).isEqualTo("1.1.4.1");
+    }
+
+    /**
+     * Multiple names can be set to the matching rule.
+     */
+    @Test
+    public final void testBuilderAllowsMultipleNames() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("my new matching rule")
+                .names("maching rule test")
+                .names("exampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("1.1.4.1");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(mr.getNames().size()).isEqualTo(3);
+        assertThat(mr.hasName("my new matching rule")).isTrue();
+        assertThat(mr.hasName("maching rule test")).isTrue();
+        assertThat(mr.hasName("exampleMatch")).isTrue();
+    }
+
+    /**
+     * Name in optional for a matching rule. (RFC 4512)
+     */
+    @Test
+    public final void testBuilderRemoveNames() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("my new matching rule")
+                .names("maching rule test")
+                .names("exampleMatch")
+                .removeAllNames()
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("1.1.4.1");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(mr.getNames().size()).isEqualTo(0);
+        assertThat(mr.getNameOrOID()).isEqualTo("1.1.4.1");
+    }
+
+    /**
+     * The builder allows to remove selected name.
+     */
+    @Test
+    public final void testBuilderRemoveSelectedName() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("my new matching rule")
+                .names("maching rule test")
+                .names("exampleMatch")
+                .removeName("maching rule test")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("1.1.4.1");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(mr.getNames().size()).isEqualTo(2);
+        assertThat(mr.hasName("my new matching rule")).isTrue();
+        assertThat(mr.hasName("exampleMatch")).isTrue();
+    }
+
+    /**
+     * The builder allows a missing description.
+     */
+    @Test
+    public final void testBuilderAllowsNoDescription() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getDescription()).isEmpty();
+        assertThat(mr.getDescription()).isEqualTo("");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+    }
+
+    /**
+     * The builder allows empty description.
+     */
+    @Test
+    public final void testBuilderAllowsEmptyDescription() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getDescription()).isEmpty();
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(mr.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+    }
+
+    /**
+     * Extra properties is not a mandatory field.
+     */
+    @Test
+    public final void testBuilderAllowsNoExtraProperties() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("Example match description")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getDescription()).isEqualTo("Example match description");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties()).isEmpty();
+    }
+
+    /**
+     * Extra properties set to null is not allowed.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testBuilderDoesntAllowNullExtraProperties() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("Example match description")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15")
+                .extraProperties(null)
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * Removes all the extra properties.
+     */
+    @Test
+    public final void testBuilderRemoveExtraProperties() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("exampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .removeAllExtraProperties()
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("1.1.4.1");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(mr.getExtraProperties()).isEmpty();
+        assertThat(mr.getNames().size()).isEqualTo(1);
+        assertThat(mr.getNames().get(0)).isEqualTo("exampleMatch");
+    }
+
+    /**
+     * If the implementation is not set, the schema will use the default matching rule for this one.
+     */
+    @Test
+    public final void testBuilderAllowsNoImplementation() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("Example match description")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().contains("The default matching rule \"2.5.13.17\" will be used instead"));
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getDescription()).isEqualTo("Example match description");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+    }
+
+    /**
+     * If the implementation is null, the schema will use the default matching rule for this one.
+     */
+    @Test
+    public final void testBuilderAllowsNullImplementation() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("Example match description")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(null)
+                .addToSchemaOverwrite()
+                .toSchema();
+        // @formatter:on
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(
+                schema.getWarnings().toString()
+                        .contains("The default matching rule \"2.5.13.17\" will be used instead")).isTrue();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr).isNotNull();
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.getDescription()).isEqualTo("Example match description");
+        assertThat(mr.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+    }
+
+    /**
+     * Sets a matching rule using a string definition.
+     */
+    @Test
+    public final void testAddingAMatchingRuleDefinitionStringNoOverride() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition = "( 1.1.4.1 NAME 'ExampleMatch' DESC 'An example of"
+                + " Matching Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
+
+        sb.addMatchingRule(definition, false);
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(
+                schema.getWarnings().toString()
+                        .contains("The default matching rule \"2.5.13.17\" will be used instead")).isTrue();
+        assertThat(schema.getMatchingRules()).isNotEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr.toString()).isEqualTo(definition);
+        assertThat(mr.isObsolete()).isFalse();
+    }
+
+    /**
+     * Sets a matching rule using a string definition.
+     */
+    @Test
+    public final void testAddingAMatchingRuleDefinitionStringOverride() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition = "( 2.5.13.0 NAME 'objectIdentifierMatch'"
+                + " OBSOLETE SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )";
+
+        sb.addMatchingRule(definition, true);
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getMatchingRules()).isNotEmpty();
+        final MatchingRule mr = schema.getMatchingRule("objectIdentifierMatch");
+        assertThat(mr.getOID()).isEqualTo("2.5.13.0");
+        assertThat(mr.toString()).isEqualTo(definition);
+        assertThat(mr.isObsolete()).isTrue();
+    }
+
+
+    /**
+     * Duplicates an existing matching rule.
+     */
+    @Test
+    public final void testDuplicatesExistingMatchingRule() {
+
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final MatchingRule.Builder nfb = new MatchingRule.Builder("1.1.4.1", sb);
+        nfb.description("This is a new matching rule")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .extraProperties("X-ORIGIN", "NO RFC")
+                .implementation(new BooleanEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite();
+        // @formatter:on
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr.getOID()).isEqualTo("1.1.4.1");
+
+        // @formatter:off
+        sb.buildMatchingRule(mr)
+            .names("Dolly")
+            .oid("2.5.13.0.1")
+            .obsolete(true)
+            .addToSchemaOverwrite();
+        // @formatter:on
+
+        schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule dolly = schema.getMatchingRule("Dolly");
+        assertThat(dolly.getOID()).isEqualTo("2.5.13.0.1");
+        assertThat(dolly.getSyntax().getDescription()).isEqualTo("Directory String");
+        assertThat(dolly.getDescription()).isEqualTo("An example of matching rule");
+        assertThat(dolly.getSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(dolly.getExtraProperties().containsKey("New extra propertie")).isFalse();
+        assertThat(dolly.getExtraProperties().containsKey("LDAP Schema Update Procedures")).isTrue();
+        assertThat(dolly.getExtraProperties().containsKey("X-ORIGIN")).isTrue();
+        assertThat(dolly.isObsolete()).isTrue();
+    }
+
+    /**
+     * Equality between matching rules.
+     */
+    @Test
+    public final void testMatchingRuleEqualsTrue() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        final MatchingRule mr1 = schema.getMatchingRule("ExampleMatch");
+
+        // @formatter:off
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("Second")
+                .description("A second example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+        final MatchingRule mr2 = schema2.getMatchingRule("Second");
+
+        assertThat(mr2.equals(mr1)).isTrue();
+    }
+
+    /**
+     * Equality between matching rules fails.
+     */
+    @Test
+    public final void testMatchingRuleEqualsFalse() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        final MatchingRule mr1 = schema.getMatchingRule("ExampleMatch");
+
+        // @formatter:off
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.2")
+                .names("Second")
+                .description("A second example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+        final MatchingRule mr2 = schema2.getMatchingRule("Second");
+
+        assertThat(mr2.equals(mr1)).isFalse();
+    }
+
+    /**
+     * Verifies the builder definition.
+     */
+    @Test
+    public final void testVerifyMatchingRuleDefinition() {
+
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final MatchingRule.Builder nfb = new MatchingRule.Builder("1.1.4.1", sb);
+        nfb.description("This is a new matching rule")
+                .names("ExampleMatch")
+                .names("MyExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures", "RFC 2252")
+                .extraProperties("X-ORIGIN", "NONE")
+                .obsolete(true)
+                .implementation(new BooleanEqualityMatchingRuleImpl())
+                .addToSchemaOverwrite();
+        // @formatter:on
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final String definition = "( 1.1.4.1 NAME ( 'ExampleMatch' 'MyExampleMatch' ) "
+                + "DESC 'An example of matching rule' OBSOLETE " + "SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 "
+                + "LDAP Schema Update Procedures 'RFC 2252' X-ORIGIN 'NONE' )";
+
+        final MatchingRule mr = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr.toString()).isEqualTo(definition);
+    }
+
+    /**
+     * Equality between builder and definition.
+     */
+    @Test
+    public final void testMatchingRuleEqualityReturnsTrueBetweenBuilderAndDefinition() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr1 = schema.getMatchingRule("ExampleMatch");
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final String definition = "( 1.1.4.1 NAME 'ExampleMatch2' DESC 'An example of"
+                + " Matching Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
+
+        sb2.addMatchingRule(definition, false);
+        // @formatter:on
+        final MatchingRule mr2 = sb2.toSchema().getMatchingRule("ExampleMatch2");
+        assertThat(mr1.equals(mr2)).isTrue();
+    }
+
+    /**
+     * Equality between builder and definition fails.
+     */
+    @Test
+    public final void testMatchingRuleEqualityReturnsTrueBetweenBuilderAndDefinitionFails() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .extraProperties("LDAP Schema Update Procedures")
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRule mr1 = schema.getMatchingRule("ExampleMatch");
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final String definition = "( 1.1.4.2 NAME 'ExampleMatch2' DESC 'An example of"
+                + " Matching Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )";
+
+        sb2.addMatchingRule(definition, false);
+        // @formatter:on
+        final MatchingRule mr2 = sb2.toSchema().getMatchingRule("ExampleMatch2");
+        assertThat(mr1.equals(mr2)).isFalse();
+    }
+
+    /**
+     * The builder allows to create chained matching rules.
+     */
+    @Test
+    public final void testCreatesMatchingRulesUsingChainingMethods() {
+        final Map<String, List<String>> extraProperties = new TreeMap<>();
+        final List<String> extra = new ArrayList<>();
+        extra.add("Custom");
+        extraProperties.put("X-ORIGIN", extra);
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildMatchingRule("1.1.4.1")
+                .names("ExampleMatch")
+                .description("An example of matching rule")
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15") // DirectoryStringSyntax OID.
+                .implementation(new DirectoryStringFirstComponentEqualityMatchingRuleImpl())
+                .extraProperties(extraProperties)
+                .addToSchema()
+            .buildMatchingRule("1.1.4.9999")
+                .names("SecondExampleMatch")
+                .description("Another example of matching rule")
+                .extraProperties(extraProperties)
+                .syntaxOID("1.3.6.1.4.1.1466.115.121.1.15")
+                .implementation(new BooleanEqualityMatchingRuleImpl())
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+
+        // First
+        final MatchingRule mr1 = schema.getMatchingRule("ExampleMatch");
+        assertThat(mr1.getOID()).isEqualTo("1.1.4.1");
+        assertThat(mr1.getDescription()).isEqualTo("An example of matching rule");
+
+        // Second
+        final MatchingRule mr2 = schema.getMatchingRule("SecondExampleMatch");
+        assertThat(mr2.getOID()).isEqualTo("1.1.4.9999");
+        assertThat(mr2.getDescription()).isEqualTo("Another example of matching rule");
+
+        assertThat(mr1.getExtraProperties()).isEqualTo(mr2.getExtraProperties());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseBuilderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseBuilderTestCase.java
new file mode 100644
index 0000000..8489a6b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseBuilderTestCase.java
@@ -0,0 +1,147 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static java.util.Collections.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.MapAssert.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+import org.testng.annotations.Test;
+
+public class MatchingRuleUseBuilderTestCase extends AbstractSchemaTestCase {
+
+    @Test
+    public void testValidMatchingRuleUse() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse(EMR_CASE_EXACT_OID)
+                .names("Matching rule use test")
+                .description("Matching rule use description")
+                .attributes("2.5.4.40", "2.5.4.52", "2.5.4.53")
+                .extraProperties("property name", "property value")
+                .addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final MatchingRuleUse mru = schema.getMatchingRuleUse(EMR_CASE_EXACT_OID);
+        assertThat(mru).isNotNull();
+        assertThat(mru.getMatchingRuleOID()).isEqualTo(EMR_CASE_EXACT_OID);
+        assertThat(mru.getNames()).containsOnly("Matching rule use test");
+        assertThat(mru.getDescription()).isEqualTo("Matching rule use description");
+        assertThat(mru.getAttributes()).containsOnly(schema.getAttributeType("2.5.4.40"),
+                                                     schema.getAttributeType("2.5.4.52"),
+                                                     schema.getAttributeType("2.5.4.53"));
+        assertThat(mru.getExtraProperties()).includes(entry("property name", singletonList("property value")));
+        assertThat(mru.isObsolete()).isFalse();
+    }
+
+    @Test
+    public void testCopyConstructor() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse(EMR_BIT_STRING_OID)
+                .description("Matching rule use description")
+                .names("Matching rule use test")
+                .attributes("2.5.4.40")
+                .extraProperties("property name", "property value")
+                .addToSchema();
+        final Schema schema = builder.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final Schema schemaCopy = builder.buildMatchingRuleUse(schema.getMatchingRuleUse(EMR_BIT_STRING_OID))
+                .oid(EMR_OCTET_STRING_OID)
+                .names("Matching rule use test copy")
+                .attributes("2.5.4.53")
+                .addToSchema()
+                .toSchema();
+        assertThat(schemaCopy.getWarnings()).isEmpty();
+
+        final MatchingRuleUse mru = schemaCopy.getMatchingRuleUse(EMR_OCTET_STRING_OID);
+        assertThat(mru).isNotNull();
+        assertThat(mru.getMatchingRuleOID()).isEqualTo(EMR_OCTET_STRING_OID);
+        assertThat(mru.getNames()).containsOnly("Matching rule use test", "Matching rule use test copy");
+        assertThat(mru.getDescription()).isEqualTo("Matching rule use description");
+        assertThat(mru.getAttributes()).containsOnly(schema.getAttributeType("2.5.4.40"),
+                                                     schema.getAttributeType("2.5.4.53"));
+        assertThat(mru.getExtraProperties()).includes(entry("property name", singletonList("property value")));
+        assertThat(mru.isObsolete()).isFalse();
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testBuilderDoesNotAllowOverwrite() throws Exception {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse(EMR_BIT_STRING_OID)
+                .names("Matching rule use test")
+                .attributes("2.5.4.40")
+                .addToSchema();
+
+        builder.buildMatchingRuleUse(EMR_BIT_STRING_OID)
+               .addToSchema()
+               .toSchema();
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testBuilderDoesNotAllowNullMatchingRuleOID() throws Exception {
+        new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse((String) null)
+                .addToSchema();
+    }
+
+    @Test
+    public void testBuilderRemoveAll() throws Exception {
+        final MatchingRuleUse.Builder builder = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse(EMR_BIT_STRING_OID)
+                .description("Matching rule use description")
+                .names("Matching rule use test")
+                .attributes("2.5.4.40", "2.5.4.52")
+                .extraProperties("property name", "property value");
+
+        final Schema schema = builder.removeAllNames()
+                .removeAllAttributes()
+                .removeAllExtraProperties()
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final MatchingRuleUse mru = schema.getMatchingRuleUse(EMR_BIT_STRING_OID);
+        assertThat(mru.getNames()).isEmpty();
+        assertThat(mru.getAttributes()).isEmpty();
+        assertThat(mru.getExtraProperties()).isEmpty();
+    }
+
+    @Test
+    public void testBuilderRemove() throws Exception {
+        final MatchingRuleUse.Builder builder = new SchemaBuilder(Schema.getCoreSchema())
+                .buildMatchingRuleUse(EMR_OCTET_STRING_OID)
+                .description("Matching rule use description")
+                .names("Matching rule use test", "I should not be in the schema")
+                .attributes("2.5.4.52", "I should not be in the schema")
+                .extraProperties("property name", "property value");
+
+        final Schema schema = builder.removeName("I should not be in the schema")
+                .removeAttribute("I should not be in the schema")
+                .removeExtraProperty("property name")
+                .addToSchema()
+                .toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final MatchingRuleUse mru = schema.getMatchingRuleUse(EMR_OCTET_STRING_OID);
+        assertThat(mru.getNames()).containsOnly("Matching rule use test");
+        assertThat(mru.getAttributes()).containsOnly(schema.getAttributeType("2.5.4.52"));
+        assertThat(mru.getExtraProperties()).isEmpty();
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxTest.java
new file mode 100644
index 0000000..c3f1c8f
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/MatchingRuleUseSyntaxTest.java
@@ -0,0 +1,45 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_MATCHING_RULE_USE_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Matching rule use syntax tests. */
+@Test
+public class MatchingRuleUseSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            {
+                "( 2.5.13.10 NAME 'fullMatchingRule' "
+                        + " DESC 'description of matching rule' OBSOLETE " + " APPLIES 2.5.4.3 "
+                        + " X-name 'this is an extension' )", true },
+            {
+                "( 2.5.13.10 NAME 'missingClosingParenthesis' "
+                        + " DESC 'description of matching rule' " + " SYNTAX 2.5.4.3 "
+                        + " X-name ( 'this is an extension' ) ", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_MATCHING_RULE_USE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NameFormTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NameFormTestCase.java
new file mode 100644
index 0000000..f66aabf
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NameFormTestCase.java
@@ -0,0 +1,1044 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.schema.NameForm.Builder;
+import org.testng.annotations.Test;
+
+/**
+ * This class tests the NameForm class. The name form builder can be only used
+ * with the schema builder.
+ */
+@SuppressWarnings("javadoc")
+public class NameFormTestCase extends AbstractSchemaTestCase {
+
+    /**
+     * Creates a new form using the required parameters only (oid, structural
+     * OID and required attributes).
+     */
+    @Test
+    public final void testCreatesANewFormWithOnlyRequiredParameters() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                    .structuralObjectClassOID("person")
+                    .requiredAttributes("sn", "cn") // ("cn, sn") is not supported.
+                    .addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms().size()).isGreaterThan(0);
+
+        for (final NameForm nf : schema.getNameForms()) {
+            assertThat(nf.hasName("hasAName ?")).isFalse();
+            assertThat(nf.getNameOrOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+            assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+            assertThat(nf.toString()).isEqualTo("( 1.3.6.1.4.1.1466.115.121.1.35 OC person MUST ( sn $ cn ) )");
+        }
+    }
+
+    /**
+     * Creates a new form with a name.
+     */
+    @Test
+    public final void testCreatesANewFormWithAName() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                    .structuralObjectClassOID("person")
+                    .names("MyNewForm")
+                    .requiredAttributes("sn", "cn")
+                    .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms().size()).isGreaterThan(0);
+
+        for (final NameForm nf : schema.getNameForms()) {
+
+            assertThat(nf.hasName("hasAName ?")).isFalse();
+            assertThat(nf.getNameOrOID()).isEqualTo("MyNewForm");
+            assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+
+            assertThat(nf.toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' OC person MUST ( sn $ cn ) )");
+        }
+    }
+
+    /**
+     * Creates a new form with optional attributes OID.
+     */
+    @Test
+    public final void testCreatesANewFormWithOptionalAttributesOid() {
+
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .structuralObjectClassOID("person")
+                .names("MyNewForm")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("owner")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms().size()).isGreaterThan(0);
+
+        for (final NameForm nf : schema.getNameForms()) {
+            assertThat(nf.hasName("hasAName ?")).isFalse();
+            assertThat(nf.getNameOrOID()).isEqualTo("MyNewForm");
+            assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+            assertThat(nf.getOptionalAttributes().toString()).contains("owner");
+
+            assertThat(nf.toString()).isEqualTo(
+                    "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' OC person MUST ( sn $ cn ) MAY owner )");
+        }
+    }
+
+    /**
+     * Creates a new form with ExtraProperties.
+     */
+    @Test
+    public final void testCreatesANewNameFormWithExtraProperties() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .structuralObjectClassOID("person")
+                .names("MyNewForm")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("owner")
+                .extraProperties("X-ORIGIN", "RFC xxx")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms().size()).isGreaterThan(0);
+
+        for (final NameForm nf : schema.getNameForms()) {
+            assertThat(nf.hasName("hasAName ?")).isFalse();
+            assertThat(nf.getNameOrOID()).isEqualTo("MyNewForm");
+            assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+            assertThat(nf.getExtraProperties().get("X-ORIGIN").get(0)).isEqualTo("RFC xxx");
+            assertThat(nf.toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' OC person "
+                + "MUST ( sn $ cn ) MAY owner X-ORIGIN 'RFC xxx' )");
+        }
+    }
+
+    /**
+     * When required attributes are absents, the builder sends exception. Here,
+     * the OID is missing. An exception is expected.
+     *
+     * @throws SchemaException
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowNullOid() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm((String) null)
+                .description("This is a description")
+                .names("name1")
+                .names("name2", "name3")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .structuralObjectClassOID("person")
+                .requiredAttributes("sn, cn")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * When required attributes are absents, the builder sends an exception.
+     * Here, the structural class OID is missing.
+     *
+     * @throws SchemaException
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowNullStructuralClassOid() {
+
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn, cn")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * When required attributes are absents, the builder sends an exception.
+     * Here, the required attributes OID is missing.
+     *
+     * @throws SchemaException
+     */
+    @Test(expectedExceptions = java.lang.IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowEmptyRequiredAttributes() {
+
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes()
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * When required attributes are absents, the builder sends an exception.
+     * Here, the required attribute is missing.
+     *
+     * @throws SchemaException
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesntAllowNullRequiredAttributes() {
+
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * Optional attributes shouldn't be equals to null. Exception expected.
+     *
+     * @throws SchemaException
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testBuilderDoesntAllowNullOptionalAttributes() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .structuralObjectClassOID("person")
+                .requiredAttributes("sn, cn")
+                .requiredAttributes((String[]) null)
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * By default optional attributes are empty.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testBuilderAllowsEmptyOptionalAttributes() {
+        // @formatter:off
+        new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                // .optionalAttributeOIDs("") empty by default.
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * Allows removing non-existent attributes without errors.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testBuilderAllowRemovingNonexistentAttributes() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("This is a description")
+                .names("MyNewForm")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .structuralObjectClassOID("person")
+                .requiredAttributes("sn")
+                .removeRequiredAttribute("unknown")
+                .removeOptionalAttribute("optionalunknown")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+
+        assertThat(schema.getNameForms()).isNotEmpty();
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+        assertThat(nf.getRequiredAttributes().size()).isEqualTo(1);
+        assertThat(nf.getRequiredAttributes().iterator().next().getNameOrOID()).isEqualTo("sn");
+        assertThat(nf.getOptionalAttributes()).isEmpty();
+    }
+
+    /**
+     * Verifying the schema builder allows to add directly a definition. The
+     * name form is created as well.
+     */
+    @Test
+    public final void testNameFormDefinition() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        // @formatter:off
+        final String nameFormDefinition = "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' "
+                + "DESC 'Description of the new form' "
+                + "OC person MUST ( sn $ cn ) "
+                + "MAY ( description $ uid ) "
+                + "X-SCHEMA-FILE 'NameFormCheckingTestCase' "
+                + "X-ORIGIN 'NameFormCheckingTestCase' )";
+        // @formatter:on
+
+        // Add the nameForm to the schemaBuilder.
+        sb.addNameForm(nameFormDefinition, false);
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        assertThat(schema.getNameForms()).isNotEmpty();
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+        assertThat(nf.getExtraProperties()).isNotEmpty();
+
+        // @formatter:off
+        assertThat(nf.toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' "
+                + "DESC 'Description of the new form' "
+                + "OC person MUST ( sn $ cn ) "
+                + "MAY ( description $ uid ) "
+                + "X-SCHEMA-FILE 'NameFormCheckingTestCase' "
+                + "X-ORIGIN 'NameFormCheckingTestCase' )");
+        // @formatter:on
+    }
+
+    /**
+     * Required attributes are missing in the following definition.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testNameFormDefinitionDoesntAllowMissingAttributes() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        // @formatter:off
+        final String nameFormDefinition = "( 1.3.6.1.4.1.1466.115.121.1.35 NAME 'MyNewForm' "
+                + "DESC 'Description of the new form' "
+                + "OC person "
+                + "MAY ( description $ uid ) "
+                + "X-SCHEMA-FILE 'NameFormCheckingTestCase' "
+                + "X-ORIGIN 'EntrySchemaCheckingTestCase' "
+                + "X-ORIGIN 'NameFormCheckingTestCase' )";
+        // @formatter:on
+
+        // Add the nameForm to the schemaBuilder.
+        sb.addNameForm(nameFormDefinition, false);
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * Duplicates a name form using the schema builder.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testDuplicatesTheNameForm() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final Builder nfb = new Builder("1.3.6.1.4.1.1466.115.121.1.35", sb);
+        nfb.description("Description of the new form")
+            .names("MyNewForm")
+            .structuralObjectClassOID("person")
+            .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+            .requiredAttributes("sn", "cn")
+            .optionalAttributes("description", "uid")
+            .addToSchema();
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms()).isNotEmpty();
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+
+        sb.buildNameForm(nf)
+            .names("Dolly")
+            .oid("1.3.6.1.4.1.1466.115.121.1.36")
+            .addToSchemaOverwrite();
+        schema = sb.toSchema();
+        assertThat(schema.getNameForms()).isNotEmpty();
+        assertThat(schema.getNameForms().size()).isEqualTo(2);
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final Iterator<NameForm> i = schema.getNameForms().iterator();
+        i.next(); // Jump the first element (== nf)
+        final NameForm dolly = i.next(); // Our new cloned NameForm.
+        assertThat(dolly.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.36"); // With the new OID !
+        assertThat(dolly.getNames().size()).isEqualTo(2);
+    }
+
+    /**
+     * Duplicates a name form using the schema builder.
+     * The duplicate name form contains an inappropriate structural class OID which made the build fails.
+     * <p>Warning from schema is : <pre>
+     * "The name form description "MyNewForm" is associated with a structural object class
+     * "wrongStructuralOID" which is not defined in the schema".</pre>
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testDuplicatesTheNameFormFails() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final Builder nfb = new Builder("1.3.6.1.4.1.1466.115.121.1.35", sb);
+        nfb.description("Description of the new form")
+            .names("MyNewForm")
+            .structuralObjectClassOID("person")
+            .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+            .requiredAttributes("sn", "cn")
+            .optionalAttributes("description", "uid")
+            .addToSchema();
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms()).isNotEmpty();
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+
+        sb.buildNameForm(nf)
+            .names("Dolly")
+            .oid("1.3.6.1.4.1.1466.115.121.1.36")
+            .structuralObjectClassOID("wrongStructuralOID")
+            .addToSchemaOverwrite();
+        schema = sb.toSchema();
+        assertThat(schema.getNameForms().size()).isEqualTo(1); // MyNewForm
+        // The duplicate name form is  not created and the schema contains warnings about.
+        assertThat(schema.getWarnings()).isNotEmpty();
+    }
+
+    /**
+     * Compare two same name forms using the equal function.
+     */
+    @Test
+    public final void testNameFormEqualsTrue() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .names("TheNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        final NameForm nf1 = schema.getNameForms().iterator().next();
+
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+            .names("MyNewForm")
+            .structuralObjectClassOID("person")
+            .requiredAttributes("sn", "cn")
+            .addToSchema().toSchema();
+        final NameForm nf2 = schema2.getNameForm("MyNewForm");
+
+        assertThat(nf1.equals(nf2)).isTrue();
+    }
+
+    /**
+     * Equals between two name forms fails.
+     */
+    @Test
+    public final void testNameFormEqualsFalse() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .names("TheNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+        final NameForm nf1 = schema.getNameForms().iterator().next();
+
+        // @formatter:off
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.36")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .names("TheNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(nf1.equals(schema2.getNameForms().iterator().next())).isFalse();
+    }
+
+    /**
+     * Testing to add a name form using the definition.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testCreateFormUsingDefinitionAndSchemaBuilder() {
+        final SchemaBuilder sb = new SchemaBuilder();
+
+        // @formatter:off
+        sb.addSchema(Schema.getCoreSchema(), false)
+            .addObjectClass(
+                "( mycustomobjectclass-oid NAME 'myCustomObjectClassOC' SUP top "
+                + "STRUCTURAL MUST cn X-ORIGIN 'NameFormTestCase')", false)
+            .addNameForm(
+                "( mycustomnameform-oid NAME 'myCustomNameForm' OC myCustomObjectClassOC "
+                + "MUST cn X-ORIGIN 'NameFormTestCase' )",
+                false)
+            .toSchema();
+        // @formatter:on
+
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameFormsWithName("mycustomnameform")).isNotNull();
+        for (final NameForm o : schema.getNameForms()) {
+            assertThat(o.getNameOrOID()).isEqualTo("myCustomNameForm");
+            assertThat(o.getOID()).isEqualTo("mycustomnameform-oid");
+            assertThat(o.getStructuralClass().getOID()).isEqualTo("mycustomobjectclass-oid");
+        }
+    }
+
+    /**
+     * Compare two same name forms using the equal function. One created by the
+     * name form builder, the other by the schema builder directly using the
+     * definition.
+     */
+    @Test
+    public final void testNameFormEqualityReturnsTrueBetweenBuilderAndDefinition() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final NameForm nf1 = schema.getNameForms().iterator().next();
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+
+        // @formatter:off
+        sb2.addNameForm(
+                "( 1.3.6.1.4.1.1466.115.121.1.35 NAME ( 'MyNewForm' ) "
+                + "DESC 'Description of the new form' "
+                + "OC person MUST ( sn $ cn ) "
+                + "MAY ( description $ uid ) "
+                + "X-ORIGIN 'NameFormCheckingTestCase' )", false);
+        // @formatter:on
+
+        final NameForm nf2 = sb2.toSchema().getNameForm("MyNewForm");
+
+        assertThat(nf1.equals(nf2)).isTrue();
+    }
+
+    /**
+     * Compare two same name forms using the equal function. One created by the
+     * name form builder, the other by the schema builder directly using the
+     * definition with different OID.
+     */
+    @Test
+    public final void testNameFormEqualityReturnsFalseBetweenBuilderAndDefinition() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final NameForm nf1 = schema.getNameForms().iterator().next();
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+
+        // @formatter:off
+        sb2.addNameForm(
+                "( 1.3.6.1.4.1.1466.115.121.1.36 NAME ( 'MyNewForm' ) " // OID changed.
+                + "DESC 'Description of the new form' "
+                + "OC person MUST ( sn $ cn ) "
+                + "MAY ( description $ uid ) "
+                + "X-ORIGIN 'NameFormCheckingTestCase' )", false);
+        // @formatter:on
+
+        final NameForm nf2 = sb2.toSchema().getNameForm("MyNewForm");
+        // Equals is only based on the OID.
+        assertThat(nf1.equals(nf2)).isFalse();
+    }
+
+    /**
+     * Validates a name form using an abstract object class instead of an
+     * structural object class and throws an error.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testNameFormValidateDoesntAllowAbstractObjectClass() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("mynewform-oid")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("top")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getNameForms()).isEmpty();
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains(
+                "This object class exists in the schema but is defined as ABSTRACT rather than structural");
+        // output is : The name form description "MyNewForm" is associated with the "top" object class.
+        // This object class exists in the schema but is defined as ABSTRACT rather than structural
+    }
+
+    /**
+     * Creates a name form using the appropriate structural object class.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testNameFormValidateAllowsStructuralObjectClass() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("mynewform-oid")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getNameForms()).isNotEmpty();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        assertThat(schema.getNameForms().iterator().next().getOID()).isEqualTo("mynewform-oid");
+        assertThat(schema.getNameForms().iterator().next().getNames().get(0)).isEqualTo("MyNewForm");
+    }
+
+    /**
+     * Adds multiple attributes... e.g : name form containing multiple
+     * extra-properties, requiredAttributes, optional attributes, names...
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testBuildsANewFormWithMultipleAttributes() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("0.0.1.2.3")
+                .description("multipleAttributes Test description")
+                .names("multipleAttributes")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase2")
+                .requiredAttributes("sn", "cn") // ("cn, sn") is not supported.
+                .requiredAttributes("uid")
+                .optionalAttributes("owner")
+                .optionalAttributes("l")
+                .names("Rock")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms()).isNotEmpty();
+
+        for (final NameForm nf : schema.getNameForms()) {
+
+            assertThat(nf.getDescription()).isEqualTo("multipleAttributes Test description");
+            assertThat(nf.getOID()).isEqualTo("0.0.1.2.3");
+
+            assertThat(nf.getNames().get(0)).isEqualTo("multipleAttributes");
+            assertThat(nf.getNames().get(1)).isEqualTo("Rock");
+            assertThat(nf.getExtraProperties().get("X-ORIGIN").get(0))
+                    .isEqualTo("NameFormCheckingTestCase");
+            assertThat(nf.getExtraProperties().get("X-ORIGIN").get(1)).isEqualTo(
+                    "NameFormCheckingTestCase2");
+
+            assertThat(nf.getStructuralClass().getNameOrOID()).isEqualTo("person");
+
+            // RequiredAttributes is accessible only after validate
+            for (final AttributeType att : nf.getRequiredAttributes()) {
+                assertThat(
+                        att.getNameOrOID().contains("cn") || att.getNameOrOID().contains("sn")
+                                || att.getNameOrOID().contains("uid")).isTrue();
+            }
+            // OptionalAttributes is accessible only after validate
+            for (final AttributeType att : nf.getOptionalAttributes()) {
+                assertThat(att.getNameOrOID().contains("owner") || att.getNameOrOID().contains("l"))
+                        .isTrue();
+            }
+        }
+    }
+
+    /**
+     * Using the schema builder for adding new name forms. Allows methods
+     * chaining.
+     * <p>
+     * e.g : (SchemaBuilder) <code>
+     * scb.addNameForm("1.2.3").build(true).addAttributeType
+     * (...).build(false).addNameForm(...)...etc.
+     * </code>
+     * <p>
+     * N.B : NameForm is validated when the SchemaBuilder is building a Schema.
+     * If the NameForm is not valid, the SchemaBuilder just remove the invalid
+     * NameForm.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testCreatesANewFormUsingChainingMethods() {
+        final Map<String, List<String>> extraProperties = new TreeMap<>();
+        final List<String> extra = new ArrayList<>();
+        extra.add("EntrySchemaCheckingTestCase");
+        extraProperties.put("X-ORIGIN", extra);
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.2.3")
+                .description("NF1's description")
+                .names("theFirstNameForm")
+                .structuralObjectClassOID("person")
+                .extraProperties(extraProperties)
+                .requiredAttributes("uid")
+                .optionalAttributes("sn")
+                .addToSchemaOverwrite()
+            .buildNameForm("4.4.4")
+                .description("NF2's description")
+                .names("theSecondNameForm")
+                .structuralObjectClassOID("person")
+                .extraProperties(extraProperties)
+                .requiredAttributes("uid")
+                .requiredAttributes("sn")
+                .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+
+        // First name form
+        final NameForm first = schema.getNameForm("theFirstNameForm");
+        assertThat(first.getOID()).isEqualTo("1.2.3");
+        assertThat(first.getDescription()).isEqualTo("NF1's description");
+        assertThat(first.getRequiredAttributes()).isNotEmpty();
+        assertThat(first.getOptionalAttributes()).isNotEmpty();
+        assertThat(first.getStructuralClass().getNameOrOID()).isEqualTo("person");
+
+        // Second name form
+        final NameForm second = schema.getNameForm("theSecondNameForm");
+        assertThat(second.getOID()).isEqualTo("4.4.4");
+        assertThat(second.getDescription()).isEqualTo("NF2's description");
+        assertThat(second.getRequiredAttributes()).isNotEmpty();
+        assertThat(second.getOptionalAttributes()).isEmpty();
+    }
+
+    /**
+     * Remove functions uses on names / required attribute.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testCreatesNewFormAndRemovesAttributes() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("0.0.1.2.3")
+                .description("multipleAttributes Test description")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase")
+                .extraProperties("X-ORIGIN", "NameFormCheckingTestCase2")
+                .requiredAttributes("sn", "cn")
+                .requiredAttributes("uid")
+                .optionalAttributes("givenName")
+                .optionalAttributes("l")
+                .names("nameform1")
+                .names("nameform2")
+                .names("nameform3")
+                .removeName("nameform2")
+                .removeRequiredAttribute("cn")
+                .removeOptionalAttribute("l")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getNameForms()).isNotEmpty();
+        final NameForm nf = schema.getNameForms().iterator().next();
+
+        assertThat(nf.getNames()).hasSize(2);
+        assertThat(nf.getNames()).contains("nameform1");
+        assertThat(nf.getNames()).contains("nameform3");
+
+        assertThat(nf.getRequiredAttributes().size()).isEqualTo(2);
+        assertThat(nf.getRequiredAttributes().toString()).contains("'sn'");
+        assertThat(nf.getRequiredAttributes().toString()).contains("uid");
+
+        assertThat(nf.getOptionalAttributes().size()).isEqualTo(1);
+    }
+
+    /**
+     * Trying to remove attributes from a duplicated name form.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testDuplicatesNameFormAndRemovesAttributes() {
+
+        // @formatter:off
+        Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormTestCase", "Forgerock", "extra")
+                .extraProperties("FROM", "NameFormTestCase")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms()).isNotEmpty();
+
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+        assertThat(nf.getRequiredAttributes().size()).isEqualTo(2);
+        assertThat(nf.getOptionalAttributes().size()).isEqualTo(2);
+
+        // @formatter:off.
+        SchemaBuilder sb = new SchemaBuilder(Schema.getCoreSchema());
+        Builder nfBuilder = new Builder(nf, sb)
+                    .names("Dolly")
+                    .oid("1.3.6.1.4.1.1466.115.121.1.36")
+                    .removeOptionalAttribute("uid")
+                    .removeOptionalAttribute("nonExistentUid")
+                    .requiredAttributes("street")
+                    .removeRequiredAttribute("sn")
+                    .removeExtraProperty("X-ORIGIN", "extra")
+                    .removeExtraProperty("X-ORIGIN", "Forgerock")
+                    .removeExtraProperty("FROM");
+
+        // @formatter:on
+        sb.addSchema(schema, true);
+        sb.addSchema(nfBuilder.addToSchemaOverwrite().toSchema(), true);
+        Schema finalSchema =  sb.toSchema();
+
+        assertThat(finalSchema.getNameForms()).isNotEmpty();
+        assertThat(finalSchema.getNameForms().size()).isEqualTo(2);
+        assertThat(finalSchema.getWarnings()).isEmpty();
+
+        final Iterator<NameForm> i = finalSchema.getNameForms().iterator();
+        i.next(); // Jump the first element (== nf)
+        final NameForm dolly = i.next();
+        assertThat(dolly.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.36");
+
+        assertThat(dolly.getRequiredAttributes().size()).isEqualTo(2);
+        assertThat(dolly.getRequiredAttributes().toString()).contains("street");
+        assertThat(dolly.getRequiredAttributes().toString()).contains("cn");
+
+        assertThat(dolly.getOptionalAttributes().size()).isEqualTo(1);
+        assertThat(dolly.getExtraProperties().get("X-ORIGIN").size()).isEqualTo(1);
+        assertThat(dolly.getExtraProperties().get("FROM")).isNull();
+    }
+
+    /**
+     * Clears attributes from a duplicated name form.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testDuplicatesNameFormAndClears() {
+
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final Builder nfb = new Builder("1.3.6.1.4.1.1466.115.121.1.35", sb);
+        nfb.description("Description of the new form")
+            .names("MyNewForm")
+            .structuralObjectClassOID("person")
+            .extraProperties("X-ORIGIN", "NameFormTestCase", "Forgerock", "extra")
+            .extraProperties("FROM", "NameFormTestCase")
+            .requiredAttributes("sn", "cn")
+            .optionalAttributes("description", "uid")
+            .addToSchema();
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms()).isNotEmpty();
+
+        final NameForm nf = schema.getNameForms().iterator().next();
+        assertThat(nf.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.35");
+        assertThat(nf.getRequiredAttributes()).hasSize(2);
+        assertThat(nf.getOptionalAttributes()).hasSize(2);
+        assertThat(nf.getExtraProperties()).hasSize(2);
+
+        sb.buildNameForm(nf)
+            .removeAllNames()
+            .names("Dolly")
+            .removeName("thisOneDoesntExist")
+            .oid("1.3.6.1.4.1.1466.115.121.1.36")
+            .removeAllOptionalAttributes()
+            .removeAllExtraProperties()
+            .removeAllRequiredAttributes()
+            .requiredAttributes("businessCategory")
+            .addToSchemaOverwrite();
+        schema = sb.toSchema();
+        assertThat(schema.getNameForms()).isNotEmpty();
+        assertThat(schema.getNameForms().size()).isEqualTo(2);
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final Iterator<NameForm> i = schema.getNameForms().iterator();
+        i.next(); // Jump the first element (== nf)
+        final NameForm dolly = i.next();
+        assertThat(dolly.getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.36");
+
+        assertThat(dolly.getNames().size()).isEqualTo(1);
+        assertThat(dolly.getNames().get(0)).isEqualTo("Dolly");
+        assertThat(dolly.getRequiredAttributes().size()).isEqualTo(1);
+        assertThat(dolly.getRequiredAttributes().iterator().next().getOID()).isEqualTo("2.5.4.15");
+        assertThat(dolly.getRequiredAttributes().iterator().next().getNameOrOID()).isEqualTo("businessCategory");
+        assertThat(dolly.getOptionalAttributes()).isEmpty();
+        assertThat(dolly.getExtraProperties()).isEmpty();
+    }
+
+    /**
+     * Adds several name forms to the same schema builder.
+     *
+     * @throws SchemaException
+     */
+    @Test
+    public final void testAddsSeveralFormsToSchemaBuilder() {
+
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.35")
+                .description("Description of the new form")
+                .names("MyNewForm")
+                .structuralObjectClassOID("person")
+                .extraProperties("X-ORIGIN", "NameFormTestCase", "Forgerock", "extra")
+                .requiredAttributes("sn", "cn")
+                .optionalAttributes("description", "uid")
+                .addToSchema()
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.36")
+                .description("Description of the second form")
+                .names("SecondForm")
+                .structuralObjectClassOID("organization")
+                .extraProperties("X-ORIGIN", "NameFormTestCase2")
+                .requiredAttributes("name")
+                .optionalAttributes("owner")
+                .addToSchema()
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.37")
+                .description("Description of the third form")
+                .names("ThirdForm")
+                .structuralObjectClassOID("groupOfNames")
+                .extraProperties("X-ORIGIN", "NameFormTestCase3", "ForgeRock")
+                .requiredAttributes("sn", "l")
+                .optionalAttributes("description", "uid")
+                .description("Description of the third form")
+                .addToSchema()
+                // we overwritten the third name form.
+            .buildNameForm("1.3.6.1.4.1.1466.115.121.1.37")
+                .names("ThirdFormOverwritten")
+                .structuralObjectClassOID("groupOfNames")
+                .extraProperties("X-ORIGIN", "RFC 2252")
+                .requiredAttributes("sn", "l")
+                .optionalAttributes("description", "uid")
+                .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameForms().size()).isEqualTo(3);
+        assertThat(schema.getNameForm("MyNewForm").getOID()).isEqualTo(
+                "1.3.6.1.4.1.1466.115.121.1.35");
+        assertThat(schema.getNameForm("SecondForm").getOID()).isEqualTo(
+                "1.3.6.1.4.1.1466.115.121.1.36");
+        // The third form is completely overwritten.
+        assertThat(schema.getNameForm("ThirdFormOverwritten").getOID()).isEqualTo(
+                "1.3.6.1.4.1.1466.115.121.1.37");
+        assertThat(schema.getNameForm("ThirdFormOverwritten").getDescription()).isEmpty();
+        assertThat(schema.getNameForm("ThirdFormOverwritten").getExtraProperties().get("X-ORIGIN").get(0))
+                .isEqualTo("RFC 2252");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..05fa330
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringEqualityMatchingRuleTest.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_NUMERIC_STRING_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the NumericStringEqualityMatchingRule. */
+@Test
+public class NumericStringEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // non-numeric characters are tolerated and treated as significant
+            {"A2B1", "A2B1", ConditionResult.TRUE },
+            {"A2B1", "A2b1", ConditionResult.FALSE },
+            {"1234567890", "1234567890", ConditionResult.TRUE },
+            {" 1234567890  ", "1234567890", ConditionResult.TRUE },
+            {" 123   4567890  ", "1234567890", ConditionResult.TRUE },
+            {"1234", "5678", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_NUMERIC_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..8aa4b05
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringOrderingMatchingRuleTest.java
@@ -0,0 +1,55 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_NUMERIC_STRING_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the NumericStringOrderingMatchingRule. */
+@Test
+public class NumericStringOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            // non-numeric characters are tolerated and treated as significant
+            {"jfhslur", "JFH", 1},
+            {"123AB", "2", -1},
+            {"1", "999999999999999999999", -1},
+            {"1", "9",  -1},
+            {"10", "9",  -1},
+            {"1 0", "9",  -1},
+            {"1", " 1 ", 0},
+            {"0", "1",  -1},
+            {"1", "0",  1},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_NUMERIC_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleTest.java
new file mode 100644
index 0000000..86fefb1
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/NumericStringSubstringMatchingRuleTest.java
@@ -0,0 +1,100 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_NUMERIC_STRING_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the NumericStringSubstringMatchingRule. */
+@Test
+public class NumericStringSubstringMatchingRuleTest extends SubstringMatchingRuleTest {
+
+    @Override
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringFinalMatchData")
+    public Object[][] createSubstringFinalMatchData() {
+        return new Object[][] {
+            {"123456789",  "123456789", ConditionResult.TRUE },
+            {"12 345 6789",  "123456789", ConditionResult.TRUE },
+            {"123456789",  "456789", ConditionResult.TRUE },
+            {"123456789",  "567", ConditionResult.FALSE },
+            {"123456789",  "123", ConditionResult.FALSE },
+            {"123456789",  " ", ConditionResult.TRUE },
+            {"123456789",  "0789", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringInitialMatchData")
+    public Object[][] createSubstringInitialMatchData() {
+        return new Object[][] {
+            { "123456789",  "12345678",   ConditionResult.TRUE },
+            { "123456789",  "2345678",    ConditionResult.FALSE },
+            { "123456789",  "1234",       ConditionResult.TRUE },
+            { "123456789",  "1",          ConditionResult.TRUE },
+            { "123456789",  "678",        ConditionResult.FALSE },
+            { "123456789",  "2",          ConditionResult.FALSE },
+            { "123456789",  " ",          ConditionResult.TRUE },
+            { "123456789",  "123456789",  ConditionResult.TRUE },
+            { "123456789",  "1234567890", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "substringMiddleMatchData")
+    public Object[][] createSubstringMiddleMatchData() {
+        return new Object[][] {
+            // The matching rule requires ordered non overlapping substrings.
+            // Issue #730 was not valid.
+            { "123456789", new String[] {"123", "234", "567", "789"}, ConditionResult.FALSE },
+            { "123456789", new String[] {"123", "234"}, ConditionResult.FALSE },
+            { "123456789", new String[] {"567", "234"}, ConditionResult.FALSE },
+            { "123456789", new String[] {"123", "456"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"123"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"456"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"789"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"123456789"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"1234567890"}, ConditionResult.FALSE },
+            { "123456789", new String[] {"9"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"1"}, ConditionResult.TRUE },
+            { "123456789", new String[] {"0"}, ConditionResult.FALSE },
+            { "123456789", new String[] {"    "}, ConditionResult.TRUE },
+            { "123456789", new String[] {"0123"}, ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(SMR_NUMERIC_STRING_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectClassBuilderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectClassBuilderTestCase.java
new file mode 100644
index 0000000..6269575
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectClassBuilderTestCase.java
@@ -0,0 +1,198 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.List;
+import java.util.Set;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static java.util.Collections.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.MapAssert.*;
+import static org.forgerock.opendj.ldap.schema.ObjectClassType.*;
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+public class ObjectClassBuilderTestCase extends AbstractSchemaTestCase {
+
+    @DataProvider
+    Object[][] validObjectClasses() {
+        // OID, obsolete, names, optional attributes, required attributes,
+        // superior object classes, type, description,
+        // extra property name, extra property values, overwrite
+        return new Object[][] {
+            // Basic object class
+            { "1.2.3.4", false, singletonList("MyObjectClass"), emptySet(), singleton("cn"),
+                singleton(TOP_OBJECTCLASS_NAME), STRUCTURAL, "MyObjectClass description.", "New extra property",
+                "New extra value", false },
+            // Allowed overrides existing core schema object class groupOfNames
+            { "2.5.6.9", false, singletonList("groupOfFirstNames"), emptySet(), singleton("cn"),
+                singleton(TOP_OBJECTCLASS_NAME), AUXILIARY, "MyObjectClass description.", "New extra property",
+                "New extra value", true },
+            // No name provided, should be validated
+            { "1.2.3.4", false, emptyList(), singleton("name"), singleton("cn"),
+                singleton(TOP_OBJECTCLASS_NAME), STRUCTURAL, "MyObjectClass description.", "New extra property",
+                "New extra value", false },
+            // Empty description, should be validated
+            { "1.2.3.4", false, emptyList(), singleton("name"), singleton("cn"),
+                singleton(TOP_OBJECTCLASS_NAME), STRUCTURAL, "", "New extra property",
+                "New extra value", false },
+        };
+    }
+
+    @Test(dataProvider = "validObjectClasses")
+    public void testValidOCBuilder(final String oid, final boolean isObsolete, final List<String> names,
+            final Set<String> optionalAttributeOIDs, final Set<String> requiredAttributesOIDs,
+            final Set<String> superiorClassOIDs, final ObjectClassType type, final String description,
+            final String extraPropertyName, final String extraPropertyValue, final boolean overwrite)
+            throws Exception {
+        final ObjectClass.Builder ocBuilder = new SchemaBuilder(getCoreSchema())
+            .buildObjectClass(oid)
+            .description(description)
+            .obsolete(isObsolete)
+            .names(names)
+            .superiorObjectClasses(superiorClassOIDs)
+            .requiredAttributes(requiredAttributesOIDs)
+            .optionalAttributes(optionalAttributeOIDs)
+            .type(type)
+            .extraProperties(extraPropertyName, extraPropertyValue);
+
+        final Schema schema = overwrite ? ocBuilder.addToSchemaOverwrite().toSchema()
+                                        : ocBuilder.addToSchema().toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final ObjectClass oc = schema.getObjectClass(oid);
+        assertThat(oc).isNotNull();
+        assertThat(oc.getOID()).isEqualTo(oid);
+        assertThat(oc.getDescription()).isEqualTo(description);
+        assertThat(oc.isObsolete()).isEqualTo(isObsolete);
+        assertThat(oc.getNames()).containsOnly(names.toArray());
+        assertSchemaElementsContainsAll(oc.getSuperiorClasses(), superiorClassOIDs);
+        assertSchemaElementsContainsAll(oc.getRequiredAttributes(), requiredAttributesOIDs);
+        assertSchemaElementsContainsAll(oc.getOptionalAttributes(), optionalAttributeOIDs);
+        assertThat(oc.getObjectClassType()).isEqualTo(type);
+        assertThat(oc.getExtraProperties()).includes(entry(extraPropertyName, singletonList(extraPropertyValue)));
+    }
+
+    @Test
+    public void testOCBuilderDefaultValues() throws Exception {
+        final SchemaBuilder sb = new SchemaBuilder(getCoreSchema());
+        final ObjectClass.Builder ocBuilder = sb.buildObjectClass("1.1.1.42")
+                .description("Default object class")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .names("defaultObjectClass");
+        final Schema schema = ocBuilder.addToSchema().toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        final ObjectClass oc = schema.getObjectClass("defaultObjectClass");
+        assertThat(oc).isNotNull();
+        assertThat(oc.getOID()).isEqualTo("1.1.1.42");
+        assertThat(oc.getDescription()).isEqualTo("Default object class");
+        assertThat(oc.isObsolete()).isFalse();
+        assertThat(oc.getNames()).containsOnly("defaultObjectClass");
+        assertSchemaElementsContainsAll(oc.getSuperiorClasses(), TOP_OBJECTCLASS_NAME);
+        final Set<AttributeType> topReqAttrs = schema.getObjectClass(TOP_OBJECTCLASS_NAME).getRequiredAttributes();
+        assertThat(oc.getRequiredAttributes()).containsOnly(topReqAttrs.toArray());
+        assertThat(oc.getOptionalAttributes()).isEmpty();
+        assertThat(oc.getObjectClassType()).isEqualTo(STRUCTURAL);
+        assertThat(oc.getExtraProperties()).isEmpty();
+    }
+
+    @Test
+    public void testOCBuilderCopyConstructor() throws Exception {
+        final SchemaBuilder sb = new SchemaBuilder(getCoreSchema());
+        final ObjectClass.Builder ocBuilder = sb.buildObjectClass("1.1.1.42")
+                .description("Object class to duplicate")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .names("ObjectClassToDuplicate")
+                .requiredAttributes("name");
+        final Schema schema = ocBuilder.addToSchema().toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        sb.buildObjectClass(schema.getObjectClass("ObjectClassToDuplicate"))
+                .oid("1.1.1.43")
+                .names("Copy")
+                .obsolete(true)
+                .addToSchemaOverwrite();
+        final Schema schemaCopy = sb.toSchema();
+        assertThat(schemaCopy.getWarnings()).isEmpty();
+
+        final ObjectClass ocCopy = schemaCopy.getObjectClass("Copy");
+        assertThat(ocCopy).isNotNull();
+        assertThat(ocCopy.getOID()).isEqualTo("1.1.1.43");
+        assertThat(ocCopy.getDescription()).isEqualTo("Object class to duplicate");
+        assertThat(ocCopy.isObsolete()).isTrue();
+        assertThat(ocCopy.getNames()).containsOnly("ObjectClassToDuplicate", "Copy");
+        assertSchemaElementsContainsAll(ocCopy.getSuperiorClasses(), TOP_OBJECTCLASS_NAME);
+        assertSchemaElementsContainsAll(ocCopy.getRequiredAttributes(), "name");
+        assertThat(ocCopy.getOptionalAttributes()).isEmpty();
+        assertThat(ocCopy.getObjectClassType()).isEqualTo(STRUCTURAL);
+        assertThat(ocCopy.getExtraProperties()).isEmpty();
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testOCBuilderDoesNotAllowOverwrite() throws Exception {
+        final ObjectClass.Builder ocBuilder = new SchemaBuilder(getCoreSchema())
+            .buildObjectClass("2.5.6.9")
+            .description("MyObjectClass description")
+            .names("groupOfFirstNames")
+            .requiredAttributes("cn")
+            .type(AUXILIARY)
+            .extraProperties("New extra property", "New extra value");
+
+        ocBuilder.addToSchema().toSchema();
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testOCBuilderDoesNotAllowEmptyOID() throws Exception {
+        final ObjectClass.Builder ocBuilder = new SchemaBuilder(getCoreSchema())
+            .buildObjectClass("")
+            .description("MyObjectClass description")
+            .names("MyObjectClass")
+            .requiredAttributes("cn")
+            .extraProperties("New extra property", "New extra value");
+
+        ocBuilder.addToSchema().toSchema();
+    }
+
+    private void assertSchemaElementsContainsAll(final Set<? extends SchemaElement> elements,
+            final Set<String> namesOrOIDs) throws Exception {
+        assertSchemaElementsContainsAll(elements, namesOrOIDs.toArray(new String[namesOrOIDs.size()]));
+    }
+
+
+    private void assertSchemaElementsContainsAll(final Set<? extends SchemaElement> elements,
+            final String... namesOrOIDs) throws Exception {
+        for (final String nameOrOID : namesOrOIDs) {
+            assertThat(assertSchemaElementsContains(elements, nameOrOID)).isTrue();
+        }
+    }
+
+    private boolean assertSchemaElementsContains(final Set<? extends SchemaElement> elements, final String nameOrOID) {
+        for (final SchemaElement element : elements) {
+            final String oid = element instanceof AttributeType ? ((AttributeType) element).getNameOrOID()
+                                                            : ((ObjectClass) element).getNameOrOID();
+            if (oid.equals(nameOrOID)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OrderingMatchingRuleTest.java
new file mode 100644
index 0000000..33f8034
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OrderingMatchingRuleTest.java
@@ -0,0 +1,101 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Ordering matching rule tests.
+ */
+@SuppressWarnings("javadoc")
+public abstract class OrderingMatchingRuleTest extends AbstractSchemaTestCase {
+    /**
+     * Create data for the OrderingMatchingRulesInvalidValues test.
+     *
+     * @return The data for the OrderingMatchingRulesInvalidValues test.
+     */
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public abstract Object[][] createOrderingMatchingRuleInvalidValues();
+
+    /**
+     * Create data for the OrderingMatchingRules test.
+     *
+     * @return The data for the OrderingMatchingRules test.
+     */
+    @DataProvider(name = "Orderingmatchingrules")
+    public abstract Object[][] createOrderingMatchingRuleTestData();
+
+    /**
+     * Test the comparison of valid values.
+     */
+    @Test(dataProvider = "Orderingmatchingrules")
+    public void orderingMatchingRules(final String value1, final String value2, final int result)
+            throws Exception {
+        // Make sure that the specified class can be instantiated as a task.
+        final MatchingRule ruleInstance = getRule();
+
+        final ByteString normalizedValue1 =
+                ruleInstance.normalizeAttributeValue(ByteString.valueOfUtf8(value1));
+        final ByteString normalizedValue2 =
+                ruleInstance.normalizeAttributeValue(ByteString.valueOfUtf8(value2));
+
+        // Test the comparator
+        final int comp = normalizedValue1.compareTo(normalizedValue2);
+        if (comp == 0) {
+            Assert.assertEquals(comp, result);
+        } else if (comp > 0) {
+            Assert.assertTrue(result > 0);
+        } else if (comp < 0) {
+            Assert.assertTrue(result < 0);
+        }
+
+        Assertion a = ruleInstance.getGreaterOrEqualAssertion(ByteString.valueOfUtf8(value2));
+        Assert.assertEquals(a.matches(normalizedValue1), ConditionResult.valueOf(result >= 0));
+
+        a = ruleInstance.getLessOrEqualAssertion(ByteString.valueOfUtf8(value2));
+        Assert.assertEquals(a.matches(normalizedValue1), ConditionResult.valueOf(result <= 0));
+
+        a = ruleInstance.getAssertion(ByteString.valueOfUtf8(value2));
+        Assert.assertEquals(a.matches(normalizedValue1), ConditionResult.valueOf(result < 0));
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "OrderingMatchingRuleInvalidValues")
+    public void orderingMatchingRulesInvalidValues(final String value) throws Exception {
+        // Make sure that the specified class can be instantiated as a task.
+        final MatchingRule ruleInstance = getRule();
+
+        // normalize the 2 provided values
+        ruleInstance.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Get the Ordering matching Rules that is to be tested.
+     *
+     * @return The Ordering matching Rules that is to be tested.
+     */
+    protected abstract MatchingRule getRule();
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxTest.java
new file mode 100644
index 0000000..31661ca
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/OtherMailboxSyntaxTest.java
@@ -0,0 +1,37 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_OTHER_MAILBOX_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Other mailbox syntax tests. */
+@Test
+public class OtherMailboxSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "MyMail$Mymailbox", true }, { "MyMailMymailbox", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_OTHER_MAILBOX_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java
new file mode 100644
index 0000000..41baa50
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PartialDateAndTimeMatchingRuleTestCase.java
@@ -0,0 +1,180 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import org.forgerock.util.time.TimeService;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class PartialDateAndTimeMatchingRuleTestCase extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            // too short
+            { "1Z" },
+            // bad year
+            { "201a0630Z" },
+            // bad month
+            { "20141330Z" },
+            // bad day
+            { "20140635Z" },
+            // bad hour
+            { "20140630351010Z" },
+            // bad minute
+            { "20140630108810Z" },
+            // bad second
+            { "20140630101080Z" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {
+            { " " },
+            { "bla" },
+            // invalid time unit values
+            { "-1Y03M11D12h48m32s" },
+            { "0Y03M11D12h48m32s" },
+            { "2014Y-1M11D12h48m32s" },
+            { "2014Y0M11D12h48m32s" },
+            { "2014Y13M11D12h48m32s" },
+            { "2014Y03M-1D12h48m32s" },
+            { "2014Y03M0D12h48m32s" },
+            { "2014Y13M32D12h48m32s" },
+            { "2014Y03M11D-1h48m32s" },
+            { "2014Y03M11D24h48m32s" },
+            { "2014Y03M11D12h-1m32s" },
+            { "2014Y03M11D12h60m32s" },
+            { "2014Y03M11D12h48m-1s" },
+            { "2014Y03M11D12h48m61s" },
+            // duplicate each time unit
+            { "1Y2014Y03M11D12h" },
+            { "2014Y1M03M11D12h" },
+            { "2014Y03M1D11D12h" },
+            { "2014Y03M11D1h12h" },
+            { "2014Y03M11D12h1m48m" },
+            { "2014Y03M11D12h48m1s32s" },
+            // February and non leap years
+            { "2014Y02M29D" },
+            { "1800Y02M29D" },
+            { "2000Y02M30D" },
+            { "2000Y02M31D" },
+            // 31st of months
+            { "2012Y04M31D" },
+            { "2012Y06M31D" },
+            { "2012Y09M31D" },
+            { "2012Y11M31D" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(TimeService.SYSTEM.now());
+        final Date nowDate = calendar.getTime();
+        calendar.add(Calendar.MONTH, 1);
+        final Date oneMonthAheadDate = calendar.getTime();
+
+        final SimpleDateFormat partialTimeUpToSeconds = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'mm'm'ss's'");
+        final SimpleDateFormat generalizedTimeUpToSeconds = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
+        final SimpleDateFormat partialTimeUpToMinutes = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'mm'm'");
+        final SimpleDateFormat generalizedTimeUpToMinutes = new SimpleDateFormat("yyyyMMddHHmm'Z'");
+        final SimpleDateFormat partialTimeUpToHours = new SimpleDateFormat("yyyy'Y'MM'M'dd'D'HH'h'");
+        final SimpleDateFormat generalizedTimeUpToHours = new SimpleDateFormat("yyyyMMddHH'Z'");
+
+        return new Object[][] {
+            // expect 3 values : attribute value, assertion value, result
+
+            // use now date and one month ahead dates
+            { generalizedTimeUpToSeconds.format(nowDate), partialTimeUpToSeconds.format(nowDate),
+                ConditionResult.TRUE },
+            { generalizedTimeUpToSeconds.format(oneMonthAheadDate), partialTimeUpToSeconds.format(oneMonthAheadDate),
+                ConditionResult.TRUE },
+            { generalizedTimeUpToMinutes.format(nowDate), partialTimeUpToMinutes.format(nowDate),
+                ConditionResult.TRUE },
+            { generalizedTimeUpToMinutes.format(oneMonthAheadDate), partialTimeUpToMinutes.format(oneMonthAheadDate),
+                ConditionResult.TRUE },
+            { generalizedTimeUpToHours.format(nowDate), partialTimeUpToHours.format(nowDate),
+                ConditionResult.TRUE },
+            { generalizedTimeUpToHours.format(oneMonthAheadDate), partialTimeUpToHours.format(oneMonthAheadDate),
+                ConditionResult.TRUE },
+            // 29th of months and leap years
+            { "20120329120000Z", "2012Y03M29D", ConditionResult.TRUE },
+            { "20120229120000Z", "2012Y02M29D", ConditionResult.TRUE },
+            { "20000229120000Z", "2000Y02M29D", ConditionResult.TRUE },
+            // Generalized time implementation does not allow leap seconds
+            // because Java does not support them. Apparently, it never will support them:
+            // @see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4272347
+            // leap seconds are allowed, even though no formula exists to validate them
+            { "20120630235959Z", "2012Y06M30D23h59m60s", ConditionResult.FALSE },
+            // no match
+            { "20111231235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            { "20121031235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            { "20121230235930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            { "20121231225930Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            { "20121231235830Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            { "20121231235829Z", "2012Y12M31D23h59m30s", ConditionResult.FALSE },
+            // 30th of months
+            { "19820930120000Z", "1982Y09M30D", ConditionResult.TRUE },
+            // 31st of months
+            { "20120131120000Z", "2012Y01M31D", ConditionResult.TRUE },
+            { "20120331120000Z", "2012Y03M31D", ConditionResult.TRUE },
+            { "20120531120000Z", "2012Y05M31D", ConditionResult.TRUE },
+            { "20120731120000Z", "2012Y07M31D", ConditionResult.TRUE },
+            { "20120831120000Z", "2012Y08M31D", ConditionResult.TRUE },
+            { "20121031120000Z", "2012Y10M31D", ConditionResult.TRUE },
+            { "20121231120000Z", "2012Y12M31D", ConditionResult.TRUE },
+            // Only single time units
+            { "20121231123000Z", "2012Y", ConditionResult.TRUE },
+            { "20121231123000Z", "2012Y12M", ConditionResult.TRUE },
+            { "20121231123000Z", "2012Y31D", ConditionResult.TRUE },
+            { "20121231123000Z", "2012Y12h", ConditionResult.TRUE },
+            { "20121231123000Z", "2012Y30m", ConditionResult.TRUE },
+            { "20121231123000Z", "2012Y0s", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return CoreSchema.getInstance().getMatchingRule(SchemaConstants.MR_PARTIAL_DATE_AND_TIME_OID);
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        Assertion assertion = getRule().getAssertion(ByteString.valueOfUtf8("2012Y"));
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+        assertThat(indexQuery).matches(
+                "intersect\\[exactMatch\\(" + MR_PARTIAL_DATE_AND_TIME_NAME + ", value=='[0-9A-Z ]*'\\)\\]");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..1ea2f06
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/PresentationAddressEqualityMatchingRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_PRESENTATION_ADDRESS_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the PresentationAddressEqualityMatchingRule. */
+public class PresentationAddressEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            {"   ", " ", ConditionResult.TRUE },
+            {"string", "string", ConditionResult.TRUE },
+            {"STRING", "string", ConditionResult.TRUE },
+            {"some string", "some other string", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_PRESENTATION_ADDRESS_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..63ec174
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ProtocolInformationEqualityMatchingRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_PROTOCOL_INFORMATION_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the ProtocolInformationEqualityMatchingRule. */
+public class ProtocolInformationEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            {"   ", " ", ConditionResult.TRUE },
+            {"string", "string", ConditionResult.TRUE },
+            {"STRING", "string", ConditionResult.TRUE },
+            {"some string", "some other string", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_PROTOCOL_INFORMATION_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RegexSyntaxTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RegexSyntaxTestCase.java
new file mode 100644
index 0000000..b538354
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RegexSyntaxTestCase.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.regex.Pattern;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Regex syntax tests. */
+@SuppressWarnings("javadoc")
+public class RegexSyntaxTestCase extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "invalid regex", false }, { "host:0.0.0", true }, };
+    }
+
+    @Test
+    public void testDecode() {
+        // This should fail due to invalid pattern.
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.1.1 DESC 'Host and Port in the format of HOST:PORT' "
+                + " X-PATTERN '^[a-z-A-Z]+:[0-9.]+\\d$' )", true);
+        builder.toSchema();
+    }
+
+    @Test
+    public void testInvalidPattern() {
+        // This should fail due to invalid pattern.
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.1.1 DESC 'Host and Port in the format of HOST:PORT' "
+                + " X-PATTERN '^[a-z-A-Z+:[0-@.]+\\d$' )", true);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Override
+    protected Syntax getRule() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addPatternSyntax("1.1.1", "Host and Port in the format of HOST:PORT", Pattern
+                .compile("^[a-z-A-Z]+:[0-9.]+\\d$"), false);
+        return builder.toSchema().getSyntax("1.1.1");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java
new file mode 100644
index 0000000..2cf6082
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeGreaterThanMatchingRuleTest.java
@@ -0,0 +1,116 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import org.forgerock.util.time.TimeService;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class RelativeTimeGreaterThanMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            // too short
+            { "1Z" },
+            // bad year
+            { "201a0630Z" },
+            // bad month
+            { "20141330Z" },
+            // bad day
+            { "20140635Z" },
+            // bad hour
+            { "20140630351010Z" },
+            // bad minute
+            { "20140630108810Z" },
+            // bad second
+            { "20140630101088Z" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {
+            { " " },
+            { "bla" },
+            { "-30b" },
+            { "-30ms" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(TimeService.SYSTEM.now());
+        final Date nowDate = calendar.getTime();
+        calendar.add(Calendar.MONTH, 1);
+        final Date oneMonthAheadDate = calendar.getTime();
+
+        final String nowGT = GeneralizedTime.valueOf(nowDate).toString();
+        final String oneMonthAheadGT = GeneralizedTime.valueOf(oneMonthAheadDate).toString();
+
+        return new Object[][] {
+            // attribute value, assertion value, result
+            { oneMonthAheadGT, "1", ConditionResult.TRUE },
+            { oneMonthAheadGT, "+1s", ConditionResult.TRUE },
+            { oneMonthAheadGT, "+1h", ConditionResult.TRUE },
+            { oneMonthAheadGT, "+1m", ConditionResult.TRUE },
+            { nowGT, "-30d", ConditionResult.TRUE },
+            { nowGT, "-30w", ConditionResult.TRUE },
+            { nowGT, "-30m", ConditionResult.TRUE },
+            { nowGT, "-30s", ConditionResult.TRUE },
+
+            { oneMonthAheadGT, "6w", ConditionResult.FALSE },
+            { nowGT, "1d", ConditionResult.FALSE },
+            { nowGT, "10s", ConditionResult.FALSE },
+            { nowGT, "+1h", ConditionResult.FALSE },
+            { nowGT, "+1m", ConditionResult.FALSE },
+            { nowGT, "+1w", ConditionResult.FALSE },
+            // OPENDJ-2397 - dates before 1970 have negative ms.
+            {"19000101010203Z", "1d", ConditionResult.FALSE},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return CoreSchema.getInstance().getMatchingRule(SchemaConstants.OMR_RELATIVE_TIME_GREATER_THAN_OID);
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        Assertion assertion = getRule().getAssertion(ByteString.valueOfUtf8("+5m"));
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+        assertThat(indexQuery).startsWith("rangeMatch(" + EMR_GENERALIZED_TIME_NAME + ", '")
+                .endsWith("' < value < '')");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java
new file mode 100644
index 0000000..aa35404
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/RelativeTimeLessThanMatchingRuleTest.java
@@ -0,0 +1,116 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.forgerock.opendj.ldap.Assertion;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+import org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.FakeIndexQueryFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import org.forgerock.util.time.TimeService;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.AbstractSubstringMatchingRuleImplTest.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+
+@SuppressWarnings("javadoc")
+@Test
+public class RelativeTimeLessThanMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            // too short
+            { "1Z" },
+            // bad year
+            { "201a0630Z" },
+            // bad month
+            { "20141330Z" },
+            // bad day
+            { "20140635Z" },
+            // bad hour
+            { "20140630351010Z" },
+            // bad minute
+            { "20140630108810Z" },
+            // bad second
+            { "20140630101088Z" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAssertionValues")
+    public Object[][] createMatchingRuleInvalidAssertionValues() {
+        return new Object[][] {
+            { " " },
+            { "bla" },
+            { "-30b" },
+            { "-30ms" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(TimeService.SYSTEM.now());
+        final Date nowDate = calendar.getTime();
+        calendar.add(Calendar.MONTH, 1);
+        final Date oneMonthAheadDate = calendar.getTime();
+
+        final String nowGT = GeneralizedTime.valueOf(nowDate).toString();
+        final String oneMonthAheadGT = GeneralizedTime.valueOf(oneMonthAheadDate).toString();
+
+        return new Object[][] {
+            // attribute value, assertion value, result
+            { oneMonthAheadGT, "6w", ConditionResult.TRUE },
+            { oneMonthAheadGT, "2400h", ConditionResult.TRUE },
+            { nowGT, "1d", ConditionResult.TRUE },
+            { nowGT, "10s", ConditionResult.TRUE },
+            { nowGT, "+1h", ConditionResult.TRUE },
+            { nowGT, "+1m", ConditionResult.TRUE },
+            { nowGT, "+1w", ConditionResult.TRUE },
+
+            { oneMonthAheadGT, "1", ConditionResult.FALSE },
+            { oneMonthAheadGT, "+30s", ConditionResult.FALSE },
+            { oneMonthAheadGT, "+2h", ConditionResult.FALSE },
+            { oneMonthAheadGT, "+1m", ConditionResult.FALSE },
+            { nowGT, "-1d", ConditionResult.FALSE },
+            { nowGT, "-2w", ConditionResult.FALSE },
+            { nowGT, "-10m", ConditionResult.FALSE },
+            { nowGT, "-1s", ConditionResult.FALSE },
+            // OPENDJ-2397 - dates before 1970 have negative ms.
+            {"19000101010203Z", "1d", ConditionResult.TRUE},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return CoreSchema.getInstance().getMatchingRule(SchemaConstants.OMR_RELATIVE_TIME_LESS_THAN_OID);
+    }
+
+    @Test
+    public void testCreateIndexQuery() throws Exception {
+        Assertion assertion = getRule().getAssertion(ByteString.valueOfUtf8("+5m"));
+
+        String indexQuery = assertion.createIndexQuery(new FakeIndexQueryFactory(newIndexingOptions(), false));
+        assertThat(indexQuery).startsWith("rangeMatch(" + EMR_GENERALIZED_TIME_NAME + ", '' < value < '");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTestCase.java
new file mode 100644
index 0000000..953c591
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTestCase.java
@@ -0,0 +1,2340 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test SchemaBuilder. */
+@SuppressWarnings("javadoc")
+public class SchemaBuilderTestCase extends AbstractSchemaTestCase {
+
+    /**
+     * Tests that schema validation resolves dependencies between parent/child
+     * attribute types regardless of the order in which they were added.
+     */
+    @Test
+    public void attributeTypeDependenciesChildThenParent() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                        .addAttributeType("( childtype-oid NAME 'childtype' SUP parenttype )",
+                                false)
+                        .addAttributeType(
+                                "( parenttype-oid NAME 'parenttype' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+                                false).toSchema();
+        assertThat(schema.getAttributeType("childtype").getSyntax()).isNotNull();
+        assertThat(schema.getAttributeType("childtype").getSyntax().getOID()).isEqualTo(
+                "1.3.6.1.4.1.1466.115.121.1.15");
+    }
+
+    /**
+     * Tests that schema validation resolves dependencies between parent/child
+     * attribute types regardless of the order in which they were added.
+     */
+    @Test
+    public void attributeTypeDependenciesParentThenChild() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                        .addAttributeType(
+                                "( parenttype-oid NAME 'parenttype' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+                                false).addAttributeType(
+                                "( childtype-oid NAME 'childtype' SUP parenttype )", false)
+                        .toSchema();
+        assertThat(schema.getAttributeType("childtype").getSyntax()).isNotNull();
+        assertThat(schema.getAttributeType("childtype").getSyntax().getOID()).isEqualTo(
+                "1.3.6.1.4.1.1466.115.121.1.15");
+    }
+
+    /** Tests that attribute types must have a syntax or a superior when strict. */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void attributeTypeNoSuperiorNoSyntaxWhenStrict() {
+        new SchemaBuilder(Schema.getCoreSchema())
+                .setOption(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX, false)
+                .addAttributeType("( parenttype-oid NAME 'parenttype' )", false);
+    }
+
+    /** Tests that attribute types do not have to have a syntax or a superior when leniant. */
+    @Test
+    public void attributeTypeNoSuperiorNoSyntaxWhenLeniant() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .addAttributeType("( parenttype-oid NAME 'parenttype' )", false)
+                .toSchema();
+        assertThat(schema.getAttributeType("parenttype").getSyntax()).isNotNull();
+        assertThat(schema.getAttributeType("parenttype").getSyntax().getOID())
+                .isEqualTo("1.3.6.1.4.1.1466.115.121.1.40");
+    }
+
+    /**
+     * Tests that schema validation handles validation failures for superior
+     * attribute types regardless of the order.
+     */
+    @Test
+    public void attributeTypeSuperiorFailureChildThenParent() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).addAttributeType(
+                        "( childtype-oid NAME 'childtype' SUP parenttype )", false)
+                        .addAttributeType("( parenttype-oid NAME 'parenttype' SUP xxx )", false)
+                        .toSchema();
+
+        try {
+            schema.getAttributeType("childtype");
+            fail("childtype should not be in the schema because its parent is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+
+        try {
+            schema.getAttributeType("parenttype");
+            fail("parenttype should not be in the schema because it is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+    }
+
+    /**
+     * Tests that schema validation handles validation failures for superior
+     * attribute types regardless of the order.
+     */
+    @Test
+    public void attributeTypeSuperiorFailureParentThenChild() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).addAttributeType(
+                        "( parenttype-oid NAME 'parenttype' SUP xxx )", false).addAttributeType(
+                        "( childtype-oid NAME 'childtype' SUP parenttype )", false).toSchema();
+
+        try {
+            schema.getAttributeType("childtype");
+            fail("childtype should not be in the schema because its parent is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+
+        try {
+            schema.getAttributeType("parenttype");
+            fail("parenttype should not be in the schema because it is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+    }
+
+    /** Test for OPENDJ-156: Errors when parsing collective attribute definitions. */
+    @Test
+    public void collectiveAttribute() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).addAttributeType(
+                        "( 2.5.4.11.1 NAME 'c-ou' "
+                                + "SUP ou SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 "
+                                + "COLLECTIVE X-ORIGIN 'RFC 3671' )", false).toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * Tests that it is possible to create a schema which is an exact copy of
+     * another and take advantage of copy on write.
+     */
+    @Test
+    public void copyOnWriteNoChanges() {
+        final Schema baseSchema = Schema.getCoreSchema();
+        final Schema schema = new SchemaBuilder(baseSchema).toSchema();
+
+        assertThat(schema).isSameAs(baseSchema);
+    }
+
+    /** Tests that it is possible to create a schema which is based on another. */
+    @Test
+    public void copyOnWriteWithChanges() {
+        final Schema baseSchema = Schema.getCoreSchema();
+        final Schema schema =
+                new SchemaBuilder(baseSchema).addAttributeType(
+                        "( testtype-oid NAME 'testtype' SUP name )", false).toSchema();
+        assertThat(schema).isNotSameAs(baseSchema);
+        assertThat(new ArrayList<>(schema.getObjectClasses())).isEqualTo(
+                new ArrayList<>(baseSchema.getObjectClasses()));
+        assertThat(schema.getAttributeTypes().containsAll(baseSchema.getAttributeTypes()));
+        assertThat(schema.getAttributeType("testtype")).isNotNull();
+        assertThat(schema.getSchemaName()).startsWith(baseSchema.getSchemaName());
+        assertThat(schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS))
+                .isEqualTo(baseSchema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
+    }
+
+    /** Tests that it is possible to create an empty schema. */
+    @Test
+    public void createEmptySchema() {
+        final Schema schema = new SchemaBuilder().toSchema();
+        assertThat(schema.getAttributeTypes()).isEmpty();
+        assertThat(schema.getObjectClasses()).isEmpty();
+        assertThat(schema.getSyntaxes()).isEmpty();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getDefaultSyntax()).isEqualTo(CoreSchema.getOctetStringSyntax());
+        assertThat(schema.getDefaultMatchingRule()).isEqualTo(
+                CoreSchema.getOctetStringMatchingRule());
+        // Could go on...
+    }
+
+    /** Tests that multiple consecutive invocations of toSchema return the exact same schema. */
+    @Test
+    public void multipleToSchema1() {
+        final Schema baseSchema = Schema.getCoreSchema();
+        final SchemaBuilder builder = new SchemaBuilder(baseSchema);
+        final Schema schema1 = builder.toSchema();
+        final Schema schema2 = builder.toSchema();
+        assertThat(schema1).isSameAs(baseSchema);
+        assertThat(schema1).isSameAs(schema2);
+    }
+
+    /** Tests that multiple consecutive invocations of toSchema return the exact same schema. */
+    @Test
+    public void multipleToSchema2() {
+        final SchemaBuilder builder =
+                new SchemaBuilder().addAttributeType(
+                        "( testtype-oid NAME 'testtype' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+                        false);
+        final Schema schema1 = builder.toSchema();
+        final Schema schema2 = builder.toSchema();
+        assertThat(schema1).isSameAs(schema2);
+        assertThat(schema1.getAttributeType("testtype")).isNotNull();
+        assertThat(schema2.getAttributeType("testtype")).isNotNull();
+    }
+
+    /**
+     * Tests that schema validation resolves dependencies between parent/child
+     * object classes regardless of the order in which they were added.
+     */
+    @Test
+    public void objectClassDependenciesChildThenParent() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).addObjectClass(
+                        "( childtype-oid NAME 'childtype' SUP parenttype STRUCTURAL MUST sn )",
+                        false).addObjectClass(
+                        "( parenttype-oid NAME 'parenttype' SUP top STRUCTURAL MUST cn )", false)
+                        .toSchema();
+
+        final AttributeType cn = schema.getAttributeType("cn");
+        final AttributeType sn = schema.getAttributeType("sn");
+
+        assertThat(schema.getObjectClass("childtype").isRequired(cn)).isTrue();
+        assertThat(schema.getObjectClass("childtype").isRequired(sn)).isTrue();
+    }
+
+    /**
+     * Tests that schema validation resolves dependencies between parent/child
+     * object classes regardless of the order in which they were added.
+     */
+    @Test
+    public void objectClassDependenciesParentThenChild() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                        .addObjectClass(
+                                "( parenttype-oid NAME 'parenttype' SUP top STRUCTURAL MUST cn )",
+                                false)
+                        .addObjectClass(
+                                "( childtype-oid NAME 'childtype' SUP parenttype STRUCTURAL MUST sn )",
+                                false).toSchema();
+
+        final AttributeType cn = schema.getAttributeType("cn");
+        final AttributeType sn = schema.getAttributeType("sn");
+
+        assertThat(schema.getObjectClass("childtype").isRequired(cn)).isTrue();
+        assertThat(schema.getObjectClass("childtype").isRequired(sn)).isTrue();
+    }
+
+    /**
+     * Tests that schema validation handles validation failures for superior
+     * object classes regardless of the order.
+     */
+    @Test
+    public void objectClassSuperiorFailureChildThenParent() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).addObjectClass(
+                        "( childtype-oid NAME 'childtype' SUP parenttype STRUCTURAL MUST sn )",
+                        false).addObjectClass(
+                        "( parenttype-oid NAME 'parenttype' SUP top STRUCTURAL MUST xxx )", false)
+                        .toSchema();
+
+        try {
+            schema.getObjectClass("childtype");
+            fail("childtype should not be in the schema because its parent is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+
+        try {
+            schema.getObjectClass("parenttype");
+            fail("parenttype should not be in the schema because it is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+    }
+
+    /**
+     * Tests that schema validation handles validation failures for superior
+     * object classes regardless of the order.
+     */
+    @Test
+    public void objectClassSuperiorFailureParentThenChild() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                        .addObjectClass(
+                                "( parenttype-oid NAME 'parenttype' SUP top STRUCTURAL MUST xxx )",
+                                false)
+                        .addObjectClass(
+                                "( childtype-oid NAME 'childtype' SUP parenttype STRUCTURAL MUST sn )",
+                                false).toSchema();
+
+        try {
+            schema.getObjectClass("childtype");
+            fail("childtype should not be in the schema because its parent is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+
+        try {
+            schema.getObjectClass("parenttype");
+            fail("parenttype should not be in the schema because it is invalid");
+        } catch (final UnknownSchemaElementException e) {
+            // Expected.
+        }
+    }
+
+    /** Tests that a schema builder can be re-used after toSchema has been called. */
+    @Test
+    public void reuseSchemaBuilder() {
+        final SchemaBuilder builder = new SchemaBuilder();
+        final Schema schema1 =
+                builder.addAttributeType(
+                        "( testtype1-oid NAME 'testtype1' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+                        false).toSchema();
+
+        final Schema schema2 =
+                builder.addAttributeType(
+                        "( testtype2-oid NAME 'testtype2' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+                        false).toSchema();
+        assertThat(schema1).isNotSameAs(schema2);
+        assertThat(schema1.getAttributeType("testtype1")).isNotNull();
+        assertThat(schema1.hasAttributeType("testtype2")).isFalse();
+        assertThat(schema2.getAttributeType("testtype1")).isNotNull();
+        assertThat(schema2.getAttributeType("testtype2")).isNotNull();
+    }
+
+    /**
+     * The SchemaBuilder (Entry) doesn't allow a null parameter. Throws a
+     * NullPointerException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void schemaBuilderDoesntAllowNullEntry() throws Exception {
+        new SchemaBuilder((Entry) null);
+    }
+
+    /**
+     * Test to build a schema with an entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryWithCoreSchema() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "attributeTypes: ( temporary-fake-attr-id NAME 'myCustomAttribute' EQUALITY"
+                + " caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+            "objectClasses: ( temporary-fake-oc-id NAME 'myCustomObjClass'"
+                + " SUP top AUXILIARY MAY myCustomAttribute )",
+            "modifiersName: cn=Directory Manager,cn=Root DNs,cn=config",
+            "modifyTimestamp: 20110620095948Z"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder(e);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        builder.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNotNull();
+        assertThat(schema.getAttributeType("myCustomAttribute").getNameOrOID()).isEqualTo(
+                "myCustomAttribute");
+        assertThat(schema.getAttributeType("myCustomAttribute").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4512' )");
+        assertThat(schema.getAttributeType("myCustomAttribute").getUsage().toString()).isEqualTo(
+                "userApplications");
+
+        assertThat(schema.getObjectClassesWithName("myCustomObjClass")).isNotEmpty();
+    }
+
+    /**
+     * Building a schema with an ldif entry but without a core schema throws an
+     * UnknownSchemaElementException. We can read in the schema warning :
+     * [...]The object class "myCustomObjClass" specifies the superior object
+     * class "top" which is not defined in the schema (...).
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderWithEntryWithoutCoreSchema() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "attributeTypes: ( temporary-fake-attr-id NAME 'myCustomAttribute' EQUALITY"
+                + " caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+            "objectClasses: ( temporary-fake-oc-id NAME 'myCustomObjClass'"
+                + " SUP top AUXILIARY MAY myCustomAttribute )",
+            "modifiersName: cn=Directory Manager,cn=Root DNs,cn=config",
+            "modifyTimestamp: 20110620095948Z"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder(e);
+
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNotNull();
+
+        Schema schema = builder.toSchema();
+        // Warnings expected.
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNotNull();
+    }
+
+    /**
+     * Adds an attribute to the schema, without a space between the usage and the
+     * right paren.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAttributeWithoutSpace() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "attributeTypes: ( foo-oid NAME 'foo' SUP name DESC 'No trailing space' USAGE userApplications)"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+        // No warnings
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("foo").getDescription()).isEqualTo("No trailing space");
+    }
+
+    /**
+     * Adds an attribute to the schema, without a space between an extension and the
+     * right paren.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAttributeExtensionWithoutSpace() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "attributeTypes: ( foo-oid NAME 'foo' SUP name DESC 'No trailing space' X-SCHEMA-FILE '99-test.ldif')"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+        // No warnings
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("foo").getDescription()).isEqualTo("No trailing space");
+    }
+
+    /**
+     * Adds a ldapsyntax to the schema. Ldapsyntaxes define allowable values can
+     * be used for an attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddLdapSyntax() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "ldapSyntaxes:"
+                + " ( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Add a new ldapsyntax' )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+        // No warnings
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getSyntaxes()).isNotEmpty();
+        assertThat(schema.getSyntax("1.3.6.1.4.1.1466.115.121.1.15").getDescription()).isEqualTo(
+                "Add a new ldapsyntax");
+    }
+
+    /**
+     * Attempt to add a malformed ldap syntax. The schema is created but
+     * contains several warnings about the encountered errors.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMalformedLdapSyntax() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "ldapSyntaxes:" // malformed syntax
+                + " ( 1.3.6.1.4.1.1466.115.121.1.15 MALFORMEDTOKEN 'binary' )"
+
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, false);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema sc = builder.toSchema();
+        // The schema must contain a warning!
+        assertThat(sc.getWarnings()).isNotEmpty();
+        assertThat(sc.getWarnings().toString()).contains("illegal token \"MALFORMEDTOKEN\"");
+    }
+
+    /**
+     * Add a matching rule use provided by the entry to the schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMatchingRuleUse() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "matchingRuleUse: ( 2.5.13.16 NAME 'bitStringMatch' APPLIES ( givenName $ surname ) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, false);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getMatchingRuleUses()).isNotEmpty();
+        assertThat(schema.getMatchingRuleUse("bitStringMatch").toString()).isEqualTo(
+                "( 2.5.13.16 NAME 'bitStringMatch' APPLIES ( givenName $ surname ) )");
+        // The schema do not contain warnings.
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * Try to add a malformed Matching Rule Use provided by the entry. The
+     * Matching Rule Use is not applied to the schema but it contains the
+     * warnings about it.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMalformedMatchingRuleUse() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "matchingRuleUse: ( 2.5.13.16 NAM 'bitStringMatch' APPLIES ( givenName $ surname ) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, false);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getMatchingRuleUses()).isEmpty();
+        // The schema must contain warnings ( : illegal token "NAM")
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("illegal token");
+    }
+
+    /**
+     * Adds a matching rule via the entry to the new schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMatchingRule() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "matchingRules: ( 2.5.13.16 NAME 'bitStringMatch'"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getMatchingRules()).isNotEmpty();
+        assertThat(schema.getMatchingRule("bitStringMatch").toString()).isEqualTo(
+                "( 2.5.13.16 NAME 'bitStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )");
+        // The schema do not contain warnings.
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * Try to add a Matching Rule via the entry to the new schema but the
+     * matchingRule is 'malformed'. Warnings can be found in the .getWarnings()
+     * function.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMalformedMatchingRule() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "matchingRules: ( 2.5.13.16 NAME 'bitStringMatch'"
+                + " SYNTAXE 1.3.6.1.4.1.1466.115.121.1.6 )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getMatchingRuleUses()).isEmpty();
+        // The schema does contain warning (e.g : it contains an illegal token "SYNTAXE")
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("illegal token");
+    }
+
+    /**
+     * Add a DITContentRule to the schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddDITContentRule() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "DITContentRules: ( 2.5.6.4 DESC 'content rule for organization' NOT "
+                        + "( x121Address $ telexNumber ) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getCoreSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getDITContentRules()).isNotEmpty();
+        assertThat(schema.getDITContentRule("2.5.6.4").toString())
+                .isEqualTo(
+                        "( 2.5.6.4 DESC 'content rule for organization' NOT ( x121Address $ telexNumber ) )");
+        // The schema do not contain warnings.
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * Try to add a malformed DITContentRule to the schema. Warnings are added
+     * to the schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMalformedDITContentRule() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "DITContentRules: ( 2.5.6.4 DESCS 'content rule for organization' NOT "
+                    + "( x121Address $ telexNumber ) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getDefaultSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getDITContentRules()).isEmpty();
+        // The schema does contain warnings(eg. it contains an illegal token "DESCS")
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("illegal token");
+    }
+
+    /**
+     * Adding a new objectclass to the schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddObjectClass() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "objectClasses:  ( 1.3.6.1.4.1.36733.2.1.1.15.1 NAME 'myNewClass'"
+                + " SUP top MUST ( cn ) MAY (sn $ uid) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getDefaultSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getDITContentRules()).isEmpty();
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getObjectClass("myNewClass").getOID()).isEqualTo(
+                "1.3.6.1.4.1.36733.2.1.1.15.1");
+    }
+
+    /**
+     * Attempt to add a new object class to the schema but forget to declare the
+     * linked attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderWithEntryAddMalformedObjectClass() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "objectClasses: ( 1.3.6.1.4.1.36733.2.1.1.15.1 NAME 'myNewClass'"
+                + " SUP top MUST ( unknownAttribute ) MAY (sn $ uid) )"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder();
+        builder.addSchema(Schema.getDefaultSchema(), false);
+        builder.addSchema(e, true);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getDITContentRules()).isEmpty();
+        // The schema does contain warnings :
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains(
+                "\"unknownAttribute\" which is not defined in the schema");
+    }
+
+    /**
+     * Adding a description in the attribute types.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAddAttributeWithEntryContainingDescriptionWithCoreSchema()
+            throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=schema",
+            "objectClass: top",
+            "objectClass: ldapSubentry",
+            "objectClass: subschema",
+            "cn: schema",
+            "attributeTypes: ( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY"
+                + " caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+            "objectClasses: ( temporary-fake-oc-id NAME 'myCustomObjClass'"
+                + " SUP top AUXILIARY MAY myCustomAttribute )",
+            "modifiersName: cn=Directory Manager,cn=Root DNs,cn=config",
+            "modifyTimestamp: 20110620095948Z"
+        };
+        // @formatter:on
+        final Entry e = new LinkedHashMapEntry(strEntry);
+        final SchemaBuilder builder = new SchemaBuilder(e);
+
+        assertThat(e.getAttribute(Schema.ATTR_LDAP_SYNTAXES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_OBJECT_CLASSES)).isNotNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULE_USE)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_MATCHING_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_CONTENT_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES)).isNull();
+        assertThat(e.getAttribute(Schema.ATTR_NAME_FORMS)).isNull();
+
+        // Need to add the core schema for the standards attributes.
+        builder.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = builder.toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNotNull();
+        assertThat(schema.getAttributeType("myCustomAttribute").getNameOrOID()).isEqualTo(
+                "myCustomAttribute");
+        assertThat(schema.getAttributeType("myCustomAttribute").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4512' )");
+        assertThat(schema.getAttributeType("myCustomAttribute").getUsage().toString()).isEqualTo(
+                "userApplications");
+        assertThat(schema.getAttributeType("myCustomAttribute").getDescription())
+                .isEqualTo("A short description");
+
+        assertThat(schema.getObjectClassesWithName("myCustomObjClass")).isNotEmpty();
+    }
+
+    /**
+     * Similar to the previous code, adding a description in the attribute
+     * types. The description is properly inserted in the schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAddAttributeContainingDescriptionWithCoreSchema()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Adding the new schema containing the customclass
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+        // Adding default core schema
+        scBuild.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = scBuild.toSchema();
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNotNull();
+        assertThat(schema.getAttributeType("myCustomAttribute").getNameOrOID()).isEqualTo(
+                "myCustomAttribute");
+        assertThat(schema.getAttributeType("myCustomAttribute").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4512' )");
+        assertThat(schema.getAttributeType("myCustomAttribute").getUsage().toString()).isEqualTo(
+                "userApplications");
+        assertThat(schema.getAttributeType("myCustomAttribute").getDescription())
+                .isEqualTo("A short description");
+        assertThat(schema.getObjectClassesWithName("myCustomObjClass")).isNotEmpty();
+    }
+
+    /**
+     * Adding an attribute to the schema using a wrong 'USAGE' is impossible.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddAttributeDoesntAllowWrongUsage() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Adding the new schema containing the customclass
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE wrongUsage )", false);
+    }
+
+    /**
+     * The schema builder doesn't allow a null attribute type.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void schemaBuilderAddAttributeTypeDoesntAllowNull() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Adding the new schema containing the customclass
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType((String) null, false);
+    }
+
+    /**
+     * The schema builder doesn't allow an empty attribute type.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddAttributeTypeDoesntAllowEmptyString() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Adding the new schema containing the customclass
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(" ", false);
+    }
+
+    /**
+     * Schema Builder doesn't allow to add attribute when left parenthesis is
+     * missing.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddAttributeDoesntAllowLeftParenthesisMising()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getDefaultSchema());
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                " temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+    }
+
+    /**
+     * Schema Builder doesn't allow to add attribute when right parenthesis is
+     * missing.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddAttributeDoesntAllowRightParenthesisMising()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getDefaultSchema());
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                " ( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications ",
+                false);
+    }
+
+    /**
+     * Add an attribute using the string definition and the constructor.
+     * Verifying the equality between the two.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderCompareAddAttributeTypesSucceed() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getDefaultSchema());
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+
+        Schema sc = scBuild.toSchema();
+        assertThat(sc.getAttributeType("myCustomAttribute").getDescription()).isEqualTo(
+                "A short description");
+        assertThat(sc.getAttributeType("myCustomAttribute").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4512' )");
+
+        // Description changes in this builder :
+        final SchemaBuilder scBuild2 = new SchemaBuilder(Schema.getDefaultSchema());
+        scBuild2.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+
+        scBuild2.buildAttributeType("temporary-fake-attr-id")
+                .names("myCustomAttribute")
+                .description("The new attribute type")
+                .equalityMatchingRule("caseIgnoreOrderingMatch")
+                .orderingMatchingRule("caseIgnoreOrderingMatch")
+                .substringMatchingRule("caseIgnoreSubstringsMatch")
+                .syntax("1.3.6.1.4.1.1466.115.121.1.15")
+                .usage(AttributeUsage.USER_APPLICATIONS)
+                .addToSchemaOverwrite();
+        Schema sc2 = scBuild2.toSchema();
+
+        assertThat(sc2.getAttributeType("myCustomAttribute").getDescription()).isEqualTo(
+                "The new attribute type");
+        assertThat(sc2.getAttributeType("myCustomAttribute").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4512' )");
+
+        assertThat(sc2.getAttributeType("myCustomAttribute")).isEqualTo(
+                sc.getAttributeType("myCustomAttribute"));
+    }
+
+    /**
+     * The objectClass defined in the schema builder need a left parenthesis.
+     * RFC4512. Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddObjectClassDoesntAllowMalformedObjectClass()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Left parenthesis is missing underneath
+        scBuild.addObjectClass(" temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+    }
+
+    /**
+     * The schema builder rejects malformed object class definition. Must throw
+     * an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void schemaBuilderAddObjectClassDoesntAllowMalformedObjectClassIllegalToken()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Wrong object class definition (AUXI)
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXI MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+    }
+
+    /** Rewrite an existing object class. */
+    @Test
+    public final void schemaBuilderAddObjectClass() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(getDefaultSchema());
+        scBuild.buildObjectClass("2.5.6.14")
+                .names("device")
+                .description("New description for the new existing Object Class")
+                .superiorObjectClasses(TOP_OBJECTCLASS_NAME)
+                .requiredAttributes("cn")
+                .optionalAttributes("seeAlso", "ou", "l", "description")
+                .extraProperties(SCHEMA_PROPERTY_ORIGIN, "RFC 4519")
+                .addToSchemaOverwrite();
+        Schema sc = scBuild.toSchema();
+
+        assertThat(sc.getWarnings()).isEmpty();
+        assertThat(sc.getObjectClass("device").getOID()).isEqualTo("2.5.6.14");
+        assertThat(sc.getObjectClass("device").getDescription()).isEqualTo(
+                "New description for the new existing Object Class");
+    }
+
+    /**
+     * The builder doesn't allow conflicting attributes marked as overwrite
+     * false. Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingAttributesOverwriteFalse()
+            throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+    }
+
+    @Test
+    public final void schemaBuilderWithAttributeUsageDifferentFromSuperior() {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+
+        // directoryOperation can't inherit from userApplications
+        scBuild.addAttributeType("(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                + " SUBSTR caseIgnoreSubstringsMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                + " NO-USER-MODIFICATION USAGE directoryOperation )", true);
+        scBuild.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = scBuild.toSchema();
+        assertThat(schema.getWarnings()).hasSize(1);
+        assertThat(schema.getWarnings().toString()).contains("attribute usage directoryOperation is not the same");
+    }
+
+    /**
+     * The builder allows to have twin or more attributes if it can overwrite
+     * them. Only the last is kept. Attributes may have same OID and name.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingAttributesOverwriteTrue() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+
+        //@formatter:off
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                true);
+
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'Another description' "
+                + "EQUALITY objectIdentifierFirstComponentMatch"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.37"
+                + " USAGE directoryOperation )",
+                true);
+        //@formatter:on
+
+        scBuild.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = scBuild.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNotNull();
+        assertThat(schema.getAttributeType("myCustomAttribute").getOID()).isEqualTo(
+                "temporary-fake-attr-id");
+        assertThat(schema.getAttributeType("myCustomAttribute").getSyntax().toString())
+                .isEqualTo(
+                        "( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' X-ORIGIN 'RFC 4512' )");
+        assertThat(schema.getAttributeType("myCustomAttribute").getUsage().toString()).isEqualTo(
+                "directoryOperation");
+    }
+
+    /**
+     * The builder doesn't allow conflicting DIT Structure Rules marked as
+     * overwrite false. Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingDITOverwriteFalse() throws Exception {
+        //@formatter:off
+        new SchemaBuilder(Schema.getDefaultSchema())
+                .addObjectClass(
+                "( testditstructureruleconstraintssupoc-oid "
+                    + "NAME 'testDITStructureRuleConstraintsSupOC' SUP top "
+                    + "STRUCTURAL MUST ou X-ORIGIN 'SchemaBackendTestCase')", false)
+                .addObjectClass(
+                "( testditstructureruleconstraintssuboc-oid "
+                + "NAME 'testDITStructureRuleConstraintsSubOC' SUP top "
+                + "STRUCTURAL MUST cn X-ORIGIN 'SchemaBackendTestCase')", false)
+                .addNameForm(
+                "( testditstructureruleconstraintsupsnf-oid "
+                + "NAME 'testDITStructureRuleConstraintsSupNF' "
+                + "OC testDITStructureRuleConstraintsSupOC MUST ou "
+                + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                .addNameForm(
+                "( testditstructureruleconstraintsubsnf-oid "
+                + "NAME 'testDITStructureRuleConstraintsSubNF' "
+                + "OC testDITStructureRuleConstraintsSubOC MUST cn "
+                + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                .addDITStructureRule(
+                "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                + "FORM testDITStructureRuleConstraintsSupNF "
+                + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                .addDITStructureRule(
+                "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                + "DESC 'A short description' FORM testDITStructureRuleConstraintsSupNF "
+                + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+            .toSchema();
+        //@formatter:on
+    }
+
+    /**
+     * The builder allows conflicting DIT Structure Rules marked as overwrite
+     * true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingDITStructureRuleOverwriteTrue()
+            throws Exception {
+        // @formatter:off
+        final Schema schema =
+            new SchemaBuilder(Schema.getDefaultSchema())
+                    .addObjectClass(
+                        "( testditstructureruleconstraintssupoc-oid "
+                        + "NAME 'testDITStructureRuleConstraintsSupOC' SUP top "
+                        + "STRUCTURAL MUST ou X-ORIGIN 'SchemaBackendTestCase')", false)
+                    .addObjectClass(
+                    "( testditstructureruleconstraintssuboc-oid "
+                    + "NAME 'testDITStructureRuleConstraintsSubOC' SUP top "
+                    + "STRUCTURAL MUST cn X-ORIGIN 'SchemaBackendTestCase')", false)
+                    .addNameForm(
+                    "( testditstructureruleconstraintsupsnf-oid "
+                        + "NAME 'testDITStructureRuleConstraintsSupNF' "
+                        + "OC testDITStructureRuleConstraintsSupOC MUST ou "
+                        + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                    .addNameForm(
+                    "( testditstructureruleconstraintsubsnf-oid "
+                        + "NAME 'testDITStructureRuleConstraintsSubNF' "
+                        + "OC testDITStructureRuleConstraintsSubOC MUST cn "
+                        + "X-ORIGIN 'SchemaBackendTestCase' )", false)
+                    .addDITStructureRule(
+                    "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                        + "FORM testDITStructureRuleConstraintsSupNF "
+                        + "X-ORIGIN 'SchemaBackendTestCase' )", true)
+                    .addDITStructureRule(
+                    "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                        + "DESC 'A short description' FORM testDITStructureRuleConstraintsSupNF "
+                        + "X-ORIGIN 'SchemaBackendTestCase' )", true)
+                    .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getDITStructureRulesWithName("testAllowATRequiredByDCR")).isNotNull();
+        assertThat(schema.getDITStructureRule(999014).getDescription()).isEqualTo(
+                "A short description");
+        assertThat(schema.getDITStructureRule(999014).getNameOrRuleID()).isEqualTo(
+                "testDITStructureRuleConstraintsSup");
+    }
+
+    /**
+     * Add a content Rule with the builder.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAddDITContentRuleBuilder() throws Exception {
+        // @formatter:off
+        final Schema schema =
+            new SchemaBuilder(Schema.getCoreSchema())
+                .addObjectClass("( 2.16.840.1.113730.3.2.2 NAME 'myCustomObjClass"
+                    + "' SUP top)", false)
+                .addDITContentRule(
+                    "( 2.16.840.1.113730.3.2.2"
+                        + " NAME 'inetOPerson'"
+                        + " DESC 'inetOrgPerson is defined in RFC2798'"
+                        + ")", true)
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getDITContentRule("inetOPerson")).isNotNull();
+        assertThat(schema.getDITContentRule("inetOPerson").getNameOrOID()).isEqualTo(
+                "inetOPerson");
+        assertThat(schema.getDITContentRule("inetOPerson").getDescription()).isEqualTo(
+                "inetOrgPerson is defined in RFC2798");
+    }
+
+    /**
+     * The builder doesn't allow conflicting DIT Structure Rules marked as
+     * overwrite false. Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingDITContentRuleOverwriteFalse()
+            throws Exception {
+        // @formatter:off
+        new SchemaBuilder(Schema.getDefaultSchema())
+            .addObjectClass(
+                "( testdontallowattributeprohibitedbydcroc-oid"
+                    + " NAME 'testDontAllowAttributeProhibitedByDCROC' SUP top"
+                    + " STRUCTURAL MUST cn MAY description"
+                    + " X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+            .addDITContentRule(
+                "( testdontallowattributeprohibitedbydcroc-oid"
+                    + " NAME 'testDontAllowAttributeProhibitedByDCR' NOT description"
+                    + " X-ORIGIN 'EntrySchemaCheckingTestCase2' )", false)
+            .addDITContentRule(
+                    "( testdontallowattributeprohibitedbydcroc-oid"
+                        + " NAME 'testDontAllowAttributeProhibitedByDCR'"
+                        + " DESC 'Ensure attributes prohibited' NOT description"
+                        + " X-ORIGIN 'EntrySchemaCheckingTestCase' )", false).toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * The builder allows conflicting DIT Content Rules marked as overwrite
+     * true. Schema checking for an entry covered by a DIT content rule to
+     * ensure that attributes prohibited by the DIT content rule are not allowed
+     * even if they are allowed by the associated object classes. (cf.
+     * EntrySchemaCheckingTestCase)
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingDITContentRuleOverwriteTrue()
+            throws Exception {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getDefaultSchema())
+            .addObjectClass(
+                "( testdontallowattributeprohibitedbydcroc-oid"
+                    + " NAME 'testDontAllowAttributeProhibitedByDCROC' SUP top"
+                    + " STRUCTURAL MUST cn MAY description"
+                    + " X-ORIGIN 'EntrySchemaCheckingTestCase')", false)
+            .addDITContentRule(
+                "( testdontallowattributeprohibitedbydcroc-oid"
+                    + " NAME 'testDontAllowAttributeProhibitedByDCR' NOT description"
+                    + " X-ORIGIN 'EntrySchemaCheckingTestCase2' )", true)
+            .addDITContentRule(
+                    "( testdontallowattributeprohibitedbydcroc-oid"
+                        + " NAME 'testDontAllowAttributeProhibitedByDCR'"
+                        + " DESC 'Ensure attributes prohibited' NOT description"
+                        + " X-ORIGIN 'EntrySchemaCheckingTestCase' )", true).toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getDITContentRule("testDontAllowAttributeProhibitedByDCR")).isNotNull();
+        assertThat(
+                schema.getDITContentRule("testDontAllowAttributeProhibitedByDCR").getDescription())
+                .isEqualTo("Ensure attributes prohibited");
+        assertThat(
+                schema.getDITContentRule("testDontAllowAttributeProhibitedByDCR").getNameOrOID())
+                .isEqualTo("testDontAllowAttributeProhibitedByDCR");
+    }
+
+    /**
+     * The builder doesn't allow conflicting Matching Rules marked as overwrite
+     * false. Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingMatchingRuleOverwriteFalse()
+            throws Exception {
+        // @formatter:off
+        new SchemaBuilder(Schema.getDefaultSchema())
+                .addMatchingRule(
+                "( 2.5.13.16 NAME 'bitStringMatche'"
+                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )", false)
+                .addMatchingRule(// Matching rules from RFC 2252
+                        "( 2.5.13.16 NAME 'bitStringMatch'"
+                        + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", false)
+                .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * The builder allows conflicting Matching Rules marked as overwrite true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingMatchingRuleOverwriteTrue()
+            throws Exception {
+        // @formatter:off
+        final Schema schema =
+            new SchemaBuilder(Schema.getDefaultSchema())
+                    .addMatchingRule(
+                    "( 2.5.13.16 NAME 'bitStringMatche'"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )", true)
+                    .addMatchingRule(// Matching rules from RFC 2252
+                            "( 2.5.13.16 NAME 'bitStringMatch'"
+                            + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )", true)
+                    .toSchema();
+        // @formatter:on
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getMatchingRule("bitStringMatch")).isNotNull();
+        assertThat(schema.getMatchingRule("bitStringMatch").getOID()).isEqualTo("2.5.13.16");
+        assertThat(schema.getMatchingRule("bitStringMatch").getSyntax().toString()).isEqualTo(
+                "( 1.3.6.1.4.1.1466.115.121.1.6 DESC 'Bit String' X-ORIGIN 'RFC 4512' )");
+    }
+
+    /**
+     * The builder doesn't allow conflicting Matching Rules marked as overwrite
+     * false. Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingMatchingRuleUseOverwriteFalse()
+            throws Exception {
+        // @formatter:off
+        new SchemaBuilder(Schema.getDefaultSchema())
+            .addMatchingRuleUse(
+                    "( 2.5.13.16 APPLIES ( givenName $ name ) )", false)
+            .addMatchingRuleUse(
+                        "( 2.5.13.16 APPLIES ( givenName $ surname ) )", false)
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * The builder allows conflicting Matching Rules marked as overwrite true.
+     * Cf. RFC 4517 3.3.20.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingMatchingRuleUseOverwriteTrue()
+            throws Exception {
+        // @formatter:off
+        final Schema schema =
+            new SchemaBuilder(Schema.getDefaultSchema())
+                .addMatchingRuleUse(
+                    "( 2.5.13.16 NAME 'bitStringMatch' APPLIES ( givenName $ name ) )", true)
+                .addMatchingRuleUse(
+                    "( 2.5.13.16 NAME 'bitStringMatch' APPLIES ( givenName $ surname ) )", true)
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getMatchingRuleUsesWithName("bitStringMatch")).isNotEmpty();
+        assertThat(schema.getMatchingRuleUses().size()).isEqualTo(1);
+
+        for (MatchingRuleUse o : schema.getMatchingRuleUses()) {
+            assertThat(o.getNameOrOID()).isEqualTo("bitStringMatch");
+            assertThat(o.getMatchingRuleOID()).isEqualTo("2.5.13.16");
+            assertThat(o.getMatchingRule().toString()).isEqualTo(
+                    "( 2.5.13.16 NAME 'bitStringMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.6"
+                            + " X-ORIGIN 'RFC 4512' )");
+        }
+        assertThat(schema.getWarnings()).isEmpty();
+    }
+
+    /**
+     * The builder doesn't allow conflicting NameForm marked as overwrite false.
+     * Must throw a ConflictingSchemaElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public final void schemaBuilderDoesntAllowConflictingNameFormOverwriteFalse()
+            throws Exception {
+        // @formatter:off
+        new SchemaBuilder(Schema.getDefaultSchema())
+            .addNameForm(
+                "( testviolatessinglevaluednameform-oid "
+                    + "NAME 'testViolatesSingleValuedNameForm' "
+                    + "OC testViolatesSingleValuedNameFormOC MUST cn "
+                    + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+            .addNameForm(
+                "( testviolatessinglevaluednameform-oid "
+                    + "NAME 'testViolatesSingleValuedNameForm' "
+                    + "OC testViolatesSingleValuedNameFormOC MUST cn "
+                    + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false)
+            .toSchema();
+        // @formatter:on
+    }
+
+    /**
+     * The builder allows conflicting Name Form marked as overwrite true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAllowsConflictingNameFormOverwriteTrue() throws Exception {
+        // @formatter:off
+        final Schema schema =
+            new SchemaBuilder(Schema.getDefaultSchema())
+                .addObjectClass(
+                    "( testviolatessinglevaluednameformoc-oid "
+                            + "NAME 'testViolatesSingleValuedNameFormOC' SUP top STRUCTURAL "
+                            + "MUST cn MAY description X-ORIGIN 'EntrySchemaCheckingTestCase')",
+                    false)
+                .addNameForm(
+                    "( testviolatessinglevaluednameform-oid "
+                        + "NAME 'testViolatesSingleValuedNameForm' "
+                        + "OC testViolatesSingleValuedNameFormOC MUST sn "
+                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", true)
+                .addNameForm(
+                    "( testviolatessinglevaluednameform-oid "
+                        + "NAME 'testViolatesSingleValuedNameForm' "
+                        + "OC testViolatesSingleValuedNameFormOC MUST cn "
+                        + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", true)
+                .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getNameFormsWithName("testViolatesSingleValuedNameForm")).isNotNull();
+        for (NameForm o : schema.getNameForms()) {
+            assertThat(o.getNameOrOID()).isEqualTo("testViolatesSingleValuedNameForm");
+            assertThat(o.getOID()).isEqualTo("testviolatessinglevaluednameform-oid");
+            assertThat(o.getStructuralClass().getOID()).isEqualTo(
+                    "testviolatessinglevaluednameformoc-oid");
+        }
+    }
+
+    /**
+     * Use the schema builder to remove an attribute.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveAttributeType() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getDefaultSchema());
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType(
+                "( temporary-fake-attr-id NAME 'myCustomAttribute' DESC 'A short description' EQUALITY case"
+                        + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                        + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )",
+                false);
+        boolean isRemoved = scBuild.removeAttributeType("myCustomAttribute");
+        assertThat(isRemoved).isTrue();
+        Schema schema = scBuild.toSchema();
+        // The following line throws an exception :
+        assertThat(schema.getAttributeType("myCustomAttribute")).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent attribute type. Do
+     * Nothing.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantAttributeType() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeAttributeType("wrongName");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent syntax.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantSyntax() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeSyntax("1.3.6.1.4.1.14aa");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to remove a syntax.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveSyntax() throws Exception {
+        assertThat(Schema.getCoreSchema().getSyntax("1.3.6.1.4.1.1466.115.121.1.15")).isNotNull();
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getDefaultSchema());
+
+        boolean isRemoved = scBuild.removeSyntax("1.3.6.1.4.1.1466.115.121.1.15");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+        assertThat(sc.getSyntax("1.3.6.1.4.1.1466.115.121.1.15")).isNull();
+    }
+
+    /**
+     * Use the schema builder to remove a DIT Content Rule.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveDitContentRule() throws Exception {
+        // @formatter:off
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        scBuild.addObjectClass("( 2.16.840.1.113730.3.2.2 NAME 'myCustomObjClass"
+                + "' SUP top)", false);
+        scBuild.addDITContentRule(
+                    "( 2.16.840.1.113730.3.2.2"
+                            + " NAME 'inetOPerson'"
+                            + " DESC 'inetOrgPerson is defined in RFC2798'"
+                            + ")", true);
+        // @formatter:on
+
+        boolean isRemoved = scBuild.removeDITContentRule("inetOPerson");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+        for (DITContentRule dit : sc.getDITContentRules()) {
+            assertThat(dit.getNameOrOID()).isNotEqualTo("inetOPerson");
+        }
+    }
+
+    /**
+     * Use the schema builder to removing a non existent DIT Content Rule.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantDitContentRule() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeDITContentRule("badDITContentRule");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a DIT Structure Rule.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveDitStructureRule() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        scBuild.addObjectClass(
+                "( testditstructureruleconstraintssupoc-oid "
+                        + "NAME 'testDITStructureRuleConstraintsSupOC' SUP top "
+                        + "STRUCTURAL MUST ou X-ORIGIN 'SchemaBackendTestCase')", false)
+                .addObjectClass(
+                        "( testditstructureruleconstraintssuboc-oid "
+                                + "NAME 'testDITStructureRuleConstraintsSubOC' SUP top "
+                                + "STRUCTURAL MUST cn X-ORIGIN 'SchemaBackendTestCase')", false)
+                .addNameForm(
+                        "( testditstructureruleconstraintsupsnf-oid "
+                                + "NAME 'testDITStructureRuleConstraintsSupNF' "
+                                + "OC testDITStructureRuleConstraintsSupOC MUST ou "
+                                + "X-ORIGIN 'SchemaBackendTestCase' )", false).addNameForm(
+                        "( testditstructureruleconstraintsubsnf-oid "
+                                + "NAME 'testDITStructureRuleConstraintsSubNF' "
+                                + "OC testDITStructureRuleConstraintsSubOC MUST cn "
+                                + "X-ORIGIN 'SchemaBackendTestCase' )", false).addDITStructureRule(
+                        "( 999014 " + "NAME 'testDITStructureRuleConstraintsSup' "
+                                + "FORM testDITStructureRuleConstraintsSupNF "
+                                + "X-ORIGIN 'SchemaBackendTestCase' )", true);
+
+        boolean isRemoved = scBuild.removeDITStructureRule(999014);
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+
+        assertThat(sc.getDITStructureRule(999014)).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent DIT Structure Rule.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantDitStructureRule() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeDITStructureRule(999014);
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a Matching Rule.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveMatchingRule() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        scBuild.addMatchingRule(
+                // Matching rules from RFC 2252
+                "( 2.5.13.16 NAME 'bitStringMatch'" + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )",
+                false);
+
+        boolean isRemoved = scBuild.removeMatchingRule("bitStringMatch");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+
+        assertThat(sc.getMatchingRule("bitStringMatch")).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent Matching Rule.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantMatchingRule() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeMatchingRule("bitStringMatchZ");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a Matching Rule Use.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveMatchingRuleUSe() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        scBuild.addMatchingRuleUse(
+                "( 2.5.13.16 NAME 'bitStringMatch' APPLIES ( givenName $ surname ) )", false);
+
+        boolean isRemoved = scBuild.removeMatchingRuleUse("bitStringMatch");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+
+        assertThat(sc.getMatchingRuleUse("bitStringMatch")).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent Matching Rule Use.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantMatchingRuleUse() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeMatchingRuleUse("bitStringMatchZ");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a Name Form.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveNameForm() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        scBuild.addNameForm("( testviolatessinglevaluednameform-oid "
+                + "NAME 'testViolatesSingleValuedNameForm' "
+                + "OC testViolatesSingleValuedNameFormOC MUST cn "
+                + "X-ORIGIN 'EntrySchemaCheckingTestCase' )", false);
+
+        boolean isRemoved = scBuild.removeNameForm("testViolatesSingleValuedNameForm");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+        assertThat(sc.getNameForm("testViolatesSingleValuedNameForm")).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent Name Form.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantNameForm() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeNameForm("bitStringMatchZ");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * Use the schema builder to removing a Object class.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = UnknownSchemaElementException.class)
+    public final void schemaBuilderRemoveObjectClass() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        scBuild.addObjectClass("( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( sn $ cn )"
+                + " MAY ( userPassword $ telephoneNumber $ seeAlso $ description )"
+                + " X-ORIGIN 'RFC 4519' )", false);
+
+        boolean isRemoved = scBuild.removeObjectClass("person");
+        assertThat(isRemoved).isTrue();
+        Schema sc = scBuild.toSchema();
+        assertThat(sc.getObjectClass("person")).isNull();
+    }
+
+    /**
+     * Use the schema builder to removing a non existent Object class.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderRemoveInexistantObjectClass() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        boolean isRemoved = scBuild.removeObjectClass("bitStringMatchZ");
+        assertThat(isRemoved).isFalse();
+    }
+
+    /**
+     * AddSchemaForEntry doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void schemaBuilderAddSchemaForEntryDoesntAllowNull() throws Exception {
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+        scBuild.addSchemaForEntry(null, null, false);
+    }
+
+    /**
+     * Try to addSchemaForEntry but the entry doesn't include the
+     * subschemaSubentry attribute. Exception expected : The entry
+     * uid=scarter,ou=People,dc=example,dc=com does not include a
+     * subschemaSubentry attribute !
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public final void schemaBuilderAddSchemaForEntryDoesntContainSubschemaMockConnection()
+            throws Exception {
+        Connection connection = mock(Connection.class);
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+
+        // @formatter:off
+        final String[] entry = {
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "postalAddress: Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode: 50369", "uid: user.0"
+        };
+
+        when(
+            connection.searchSingleEntry((SearchRequest) any()))
+                .thenReturn(Responses.newSearchResultEntry(entry));
+        // @formatter:on
+
+        scBuild.addSchemaForEntry(connection,
+                DN.valueOf("uid=scarter,ou=People,dc=example,dc=com"), false);
+    }
+
+    /**
+     * Retrieving an LDAP Server's schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAddSchemaForEntryMockConnection() throws Exception {
+        Connection connection = mock(Connection.class);
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+
+        // @formatter:off
+        final String[] entry = {
+            "# Search result entry: uid=bjensen,ou=People,dc=example,dc=com",
+            "dn: uid=bjensen,ou=People,dc=example,dc=com",
+            "subschemaSubentry: cn=schema",
+            "entryDN: uid=bjensen,ou=people,dc=example,dc=com",
+            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c"
+            // N.B : also works with previous example but needs the subschemaSubentry line.
+        };
+
+        // Send a search entry result :
+        when(
+            connection.searchSingleEntry((SearchRequest) any()))
+                .thenReturn(Responses.newSearchResultEntry(entry));
+        // @formatter:on
+
+        scBuild.addSchemaForEntry(connection,
+                DN.valueOf("uid=bjensen,ou=People,dc=example,dc=com"), false);
+
+        Schema sc = scBuild.toSchema();
+        // We retrieve the schema :
+        assertThat(sc.getSyntaxes()).isNotNull();
+        assertThat(sc.getAttributeTypes()).isNotNull();
+        assertThat(sc.getAttributeTypes()).isNotEmpty();
+        assertThat(sc.getObjectClasses()).isNotNull();
+        assertThat(sc.getObjectClasses()).isNotEmpty();
+        assertThat(sc.getMatchingRuleUses()).isNotNull();
+        assertThat(sc.getMatchingRuleUses()).isEmpty();
+        assertThat(sc.getMatchingRules()).isNotNull();
+        assertThat(sc.getMatchingRules()).isNotEmpty();
+        assertThat(sc.getDITContentRules()).isNotNull();
+        assertThat(sc.getDITContentRules()).isEmpty();
+        assertThat(sc.getDITStuctureRules()).isNotNull();
+        assertThat(sc.getDITStuctureRules()).isEmpty();
+        assertThat(sc.getNameForms()).isNotNull();
+        assertThat(sc.getNameForms()).isEmpty();
+
+        connection.close();
+    }
+
+    /**
+     * Asynchronously retrieving an LDAP Server's schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void schemaBuilderAddSchemaForEntryAsyncMockConnection() throws Exception {
+        Connection connection = mock(Connection.class);
+        final SchemaBuilder scBuild = new SchemaBuilder(Schema.getCoreSchema());
+
+        // @formatter:off
+        final String[] entry = {
+            "# Search result entry: uid=bjensen,ou=People,dc=example,dc=com",
+            "dn: uid=bjensen,ou=People,dc=example,dc=com",
+            "subschemaSubentry: cn=schema",
+            "entryDN: uid=bjensen,ou=people,dc=example,dc=com",
+            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c"
+            // N.B : also works with previous example but needs the subschemaSubentry line.
+        };
+
+        // Send a search entry result promise :
+        LdapPromise<SearchResultEntry> result =
+                newSuccessfulLdapPromise(Responses.newSearchResultEntry(entry));
+        when(connection.searchSingleEntryAsync((SearchRequest) any())).thenReturn(result);
+        DN testDN = DN.valueOf("uid=bjensen,ou=People,dc=example,dc=com");
+        // @formatter:on
+        Schema sc = scBuild.addSchemaForEntryAsync(connection, testDN, false).getOrThrow().toSchema();
+
+        // We retrieve the schema
+        assertThat(sc.getSyntaxes()).isNotNull();
+        assertThat(sc.getAttributeTypes()).isNotNull();
+        assertThat(sc.getAttributeTypes()).isNotEmpty();
+        assertThat(sc.getObjectClasses()).isNotNull();
+        assertThat(sc.getObjectClasses()).isNotEmpty();
+        assertThat(sc.getMatchingRuleUses()).isNotNull();
+        assertThat(sc.getMatchingRuleUses()).isEmpty();
+        assertThat(sc.getMatchingRules()).isNotNull();
+        assertThat(sc.getMatchingRules()).isNotEmpty();
+        assertThat(sc.getDITContentRules()).isNotNull();
+        assertThat(sc.getDITContentRules()).isEmpty();
+        assertThat(sc.getDITStuctureRules()).isNotNull();
+        assertThat(sc.getDITStuctureRules()).isEmpty();
+        assertThat(sc.getNameForms()).isNotNull();
+        assertThat(sc.getNameForms()).isEmpty();
+
+        connection.close();
+    }
+
+    @Test
+    public void defaultSyntax() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).toSchema().asNonStrictSchema();
+        assertThat(schema.getDefaultSyntax()).isEqualTo(CoreSchema.getOctetStringSyntax());
+        assertThat(schema.getAttributeType("dummy").getSyntax()).isEqualTo(
+                CoreSchema.getOctetStringSyntax());
+    }
+
+    @Test
+    public void overrideDefaultSyntax() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                    .setOption(DEFAULT_SYNTAX_OID, getDirectoryStringSyntax().getOID())
+                    .toSchema().asNonStrictSchema();
+        assertThat(schema.getDefaultSyntax()).isEqualTo(getDirectoryStringSyntax());
+        assertThat(schema.getAttributeType("dummy").getSyntax()).isEqualTo(getDirectoryStringSyntax());
+    }
+
+    @Test
+    public void defaultMatchingRule() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema()).toSchema().asNonStrictSchema();
+        assertThat(schema.getDefaultMatchingRule()).isEqualTo(
+                CoreSchema.getOctetStringMatchingRule());
+        assertThat(schema.getAttributeType("dummy").getEqualityMatchingRule()).isEqualTo(
+                CoreSchema.getOctetStringMatchingRule());
+    }
+
+    @Test
+    public void overrideMatchingRule() {
+        final Schema schema =
+                new SchemaBuilder(Schema.getCoreSchema())
+                    .setOption(DEFAULT_MATCHING_RULE_OID, getCaseIgnoreMatchingRule().getOID())
+                    .toSchema().asNonStrictSchema();
+        assertThat(schema.getDefaultMatchingRule()).isEqualTo(
+                CoreSchema.getCaseIgnoreMatchingRule());
+        assertThat(schema.getAttributeType("dummy").getEqualityMatchingRule()).isEqualTo(
+                CoreSchema.getCaseIgnoreMatchingRule());
+    }
+
+    @Test
+    public void defaultSyntaxDefinedInSchema() {
+        // The next line was triggering a NPE with OPENDJ-1252.
+        final Schema schema =
+                new SchemaBuilder().addSyntax("( 9.9.9 DESC 'Test Syntax' )", false).addSyntax(
+                        CoreSchema.getOctetStringSyntax().toString(), false).toSchema();
+
+        // Ensure that the substituted syntax is usable.
+        assertThat(schema.getSyntax("9.9.9").valueIsAcceptable(ByteString.valueOfUtf8("test"), null))
+                .isTrue();
+    }
+
+    @Test
+    public void defaultMatchingRuleDefinedInSchema() throws DecodeException {
+        final Schema schema =
+                new SchemaBuilder().addSyntax(CoreSchema.getOctetStringSyntax().toString(), false)
+                        .addMatchingRule(
+                                "( 9.9.9 NAME 'testRule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+                                false).addMatchingRule(
+                                CoreSchema.getOctetStringMatchingRule().toString(), false)
+                        .toSchema();
+
+        // Ensure that the substituted rule is usable: was triggering a NPE with OPENDJ-1252.
+        assertThat(
+                schema.getMatchingRule("9.9.9").normalizeAttributeValue(ByteString.valueOfUtf8("test")))
+                .isEqualTo(ByteString.valueOfUtf8("test"));
+    }
+
+    @Test
+    public void enumSyntaxAddThenRemove() {
+        final Schema coreSchema = Schema.getCoreSchema();
+        final Schema schemaWithEnum = new SchemaBuilder(coreSchema)
+            .addSyntax("( 3.3.3  DESC 'Day Of The Week'  "
+                    + "X-ENUM  ( 'monday' 'tuesday'   'wednesday'  'thursday'  'friday'  'saturday' 'sunday') )",
+                    false)
+            .toSchema();
+
+        assertThat(schemaWithEnum.getWarnings()).isEmpty();
+        assertThat(schemaWithEnum.getMatchingRules())
+            .as("Expected an enum ordering matching rule to be added for the enum syntax")
+            .hasSize(coreSchema.getMatchingRules().size() + 1);
+
+        final SchemaBuilder builder = new SchemaBuilder(schemaWithEnum);
+        assertThat(builder.removeSyntax("3.3.3")).isTrue();
+        final Schema schemaNoEnum = builder.toSchema();
+
+        assertThat(schemaNoEnum.getWarnings()).isEmpty();
+        assertThat(schemaNoEnum.getMatchingRules())
+            .as("Expected the enum ordering matching rule to be removed at the same time as the enum syntax")
+            .hasSize(coreSchema.getMatchingRules().size());
+    }
+
+    @Test
+    public void attributeTypesUseNewlyBuiltSyntaxes() throws Exception {
+        final Schema coreSchema = Schema.getCoreSchema();
+        final Schema schema = new SchemaBuilder(coreSchema)
+                .addAttributeType("( 1.2.3.4.5.7 NAME 'associateoid'  "
+                                          + "EQUALITY 2.5.13.2 ORDERING 2.5.13.3 SUBSTR 2.5.13.4 "
+                                          + "SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications "
+                                          + "X-APPROX '1.3.6.1.4.1.26027.1.4.1' )", false)
+                .toSchema();
+
+        Syntax dnSyntax = schema.getAttributeType("distinguishedName").getSyntax();
+        assertThat(dnSyntax).isSameAs(schema.getSyntax("1.3.6.1.4.1.1466.115.121.1.12"));
+
+        LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
+        boolean isValid = dnSyntax.valueIsAcceptable(ByteString.valueOfUtf8("associateoid=test"), invalidReason);
+        assertThat(isValid)
+                .as("Value should have been valid, but it is not: " + invalidReason.toString())
+                .isTrue();
+    }
+
+    @DataProvider
+    private Object[][] schemasWithEnumerationSyntaxes() {
+        final Schema coreSchema = Schema.getCoreSchema();
+
+        final Schema usingAddEnumerationSyntax = new SchemaBuilder("usingAddEnumerationSyntax")
+                .addSchema(coreSchema, true)
+                .addEnumerationSyntax("3.3.3", "Primary colors", true, "red", "green", "blue")
+                .toSchema();
+
+        final Schema usingAddSyntaxString = new SchemaBuilder("usingAddSyntaxString")
+                .addSchema(coreSchema, true)
+                .addSyntax("( 3.3.3  DESC 'Primary colors' X-ENUM ( 'red' 'green' 'blue' ))", true)
+                .toSchema();
+
+        final Schema usingBuildSyntaxCopy = new SchemaBuilder("usingBuildSyntaxCopy")
+                .addSchema(coreSchema, true)
+                .buildSyntax(usingAddEnumerationSyntax.getSyntax("3.3.3")).addToSchema()
+                .toSchema();
+
+        final Schema usingBuildSyntaxIncremental = new SchemaBuilder("usingBuildSyntaxIncremental")
+                .addSchema(coreSchema, true)
+                .buildSyntax("3.3.3")
+                .description("Primary colors")
+                .extraProperties("X-ENUM", "red", "green", "blue")
+                .addToSchema()
+                .toSchema();
+
+        final Schema usingCopyOfSchema = new SchemaBuilder("usingCopyOfSchema")
+                .addSchema(usingAddEnumerationSyntax, true)
+                .toSchema();
+
+        return new Object[][] {
+            { usingAddEnumerationSyntax },
+            { usingAddSyntaxString },
+            { usingBuildSyntaxCopy },
+            { usingBuildSyntaxIncremental },
+            { usingCopyOfSchema }
+        };
+    }
+
+    @Test(dataProvider = "schemasWithEnumerationSyntaxes")
+    public void enumerationSyntaxesCanBeCreatedUsingDifferentApproaches(Schema schema) {
+        assertThat(schema.getWarnings()).isEmpty();
+        assertThat(schema.getMatchingRules())
+                .as("Expected an enum ordering matching rule to be added for the enum syntax")
+                .hasSize(Schema.getCoreSchema().getMatchingRules().size() + 1);
+
+        LocalizableMessageBuilder msgBuilder = new LocalizableMessageBuilder();
+        Syntax primaryColors = schema.getSyntax("3.3.3");
+        assertThat(primaryColors.valueIsAcceptable(ByteString.valueOfUtf8("red"), msgBuilder)).isTrue();
+        assertThat(primaryColors.valueIsAcceptable(ByteString.valueOfUtf8("yellow"), msgBuilder)).isFalse();
+
+        MatchingRule matchingRule = schema.getMatchingRule(OMR_OID_GENERIC_ENUM + "." + "3.3.3");
+        assertThat(matchingRule).isNotNull();
+        assertThat(matchingRule).isSameAs(primaryColors.getOrderingMatchingRule());
+    }
+
+    @DataProvider
+    private Object[][] schemasWithSubstitutionSyntaxes() {
+        final Schema coreSchema = Schema.getCoreSchema();
+
+        final Schema usingAddSubstitutionSyntax = new SchemaBuilder("usingAddSubstitutionSyntax")
+                .addSchema(coreSchema, true)
+                .addSubstitutionSyntax("3.3.3", "Long Integer", "1.3.6.1.4.1.1466.115.121.1.27", true)
+                .toSchema();
+
+        final Schema usingAddSyntaxString = new SchemaBuilder("usingAddSyntaxString")
+                .addSchema(coreSchema, true)
+                .addSyntax("( 3.3.3  DESC 'Long Integer' X-SUBST '1.3.6.1.4.1.1466.115.121.1.27' )", true)
+                .toSchema();
+
+        final Schema usingBuildSyntaxCopy = new SchemaBuilder("usingBuildSyntaxCopy")
+                .addSchema(coreSchema, true)
+                .buildSyntax(usingAddSubstitutionSyntax.getSyntax("3.3.3")).addToSchema()
+                .toSchema();
+
+        final Schema usingBuildSyntaxIncremental = new SchemaBuilder("usingBuildSyntaxIncremental")
+                .addSchema(coreSchema, true)
+                .buildSyntax("3.3.3")
+                .description("Long Integer")
+                .extraProperties("X-SUBST", "1.3.6.1.4.1.1466.115.121.1.27")
+                .addToSchema()
+                .toSchema();
+
+        final Schema usingCopyOfSchema = new SchemaBuilder("usingCopyOfSchema")
+                .addSchema(usingAddSubstitutionSyntax, true)
+                .toSchema();
+
+        return new Object[][] {
+            { usingAddSubstitutionSyntax },
+            { usingAddSyntaxString },
+            { usingBuildSyntaxCopy },
+            { usingBuildSyntaxIncremental },
+            { usingCopyOfSchema }
+        };
+    }
+
+    @Test(dataProvider = "schemasWithSubstitutionSyntaxes")
+    public void substitutionSyntaxesCanBeCreatedUsingDifferentApproaches(Schema schema) {
+        assertThat(schema.getWarnings()).isEmpty();
+
+        LocalizableMessageBuilder msgBuilder = new LocalizableMessageBuilder();
+        Syntax longInteger = schema.getSyntax("3.3.3");
+        assertThat(longInteger.valueIsAcceptable(ByteString.valueOfUtf8("12345"), msgBuilder)).isTrue();
+        assertThat(longInteger.valueIsAcceptable(ByteString.valueOfUtf8("NaN"), msgBuilder)).isFalse();
+
+        MatchingRule matchingRule = schema.getMatchingRule("integerOrderingMatch");
+        assertThat(matchingRule).isNotNull();
+        assertThat(matchingRule).isSameAs(longInteger.getOrderingMatchingRule());
+    }
+
+    @DataProvider
+    private Object[][] schemasWithPatternSyntaxes() {
+        final Schema coreSchema = Schema.getCoreSchema();
+
+        final Schema usingAddPatternSyntax = new SchemaBuilder("usingAddSubstitutionSyntax")
+                .addSchema(coreSchema, true)
+                .addPatternSyntax("3.3.3", "Host and Port", Pattern.compile("[^:]+:\\d+"), true)
+                .toSchema();
+
+        final Schema usingAddSyntaxString = new SchemaBuilder("usingAddSyntaxString")
+                .addSchema(coreSchema, true)
+                .addSyntax("( 3.3.3  DESC 'Host and Port' X-PATTERN '[^:]+:\\d+' )", true)
+                .toSchema();
+
+        final Schema usingBuildSyntaxCopy = new SchemaBuilder("usingBuildSyntaxCopy")
+                .addSchema(coreSchema, true)
+                .buildSyntax(usingAddPatternSyntax.getSyntax("3.3.3")).addToSchema()
+                .toSchema();
+
+        final Schema usingBuildSyntaxIncremental = new SchemaBuilder("usingBuildSyntaxIncremental")
+                .addSchema(coreSchema, true)
+                .buildSyntax("3.3.3")
+                .description("Host and Port")
+                .extraProperties("X-PATTERN", "[^:]+:\\d+")
+                .addToSchema()
+                .toSchema();
+
+        final Schema usingCopyOfSchema = new SchemaBuilder("usingCopyOfSchema")
+                .addSchema(usingAddPatternSyntax, true)
+                .toSchema();
+
+        return new Object[][] {
+            { usingAddPatternSyntax },
+            { usingAddSyntaxString },
+            { usingBuildSyntaxCopy },
+            { usingBuildSyntaxIncremental },
+            { usingCopyOfSchema }
+        };
+    }
+
+    @Test(dataProvider = "schemasWithPatternSyntaxes")
+    public void patternSyntaxesCanBeCreatedUsingDifferentApproaches(Schema schema) {
+        assertThat(schema.getWarnings()).isEmpty();
+
+        LocalizableMessageBuilder msgBuilder = new LocalizableMessageBuilder();
+        Syntax longInteger = schema.getSyntax("3.3.3");
+        assertThat(longInteger.valueIsAcceptable(ByteString.valueOfUtf8("localhost:389"), msgBuilder)).isTrue();
+        assertThat(longInteger.valueIsAcceptable(ByteString.valueOfUtf8("bad"), msgBuilder)).isFalse();
+
+        MatchingRule matchingRule = schema.getMatchingRule("caseIgnoreOrderingMatch");
+        assertThat(matchingRule).isNotNull();
+        assertThat(matchingRule).isSameAs(longInteger.getOrderingMatchingRule());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatTest.java
new file mode 100644
index 0000000..5ab1e6c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatTest.java
@@ -0,0 +1,299 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+/**
+ * Tests schema compatibility options.
+ */
+public class SchemaCompatTest extends AbstractSchemaTestCase {
+    /**
+     * Returns test data for valid attribute descriptions.
+     *
+     * @return The test data.
+     */
+    @DataProvider
+    public Object[][] validAttributeDescriptions() {
+        // @formatter:off
+        return new Object[][] {
+            // No options.
+            { "cn", false },
+            { "cn-xxx", false },
+            { "cn", true },
+            { "cn-xxx", true },
+            { "cn_xxx", true },
+            { "cn.xxx", true },
+            // With options.
+            { "cn;xxx", false },
+            { "cn;xxx-yyy", false },
+            { "cn;xxx", true },
+            { "cn;xxx-yyy", true },
+            { "cn;xxx_yyy", true },
+            { "cn;xxx.yyy", true },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests valid attribute description parsing behavior depends on compat
+     * options.
+     *
+     * @param atd
+     *            The attribute description to be parsed.
+     * @param allowIllegalCharacters
+     *            {@code true} if the attribute description requires the
+     *            compatibility option to be set.
+     */
+    @Test(dataProvider = "validAttributeDescriptions")
+    public void testValidAttributeDescriptions(String atd, boolean allowIllegalCharacters) {
+        SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema())
+            .setOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS, allowIllegalCharacters);
+        AttributeDescription.valueOf(atd, builder.toSchema().asNonStrictSchema());
+    }
+
+    /**
+     * Returns test data for invalid attribute descriptions.
+     *
+     * @return The test data.
+     */
+    @DataProvider
+    public Object[][] invalidAttributeDescriptions() {
+        // @formatter:off
+        return new Object[][] {
+            // No options.
+            { "cn+xxx", false }, // always invalid
+            { "cn_xxx", false },
+            { "cn.xxx", false },
+            { "cn+xxx", true }, // always invalid
+            // With options.
+            { "cn;xxx+yyy", false }, // always invalid
+            { "cn;xxx_yyy", false },
+            { "cn;xxx.yyy", false },
+            { "cn;xxx+yyy", true }, // always invalid
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Tests invalid attribute description parsing behavior depends on compat
+     * options.
+     *
+     * @param atd
+     *            The attribute description to be parsed.
+     * @param allowIllegalCharacters
+     *            {@code true} if the attribute description requires the
+     *            compatibility option to be set.
+     */
+    @Test(dataProvider = "invalidAttributeDescriptions", expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testInvalidAttributeDescriptions(String atd, boolean allowIllegalCharacters) {
+        SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.setOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS, allowIllegalCharacters);
+        AttributeDescription.valueOf(atd, builder.toSchema().asNonStrictSchema());
+    }
+
+    private static final Syntax ATD_SYNTAX = CoreSchema.getAttributeTypeDescriptionSyntax();
+    private static final Syntax OCD_SYNTAX = CoreSchema.getObjectClassDescriptionSyntax();
+
+    /**
+     * Returns test data for invalid schema elements.
+     *
+     * @return The test data.
+     */
+    @DataProvider
+    public Object[][] invalidSchemaElements() {
+        // @formatter:off
+        return new Object[][] {
+            { "(testtype+oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(testtype_oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(testtype.oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(1.2.8.5 NAME 'test+type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(1.2.8.5 NAME 'test.type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(1.2.8.5 NAME 'test_type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(1.2.8.5 NAME 'test+type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              true
+            },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Test schema builder schema element parsing with compat chars.
+     *
+     * @param element
+     *            The schema element.
+     * @param syntax
+     *            The type of element.
+     * @param allowIllegalCharacters
+     *            {@code true} if the element requires the compatibility option
+     *            to be set.
+     */
+    @Test(dataProvider = "invalidSchemaElements", expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testInvalidSchemaBuilderElementParsers(String element, Syntax syntax, boolean allowIllegalCharacters) {
+        SchemaBuilder builder = new SchemaBuilder();
+        builder.setOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS, allowIllegalCharacters);
+
+        if (syntax == ATD_SYNTAX) {
+            builder.addAttributeType(element, false);
+        } else if (syntax == OCD_SYNTAX) {
+            builder.addObjectClass(element, false);
+        }
+    }
+
+    /**
+     * Returns test data for valid schema elements.
+     *
+     * @return The test data.
+     */
+    @DataProvider
+    public Object[][] validSchemaElements() {
+        // @formatter:off
+        return new Object[][] {
+            { "(1.2.8.5 NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(testtype-oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(testtype_oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              true
+            },
+            { "(testtype.oid NAME 'testtype' DESC 'full type' OBSOLETE SUP cn "
+                         + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                         + " SUBSTR caseIgnoreSubstringsMatch"
+                         + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                         + " USAGE userApplications )",
+              ATD_SYNTAX,
+              true
+            },
+            { "(1.2.8.5 NAME 'test-type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              false
+            },
+            { "(1.2.8.5 NAME 'test.type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              true
+            },
+            { "(1.2.8.5 NAME 'test_type' DESC 'full type' OBSOLETE SUP cn "
+                    + " EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch"
+                    + " SUBSTR caseIgnoreSubstringsMatch"
+                    + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE"
+                    + " USAGE userApplications )",
+              ATD_SYNTAX,
+              true
+            },
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Test schema builder schema element parsing with compat chars.
+     *
+     * @param element
+     *            The schema element.
+     * @param syntax
+     *            The type of element.
+     * @param allowIllegalCharacters
+     *            {@code true} if the element requires the compatibility option
+     *            to be set.
+     */
+    @Test(dataProvider = "validSchemaElements")
+    public void testValidSchemaBuilderElementParsers(String element, Syntax syntax, boolean allowIllegalCharacters) {
+        SchemaBuilder builder = new SchemaBuilder();
+        builder.setOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS, allowIllegalCharacters);
+
+        if (syntax == ATD_SYNTAX) {
+            builder.addAttributeType(element, false);
+        } else if (syntax == OCD_SYNTAX) {
+            builder.addObjectClass(element, false);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaOptionsTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaOptionsTestCase.java
new file mode 100644
index 0000000..b36e7af
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaOptionsTestCase.java
@@ -0,0 +1,75 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the {@link SchemaOptions} class.
+ */
+public class SchemaOptionsTestCase extends AbstractSchemaTestCase {
+    private static final int TEST_OPTION_DEFAULT_VALUE = 42;
+    private static final Option<Integer> TEST_OPTION = Option.of(Integer.class, TEST_OPTION_DEFAULT_VALUE);
+
+    @DataProvider
+    private Object[][] defaultSchemaOptions() {
+        return new Object[][] {
+            { ALLOW_MALFORMED_CERTIFICATES },
+            { ALLOW_MALFORMED_JPEG_PHOTOS },
+            { ALLOW_MALFORMED_NAMES_AND_OPTIONS },
+            { ALLOW_NON_STANDARD_TELEPHONE_NUMBERS },
+            { ALLOW_ZERO_LENGTH_DIRECTORY_STRINGS },
+            { STRICT_FORMAT_FOR_COUNTRY_STRINGS },
+            { STRIP_UPPER_BOUND_FOR_ATTRIBUTE_TYPE }};
+    }
+
+    @Test(dataProvider = "defaultSchemaOptions")
+    public void testDefaultSchemaOptions(Option<?> option) {
+        Options defaultOptions = Options.defaultOptions();
+        assertThat(new SchemaBuilder().getOptions().get(option)).isEqualTo(defaultOptions.get(option));
+    }
+
+    @Test
+    public void testAddSchemaOption() {
+        assertThat(newSchemaBuilder().getOptions().get(TEST_OPTION)).isEqualTo(TEST_OPTION_DEFAULT_VALUE);
+    }
+
+    @Test
+    public void testSetSchemaOption() {
+        assertThat(newSchemaBuilder().setOption(TEST_OPTION, 0).getOptions().get(TEST_OPTION)).isEqualTo(0);
+    }
+
+    @Test
+    public void testSchemaOptionsCopy() {
+        final Options copiedOptions = Options.copyOf(newSchemaBuilder().getOptions());
+        assertThat(copiedOptions.get(TEST_OPTION)).isEqualTo(TEST_OPTION_DEFAULT_VALUE);
+    }
+
+    @Test(expectedExceptions = UnsupportedOperationException.class)
+    public void testAsReadOnlyOptions() {
+        Options.unmodifiableCopyOf(new SchemaBuilder().getOptions()).set(ALLOW_MALFORMED_CERTIFICATES, false);
+    }
+
+    private SchemaBuilder newSchemaBuilder() {
+        return new SchemaBuilder().setOption(TEST_OPTION, TEST_OPTION_DEFAULT_VALUE);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaTestCase.java
new file mode 100644
index 0000000..5a39946
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaTestCase.java
@@ -0,0 +1,114 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/** Tests the Schema class. */
+@SuppressWarnings("javadoc")
+public class SchemaTestCase extends AbstractSchemaTestCase {
+    @Test(description = "Unit test for OPENDJ-1477")
+    public void asNonStrictSchemaAlwaysReturnsSameInstance() {
+        final Schema schema = Schema.getCoreSchema();
+        final Schema nonStrictSchema1 = schema.asNonStrictSchema();
+        final Schema nonStrictSchema2 =
+                schema.asNonStrictSchema().asStrictSchema().asNonStrictSchema();
+        assertThat(nonStrictSchema1).isSameAs(nonStrictSchema2);
+    }
+
+    @Test(description = "Unit test for OPENDJ-1477")
+    public void asStrictSchemaAlwaysReturnsSameInstance() {
+        final Schema schema = Schema.getCoreSchema();
+        final Schema strictSchema1 = schema.asStrictSchema();
+        final Schema strictSchema2 = schema.asStrictSchema().asNonStrictSchema().asStrictSchema();
+        assertThat(strictSchema1).isSameAs(strictSchema2);
+    }
+
+    /**
+     * Asynchronously retrieving a simple schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testReadSchemaAsyncMethodsMockConnection() throws Exception {
+        Connection connection = mock(Connection.class);
+
+        // @formatter:off
+        final String[] entry = {
+            "# Search result entry: uid=bjensen,ou=People,dc=example,dc=com",
+            "dn: uid=bjensen,ou=People,dc=example,dc=com",
+            "subschemaSubentry: cn=schema",
+            "entryDN: uid=bjensen,ou=people,dc=example,dc=com",
+            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c"
+            // N.B : also works with previous example but needs the subschemaSubentry line.
+        };
+
+        // Send a search entry result promise :
+        LdapPromise<SearchResultEntry> result = newSuccessfulLdapPromise(Responses.newSearchResultEntry(entry));
+        when(connection.searchSingleEntryAsync((SearchRequest) any())).thenReturn(result);
+        DN testDN = DN.valueOf("uid=bjensen,ou=People,dc=example,dc=com");
+        // @formatter:on
+        Schema[] schemas = new Schema[] {
+                Schema.readSchemaAsync(connection, testDN).getOrThrow(),
+                Schema.readSchemaForEntryAsync(connection, testDN).getOrThrow()
+        };
+
+        // We retrieve the schemas :
+        for (Schema sc : schemas) {
+            assertThat(sc.getSyntaxes()).isNotNull();
+            assertThat(sc.getAttributeTypes()).isNotNull();
+            assertThat(sc.getObjectClasses()).isNotNull();
+            assertThat(sc.getMatchingRuleUses()).isNotNull();
+            assertThat(sc.getMatchingRuleUses()).isEmpty();
+            assertThat(sc.getMatchingRules()).isNotNull();
+            assertThat(sc.getDITContentRules()).isNotNull();
+            assertThat(sc.getDITContentRules()).isEmpty();
+            assertThat(sc.getDITStuctureRules()).isNotNull();
+            assertThat(sc.getDITStuctureRules()).isEmpty();
+            assertThat(sc.getNameForms()).isNotNull();
+            assertThat(sc.getNameForms()).isEmpty();
+        }
+        connection.close();
+    }
+
+    @Test
+    public void getAttributeTypeWithDifferentNamesReturnSame() throws Exception {
+        Schema schema = CoreSchema.getInstance();
+        AttributeType cnAttrType = schema.getAttributeType("cn");
+        assertThat(cnAttrType).isSameAs(schema.getAttributeType("commonname"));
+        assertThat(cnAttrType).isSameAs(schema.getAttributeType("commonName"));
+        assertThat(cnAttrType).isSameAs(schema.getAttributeType("CN"));
+    }
+
+    @Test
+    public void getAttributeTypeWithDifferentPlaceholderNames() throws Exception {
+        Schema schema = CoreSchema.getInstance().asNonStrictSchema();
+        AttributeType placeHolderAttrType = schema.getAttributeType("placeholder");
+        assertThat(placeHolderAttrType).isEqualTo(schema.getAttributeType("PLACEHOLDER"));
+        assertThat(placeHolderAttrType).isNotEqualTo(schema.getAttributeType("another_placeholder"));
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaUtilsTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaUtilsTest.java
new file mode 100644
index 0000000..217222b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SchemaUtilsTest.java
@@ -0,0 +1,252 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import org.fest.assertions.Assertions;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.SubstringReader;
+
+/**
+ * Test schema utilities.
+ */
+@SuppressWarnings("javadoc")
+public class SchemaUtilsTest extends AbstractSchemaTestCase {
+
+    @DataProvider(name = "invalidOIDs")
+    public Object[][] createInvalidOIDs() {
+        return new Object[][] { { "" }, { ".0" }, { "0." }, { "100." }, { ".999" }, { "1one" },
+            { "one+two+three" }, { "one.two.three" },
+            // AD puts quotes around OIDs - test mismatched quotes.
+            { "'0" }, { "'10" }, { "999'" }, { "0.0'" }, };
+    }
+
+    @DataProvider(name = "validOIDs")
+    public Object[][] createValidOIDs() {
+        return new Object[][] {
+            // Compliant NOIDs
+            { "0.0" }, { "1.0" }, { "2.0" }, { "3.0" }, { "4.0" }, { "5.0" }, { "6.0" }, { "7.0" },
+            { "8.0" }, { "9.0" }, { "0.1" }, { "0.2" }, { "0.3" }, { "0.4" }, { "0.5" }, { "0.6" },
+            { "0.7" }, { "0.8" }, { "0.9" }, { "10.0" }, { "100.0" }, { "999.0" }, { "0.100" },
+            { "0.999" }, { "100.100" }, { "999.999" }, { "111.22.333.44.55555.66.777.88.999" },
+            { "a" },
+            { "a2" },
+            { "a-" },
+            { "one" },
+            { "one1" },
+            { "one-two" },
+            { "one1-two2-three3" },
+            // AD puts quotes around OIDs - not compliant but we need to
+            // handle them.
+            { "'0.0'" }, { "'10.0'" }, { "'999.0'" }, { "'111.22.333.44.55555.66.777.88.999'" },
+            { "'a'" }, { "'a2'" }, { "'a-'" }, { "'one'" }, { "'one1'" }, { "'one-two'" },
+            { "'one1-two2-three3'" },
+            // Not strictly legal, but we'll be lenient with what we accept.
+            { "0" }, { "1" }, { "2" }, { "3" }, { "4" }, { "5" }, { "6" }, { "7" }, { "8" },
+            { "9" }, { "00" }, { "01" }, { "01.0" }, { "0.01" }, };
+    }
+
+    @Test(dataProvider = "invalidOIDs", expectedExceptions = DecodeException.class)
+    public void testReadOIDInvalid(final String oid) throws DecodeException {
+        final SubstringReader reader = new SubstringReader(oid);
+        SchemaUtils.readOID(reader, false);
+    }
+
+    @Test(dataProvider = "validOIDs")
+    public void testReadOIDValid(final String oid) throws DecodeException {
+        String expected = oid;
+        if (oid.startsWith("'")) {
+            expected = oid.substring(1, oid.length() - 1);
+        }
+
+        final SubstringReader reader = new SubstringReader(oid);
+        Assert.assertEquals(SchemaUtils.readOID(reader, false), expected);
+    }
+
+    @DataProvider
+    public Object[][] nonAsciiStringProvider() throws Exception {
+        final String nonAsciiChars = "ëéèêœ";
+        final String nonAsciiCharsReplacement = new String(
+                new byte[] { b(0x65), b(0xcc), b(0x88), b(0x65), b(0xcc),
+                    b(0x81), b(0x65), b(0xcc), b(0x80), b(0x65), b(0xcc),
+                    b(0x82), b(0xc5), b(0x93), }, "UTF8");
+        return new Object[][] {
+            { nonAsciiChars, false, false, nonAsciiCharsReplacement },
+            { nonAsciiChars, false, true,  nonAsciiCharsReplacement },
+            { nonAsciiChars, true,  false, nonAsciiCharsReplacement },
+            { nonAsciiChars, true,  true,  nonAsciiCharsReplacement },
+        };
+    }
+
+    @DataProvider
+    public Object[][] stringProvider() throws Exception {
+        final String allSpaceChars = "\u0009\n\u000b\u000c\r\u000e";
+        final String mappedToNothingChars = "\u007F"
+            + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008"
+            + "\u000E\u000F"
+            + "\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019"
+            + "\u001A\u001B\u001C\u001D\u001E\u001F";
+        return new Object[][] {
+            // empty always remains empty
+            { "", false, false, "" },
+            { "", false, true,  "" },
+            { "", true,  false, "" },
+            { "", true,  true,  "" },
+            // double space chars are always converted to single char
+            { "  ", false, false, " " },
+            { "  ", false, true,  " " },
+            { "  ", true,  false, " " },
+            { "  ", true,  true,  " " },
+            // trim all space chars to a single space
+            { allSpaceChars, false, false, " " },
+            { allSpaceChars, false, true,  " " },
+            { allSpaceChars, true,  false, " " },
+            { allSpaceChars, true,  true,  " " },
+            // remove chars that are not mapped to anything
+            { mappedToNothingChars, false, false, " " },
+            { mappedToNothingChars, false, true,  " " },
+            { mappedToNothingChars, true,  false, " " },
+            { mappedToNothingChars, true,  true,  " " },
+        };
+    }
+
+    /** Mixes trimming and case folding tests. */
+    @DataProvider
+    public Object[][] stringWithSpacesProvider() {
+        return new Object[][] {
+            { " this is a string ", false, false, " this is a string " },
+            { " this is a string ", false, true,  " this is a string " },
+            { " this is a string ", true,  false, "this is a string" },
+            { " this is a string ", true,  true,  "this is a string" },
+            { "   this  is    a   string  ", false, false, " this is a string " },
+            { "   this  is    a   string  ", false, true,  " this is a string " },
+            { "   this  is    a   string  ", true,  false, "this is a string" },
+            { "   this  is    a   string  ", true,  true,  "this is a string" },
+            { " THIS IS A STRING ", false, false, " THIS IS A STRING " },
+            { " THIS IS A STRING ", false, true,  " this is a string " },
+            { " THIS IS A STRING ", true,  false, "THIS IS A STRING" },
+            { " THIS IS A STRING ", true,  true,  "this is a string" },
+        };
+    }
+
+    private byte b(int i) {
+        return (byte) i;
+    }
+
+    @Test(dataProvider = "stringProvider")
+    public void testNormalizeStringProvider(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        ByteString val = ByteString.valueOfUtf8(value);
+        ByteString normValue = SchemaUtils.normalizeStringAttributeValue(val, trim, foldCase);
+        Assertions.assertThat(normValue.toString()).isEqualTo(expected);
+    }
+
+    @Test(dataProvider = "nonAsciiStringProvider")
+    public void testNormalizeStringWithNonAscii(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        testNormalizeStringProvider(value, trim, foldCase, expected);
+    }
+
+    @Test(dataProvider = "stringWithSpacesProvider")
+    public void testNormalizeStringWithSpaces(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        testNormalizeStringProvider(value, trim, foldCase, expected);
+    }
+
+    @Test(dataProvider = "stringProvider")
+    public void testNormalizeIA5String(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        ByteString val = ByteString.valueOfUtf8(value);
+        ByteString normValue = SchemaUtils.normalizeIA5StringAttributeValue(val, trim, foldCase);
+        Assertions.assertThat(normValue.toString()).isEqualTo(expected);
+    }
+
+    @Test(dataProvider = "nonAsciiStringProvider", expectedExceptions = { DecodeException.class })
+    public void testNormalizeIA5StringShouldThrowForNonAscii(
+            String value, boolean trim, boolean foldCase, String expected) throws Exception {
+        testNormalizeIA5String(value, trim, foldCase, expected);
+    }
+
+    @Test(dataProvider = "stringWithSpacesProvider")
+    public void testNormalizeIA5StringWithSpaces(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        testNormalizeIA5String(value, trim, foldCase, expected);
+    }
+
+    @Test(dataProvider = "stringProvider")
+    public void testNormalizeStringList(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        ByteString val = ByteString.valueOfUtf8(value);
+        ByteString normValue = SchemaUtils.normalizeStringListAttributeValue(val, trim, foldCase);
+        Assertions.assertThat(normValue.toString()).isEqualTo(expected);
+    }
+
+    @Test(dataProvider = "nonAsciiStringProvider")
+    public void testNormalizeStringListWithNonAscii(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        testNormalizeStringList(value, trim, foldCase, expected);
+    }
+
+    @DataProvider
+    public Object[][] stringListProvider() throws Exception {
+        return new Object[][] {
+            { "this$is$a$list", false, false, "this$is$a$list" },
+            { "this$is$a$list", false, true,  "this$is$a$list"},
+            { "this$is$a$list", true,  false, "this$is$a$list" },
+            { "this$is$a$list", true,  true,  "this$is$a$list" },
+            { "this $ is $ a $ list", false, false, "this$is$a$list" },
+            { "this $ is $ a $ list", false, true,  "this$is$a$list" },
+            { "this $ is $ a $ list", true,  false, "this$is$a$list" },
+            { "this $ is $ a $ list", true,  true,  "this$is$a$list" },
+            { "this $ is \\\\ $ a $ list", false, false, "this$is \\\\$a$list" },
+            { "this $ is \\\\ $ a $ list", false, true,  "this$is \\\\$a$list" },
+            { "this $ is \\\\ $ a $ list", true,  false, "this$is \\\\$a$list" },
+            { "this $ is \\\\ $ a $ list", true,  true,  "this$is \\\\$a$list" },
+            { "$ this $ is $ a $ list", false, false, "$this$is$a$list" },
+            { "$ this $ is $ a $ list", false, true,  "$this$is$a$list" },
+            { "$ this $ is $ a $ list", true,  false, "$this$is$a$list" },
+            { "$ this $ is $ a $ list", true,  true,  "$this$is$a$list" },
+        };
+    }
+
+    @Test(dataProvider = "stringListProvider")
+    public void testNormalizeStringListWithList(String value, boolean trim, boolean foldCase, String expected)
+            throws Exception {
+        testNormalizeStringList(value, trim, foldCase, expected);
+    }
+
+    @DataProvider
+    public Object[][] numericStringProvider() throws Exception {
+        return new Object[][] {
+            { "", "" },
+            { "   ", "" },
+            { " 123  ", "123" },
+            { " 123  456  ", "123456" },
+        };
+    }
+
+    @Test(dataProvider = "numericStringProvider")
+    public void testNormalizeNumericString(String value, String expected) throws Exception {
+        ByteString val = ByteString.valueOfUtf8(value);
+        ByteString normValue = SchemaUtils.normalizeNumericStringAttributeValue(val);
+        Assertions.assertThat(normValue.toString()).isEqualTo(expected);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstitutionSyntaxTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstitutionSyntaxTestCase.java
new file mode 100644
index 0000000..7a5bf7b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstitutionSyntaxTestCase.java
@@ -0,0 +1,102 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_IA5_STRING_OID;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Substitution syntax tests. */
+@SuppressWarnings("javadoc")
+public class SubstitutionSyntaxTestCase extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "12345678", true }, { "12345678\u2163", false }, };
+    }
+
+    @Test
+    public void testSelfSubstitute1() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.3.6.1.4.1.1466.115.121.1.15 "
+                + " DESC 'Replacing DirectorySyntax'  "
+                + " X-SUBST '1.3.6.1.4.1.1466.115.121.1.15' )", true);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test
+    public void testSelfSubstitute2() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSubstitutionSyntax("1.3.6.1.4.1.1466.115.121.1.15", "Replacing DirectorySyntax",
+                "1.3.6.1.4.1.1466.115.121.1.15", true);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testSubstituteCore1() throws ConflictingSchemaElementException {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.3.6.1.4.1.1466.115.121.1.26 "
+                + " DESC 'Replacing DirectorySyntax'  " + " X-SUBST '9.9.9' )", false);
+    }
+
+    @Test
+    public void testSubstituteCore1Override() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.3.6.1.4.1.1466.115.121.1.26 "
+                + " DESC 'Replacing DirectorySyntax'  " + " X-SUBST '9.9.9' )", true);
+    }
+
+    @Test(expectedExceptions = ConflictingSchemaElementException.class)
+    public void testSubstituteCore2() throws ConflictingSchemaElementException {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSubstitutionSyntax("1.3.6.1.4.1.1466.115.121.1.26", "Replacing DirectorySyntax",
+                "9.9.9", false);
+    }
+
+    @Test
+    public void testSubstituteCore2Override() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSubstitutionSyntax("1.3.6.1.4.1.1466.115.121.1.26", "Replacing DirectorySyntax",
+                "9.9.9", true);
+    }
+
+    @Test
+    public void testUndefinedSubstitute1() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSyntax("( 1.3.6.1.4.1.1466.115.121.1.15 "
+                + " DESC 'Replacing DirectorySyntax'  " + " X-SUBST '1.1.1' )", true);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Test
+    public void testUndefinedSubstitute2() {
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSubstitutionSyntax("1.3.6.1.4.1.1466.115.121.1.15", "Replacing DirectorySyntax",
+                "1.1.1", true);
+        Assert.assertFalse(builder.toSchema().getWarnings().isEmpty());
+    }
+
+    @Override
+    protected Syntax getRule() {
+        // Use IA5String syntax as our substitute.
+        final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+        builder.addSubstitutionSyntax("9.9.9", "Unimplemented Syntax", SYNTAX_IA5_STRING_OID, false);
+        return builder.toSchema().getSyntax("9.9.9");
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstringMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstringMatchingRuleTest.java
new file mode 100644
index 0000000..cee7db7
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SubstringMatchingRuleTest.java
@@ -0,0 +1,217 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.testng.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Abstract class for building test for the substring matching rules. This class
+ * is intended to be extended by one class for each substring matching rules.
+ */
+@SuppressWarnings("javadoc")
+public abstract class SubstringMatchingRuleTest extends AbstractSchemaTestCase {
+    /**
+     * Generate invalid assertion values for the Matching Rule test.
+     *
+     * @return the data for the EqualityMatchingRulesInvalidValuestest.
+     */
+    @DataProvider(name = "substringInvalidAssertionValues")
+    public abstract Object[][] createMatchingRuleInvalidAssertionValues();
+
+    /**
+     * Generate invalid attribute values for the Matching Rule test.
+     *
+     * @return the data for the EqualityMatchingRulesInvalidValuestest.
+     */
+    @DataProvider(name = "substringInvalidAttributeValues")
+    public abstract Object[][] createMatchingRuleInvalidAttributeValues();
+
+    /**
+     * Generate data for the test of the final string match.
+     *
+     * @return the data for the test of the final string match.
+     */
+    @DataProvider(name = "substringInitialMatchData")
+    public abstract Object[][] createSubstringFinalMatchData();
+
+    /**
+     * Generate data for the test of the initial string match.
+     *
+     * @return the data for the test of the initial string match.
+     */
+    @DataProvider(name = "substringInitialMatchData")
+    public abstract Object[][] createSubstringInitialMatchData();
+
+    /**
+     * Generate data for the test of the middle string match.
+     *
+     * @return the data for the test of the middle string match.
+     */
+    @DataProvider(name = "substringMiddleMatchData")
+    public abstract Object[][] createSubstringMiddleMatchData();
+
+    /**
+     * Test the normalization and the final substring match.
+     */
+    @Test(dataProvider = "substringFinalMatchData")
+    public void finalMatchingRules(final String value, final String finalValue,
+            final ConditionResult result) throws Exception {
+        final MatchingRule rule = getRule();
+
+        // normalize the 2 provided values and check that they are equals
+        final ByteString normalizedValue = rule.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+
+        final ConditionResult substringAssertionMatches =
+            rule.getSubstringAssertion(null, null, ByteString.valueOfUtf8(finalValue)).matches(normalizedValue);
+        final ConditionResult assertionMatches =
+            rule.getAssertion(ByteString.valueOfUtf8("*" + finalValue)).matches(normalizedValue);
+        final String message = getMessage("final", rule, value, finalValue);
+        assertEquals(substringAssertionMatches, result, message);
+        assertEquals(assertionMatches, result, message);
+    }
+
+    /**
+     * Test the normalization and the initial substring match.
+     */
+    @Test(dataProvider = "substringInitialMatchData")
+    public void initialMatchingRules(final String value, final String initial,
+            final ConditionResult result) throws Exception {
+        final MatchingRule rule = getRule();
+
+        // normalize the 2 provided values and check that they are equals
+        final ByteString normalizedValue = rule.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+
+        final ConditionResult substringAssertionMatches =
+            rule.getSubstringAssertion(ByteString.valueOfUtf8(initial), null, null).matches(normalizedValue);
+        final ConditionResult assertionMatches =
+            rule.getAssertion(ByteString.valueOfUtf8(initial + "*")).matches(normalizedValue);
+        final String message = getMessage("initial", rule, value, initial);
+        assertEquals(substringAssertionMatches, result, message);
+        assertEquals(assertionMatches, result, message);
+    }
+
+    private String getMessage(final String prefix, final MatchingRule rule,
+            final String value, final String assertionValue) {
+        return prefix + " substring matching rule " + rule
+                + " failed for values : \"" + value + "\" and \"" + assertionValue + "\".";
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "substringInvalidAssertionValues")
+    public void matchingRulesInvalidAssertionValues(final String subInitial, final String[] anys,
+            final String subFinal) throws Exception {
+        // Get the instance of the rule to be tested.
+        final MatchingRule rule = getRule();
+
+        final List<ByteSequence> anyList = new ArrayList<>(anys.length);
+        for (final String middleSub : anys) {
+            anyList.add(ByteString.valueOfUtf8(middleSub));
+        }
+        rule.getSubstringAssertion(subInitial == null ? null : ByteString.valueOfUtf8(subInitial), anyList,
+                subFinal == null ? null : ByteString.valueOfUtf8(subFinal));
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "substringInvalidAssertionValues")
+    public void matchingRulesInvalidAssertionValuesString(final String subInitial,
+            final String[] anys, final String subFinal) throws Exception {
+        // Get the instance of the rule to be tested.
+        final MatchingRule rule = getRule();
+
+        final StringBuilder assertionString = new StringBuilder();
+        if (subInitial != null) {
+            assertionString.append(subInitial);
+        }
+        assertionString.append("*");
+        for (final String middleSub : anys) {
+            assertionString.append(middleSub);
+            assertionString.append("*");
+        }
+        if (subFinal != null) {
+            assertionString.append(subFinal);
+        }
+        rule.getAssertion(ByteString.valueOfUtf8(assertionString.toString()));
+    }
+
+    /**
+     * Test the normalization and the middle substring match.
+     */
+    @Test(dataProvider = "substringMiddleMatchData")
+    public void middleMatchingRules(final String value, final String[] middleSubs,
+            final ConditionResult result) throws Exception {
+        final MatchingRule rule = getRule();
+
+        // normalize the 2 provided values and check that they are equals
+        final ByteString normalizedValue = rule.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+
+        final StringBuilder printableMiddleSubs = new StringBuilder();
+        final List<ByteSequence> middleList = new ArrayList<>(middleSubs.length);
+        printableMiddleSubs.append("*");
+        for (final String middleSub : middleSubs) {
+            printableMiddleSubs.append(middleSub);
+            printableMiddleSubs.append("*");
+            middleList.add(ByteString.valueOfUtf8(middleSub));
+        }
+
+        final ConditionResult substringAssertionMatches =
+            rule.getSubstringAssertion(null, middleList, null).matches(normalizedValue);
+        final ConditionResult assertionMatches =
+            rule.getAssertion(ByteString.valueOfUtf8(printableMiddleSubs)).matches(normalizedValue);
+        final String message = getMessage("middle", rule, value, printableMiddleSubs.toString());
+        assertEquals(substringAssertionMatches, result, message);
+        assertEquals(assertionMatches, result, message);
+    }
+
+    /**
+     * Test that invalid values are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class,
+            dataProvider = "substringInvalidAttributeValues")
+    public void substringInvalidAttributeValues(final String value) throws Exception {
+        // Get the instance of the rule to be tested.
+        final MatchingRule rule = getRule();
+
+        rule.normalizeAttributeValue(ByteString.valueOfUtf8(value));
+    }
+
+    /**
+     * Get an instance of the matching rule.
+     *
+     * @return An instance of the matching rule to test.
+     */
+    protected abstract MatchingRule getRule();
+
+    protected String[] strings(String... strings) {
+        return strings;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SyntaxTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SyntaxTestCase.java
new file mode 100644
index 0000000..f402050
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/SyntaxTestCase.java
@@ -0,0 +1,598 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+
+package org.forgerock.opendj.ldap.schema;
+
+import org.forgerock.opendj.ldap.schema.Syntax.Builder;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+/**
+ * This class tests the Syntax class.
+ */
+@SuppressWarnings("javadoc")
+public class SyntaxTestCase extends AbstractSchemaTestCase {
+
+    @Test
+    public final void testBuilderCreatesCustomSyntax() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildSyntax("1.9.1.2.3")
+                .description("Security Label")
+                .extraProperties("X-TEST", "1", "2", "3")
+                .implementation(new DirectoryStringSyntaxImpl())
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+        assertThat(schema.getWarnings()).isEmpty();
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("Security Label");
+        assertThat(syntax.getExtraProperties().get("X-TEST")).hasSize(3);
+        assertThat(syntax.getApproximateMatchingRule().getNameOrOID()).isEqualTo("ds-mr-double-metaphone-approx");
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreSubstringsMatch");
+        assertThat(syntax.toString()).isEqualTo("( 1.9.1.2.3 DESC 'Security Label' X-TEST ( '1' '2' '3' ) )");
+        assertThat(syntax.isHumanReadable()).isTrue();
+        assertThat(syntax.isBEREncodingRequired()).isFalse();
+    }
+
+    /**
+     * Tests that unrecognized syntaxes are automatically substituted with the default syntax during building.
+     */
+    @Test
+    public final void testBuilderSubstitutesUnknownSyntaxWithDefaultSyntax() {
+        final SchemaBuilder sb = new SchemaBuilder(Schema.getCoreSchema());
+        sb.buildSyntax("1.2.3.4.5").addToSchema();
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).hasSize(1);
+        final Syntax syntax = schema.getSyntax("1.2.3.4.5");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEmpty();
+        assertThat(syntax.getApproximateMatchingRule()).isNull();
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isNull();
+    }
+
+    /**
+     * Tests that unrecognized syntaxes are automatically substituted with the default syntax and matching rule.
+     */
+    @Test
+    public final void testDefaultSyntaxSubstitution() {
+        final Syntax syntax = Schema.getCoreSchema().getSyntax("1.2.3.4.5");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEmpty();
+        // Dynamically created syntaxes include the X-SUBST extension.
+        assertThat(syntax.getExtraProperties().get("X-SUBST").get(0)).isEqualTo("1.3.6.1.4.1.1466.115.121.1.40");
+        assertThat(syntax.getApproximateMatchingRule()).isNull();
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isNull();
+    }
+
+    /**
+     * The builder requires an OID or throw an exception.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesNotAllowEmptyOid() {
+        new SchemaBuilder(Schema.getCoreSchema()).buildSyntax("").addToSchema();
+    }
+
+    /**
+     * The builder requires an OID or throw an exception.
+     */
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public final void testBuilderDoesNotAllowNullOid() {
+        new SchemaBuilder(Schema.getCoreSchema()).buildSyntax((String) null).addToSchema();
+    }
+
+    /**
+     * When syntax is missing, the default one is set. Actual default is OctetString.(case match)
+     */
+    @Test
+    public final void testBuilderAllowsNullSyntax() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").implementation(null).addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("It will be substituted by the default syntax");
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getApproximateMatchingRule()).isEqualTo(null);
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isEqualTo(null);
+        assertThat(syntax.toString()).isEqualTo("( 1.9.1.2.3 )");
+    }
+
+    /**
+     * When syntax is missing, the default one is set. Actual default is OctetString.(case match)
+     */
+    @Test
+    public final void testBuilderAllowsNoSyntax() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("It will be substituted by the default syntax");
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getApproximateMatchingRule()).isEqualTo(null);
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isEqualTo(null);
+        assertThat(syntax.toString()).isEqualTo("( 1.9.1.2.3 )");
+    }
+
+    /**
+     * When syntax is missing, the default one is set. Actual default is set to directory string
+     * (1.3.6.1.4.1.1466.115.121.1.15) - matchingRules of caseIgnoreMatch and caseIgnoreSubstringsMatch.
+     */
+    @Test
+    public final void testBuilderAllowsNoSyntaxCaseWhereDefaultSyntaxIsChanged() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .setOption(DEFAULT_SYNTAX_OID, "1.3.6.1.4.1.1466.115.121.1.15")
+                .buildSyntax("1.9.1.2.3").addToSchema()
+                .toSchema();
+
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getWarnings().toString()).contains("It will be substituted by the default syntax");
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getApproximateMatchingRule().getNameOrOID()).isEqualTo("ds-mr-double-metaphone-approx");
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreSubstringsMatch");
+        assertThat(syntax.toString()).isEqualTo("( 1.9.1.2.3 )");
+    }
+
+    /**
+     * The builder allows a missing description.
+     */
+    @Test
+    public final void testBuilderAllowsNoDescription() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").addToSchema()
+                .toSchema();
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("");
+    }
+
+    /**
+     * The builder allows a missing description.
+     */
+    @Test
+    public final void testBuilderAllowsNullDescription() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").description(null).addToSchema()
+                .toSchema();
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("");
+    }
+
+    /**
+     * The builder allows a missing description.
+     */
+    @Test
+    public final void testBuilderAllowsEmptyDescription() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").description("").addToSchema()
+                .toSchema();
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("");
+    }
+
+    /**
+     * Extra properties is not a mandatory field.
+     */
+    @Test
+    public final void testBuilderAllowsNoExtraProperties() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3").addToSchema()
+                .toSchema();
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getExtraProperties().isEmpty()).isTrue();
+    }
+
+    /**
+     * Extra properties set to null is not allowed.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testBuilderDoesNotAllowNullExtraProperties() {
+        new SchemaBuilder(Schema.getCoreSchema()).buildSyntax("1.9.1.2.3").extraProperties(null);
+    }
+
+    /**
+     * Removes all the extra properties.
+     */
+    @Test
+    public final void testBuilderRemoveExtraProperties() {
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3")
+                .extraProperties("X-ENUM", "1", "2", "3").removeAllExtraProperties().addToSchema()
+                .toSchema();
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getExtraProperties().isEmpty()).isTrue();
+    }
+
+    /**
+     * Removes specified extra properties.
+     */
+    @Test
+    public final void testBuilderRemoveSpecifiedExtraProperties() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildSyntax("1.9.1.2.3")
+                .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+                .extraProperties("X-ORIGIN", "Sam Carter")
+                .removeExtraProperty("X-ENUM", "top-secret")
+                .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        final Syntax syntax = schema.getSyntax("1.9.1.2.3");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getExtraProperties().isEmpty()).isFalse();
+        assertThat(syntax.getExtraProperties().get("X-ENUM").size()).isEqualTo(2);
+        assertThat(syntax.getExtraProperties().get("X-ORIGIN").size()).isEqualTo(1);
+    }
+
+    /**
+     * Sets a syntax using a string definition.
+     */
+    @Test
+    public final void testAddingBERSyntaxDefinition() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition = "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'X.509 Certificate' )";
+
+        sb.addSyntax(definition, true);
+
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        final Syntax syntax = schema.getSyntax("1.3.6.1.4.1.1466.115.121.1.8");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("X.509 Certificate");
+        assertThat(syntax.getExtraProperties().isEmpty()).isTrue();
+        assertThat(syntax.getApproximateMatchingRule()).isNull();
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("certificateExactMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isNull();
+        assertThat(syntax.isBEREncodingRequired()).isTrue();
+        assertThat(syntax.isHumanReadable()).isFalse();
+    }
+
+    /**
+     * Sets a syntax using a string definition.
+     */
+    @Test
+    public final void testAddingASyntaxDefinitionStringOverride() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition = "( 1.3.6.1.4.1.4203.1.1.2 DESC 'Authentication Password Syntaxe'"
+                + " X-ORIGIN 'RFC 4512' )";
+
+        sb.addSyntax(definition, true);
+
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isEmpty();
+        final Syntax syntax = schema.getSyntax("1.3.6.1.4.1.4203.1.1.2");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("Authentication Password Syntaxe");
+        assertThat(syntax.getExtraProperties().isEmpty()).isFalse();
+        assertThat(syntax.getExtraProperties().get("X-ORIGIN").get(0)).isEqualTo("RFC 4512");
+        assertThat(syntax.getApproximateMatchingRule()).isNull();
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("authPasswordExactMatch");
+        assertThat(syntax.getOrderingMatchingRule()).isNull();
+        assertThat(syntax.getSubstringMatchingRule()).isNull();
+    }
+
+    /**
+     * Sets a syntax using a string definition.
+     */
+    @Test
+    public final void testAddingUnknownSyntaxDefinitionString() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition = "( 1.3.6.1.4.1.4203.1.1.9999 DESC 'Custom Authentication Password'"
+                + " X-ORIGIN 'None' )";
+
+        sb.addSyntax(definition, false);
+
+        final Schema schema = sb.toSchema();
+        assertThat(schema.getWarnings()).isNotEmpty();
+        final Syntax syntax = schema.getSyntax("1.3.6.1.4.1.4203.1.1.9999");
+        assertThat(syntax).isNotNull();
+        assertThat(syntax.getDescription()).isEqualTo("Custom Authentication Password");
+        assertThat(syntax.getExtraProperties().isEmpty()).isFalse();
+        assertThat(syntax.getExtraProperties().get("X-ORIGIN").get(0)).isEqualTo("None");
+        assertThat(syntax.getApproximateMatchingRule()).isNull();
+        assertThat(syntax.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(syntax.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(syntax.getSubstringMatchingRule()).isNull();
+    }
+
+    /**
+     * Duplicates a syntax.
+     */
+    @Test
+    public final void testBuilderDuplicatesExistingSyntax() {
+        final Schema schema1 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.2.3.4.5.6").description("v1").addToSchema()
+                .toSchema();
+
+        final Syntax syntax1 = schema1.getSyntax("1.2.3.4.5.6");
+        assertThat(syntax1.getDescription()).isEqualTo("v1");
+
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax(syntax1).description("v2").addToSchema()
+                .toSchema();
+
+        final Syntax syntax2 = schema2.getSyntax("1.2.3.4.5.6");
+        assertThat(syntax2.getDescription()).isEqualTo("v2");
+        assertThat(syntax2.toString()).isEqualTo("( 1.2.3.4.5.6 DESC 'v2' )");
+    }
+
+    /**
+     * Another duplicated syntax example.
+     */
+    @Test
+    public final void testBuilderDuplicatesSyntax() {
+        final SchemaBuilder sb = new SchemaBuilder();
+        sb.addSchema(Schema.getCoreSchema(), false);
+        // @formatter:off
+        final Builder nfb = new Builder("1.2.3.4.5.9999", sb);
+        nfb.description("Description of the new syntax")
+            .extraProperties("X-ORIGIN", "SyntaxCheckingTestCase")
+            .addToSchema();
+
+        Schema schema = sb.toSchema();
+        assertThat(schema.getSyntaxes()).isNotEmpty();
+        final Syntax syntax = schema.getSyntax("1.2.3.4.5.9999");
+        assertThat(syntax.getDescription()).isEqualTo("Description of the new syntax");
+
+        sb.buildSyntax(syntax)
+            .oid("1.2.3.4.5.99996")
+            .extraProperties("X-ORIGIN", "Unknown")
+            .addToSchema();
+        schema = sb.toSchema();
+        assertThat(schema.getSyntaxes()).isNotEmpty();
+        // The duplicated syntax.
+        final Syntax dolly = schema.getSyntax("1.2.3.4.5.99996");
+        assertThat(dolly.getDescription()).isEqualTo("Description of the new syntax");
+        assertThat(dolly.getExtraProperties().size()).isEqualTo(1);
+        assertThat(dolly.getExtraProperties().get("X-ORIGIN").get(0)).isEqualTo("SyntaxCheckingTestCase");
+        assertThat(dolly.getExtraProperties().get("X-ORIGIN").get(1)).isEqualTo("Unknown");
+        assertThat(dolly.getExtraProperties().get("X-ORIGIN").size()).isEqualTo(2);
+
+        // The original hasn't changed.
+        final Syntax originalSyntax = schema.getSyntax("1.2.3.4.5.9999");
+        assertThat(originalSyntax.getDescription()).isEqualTo("Description of the new syntax");
+        assertThat(originalSyntax.getExtraProperties().get("X-ORIGIN").size()).isEqualTo(1);
+    }
+
+    /**
+     * Equality between syntaxes.
+     */
+    @Test
+    public final void testBuilderSyntaxesEqualsTrue() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3")
+                    .description("Security Label")
+                    .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+                    .implementation(new DirectoryStringSyntaxImpl())
+                    .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        final Syntax syntax1 = schema.getSyntax("1.9.1.2.3");
+
+        // @formatter:off
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3")
+                    .description("Security Label")
+                    .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+                    .implementation(new DirectoryStringSyntaxImpl())
+                    .addToSchema()
+                .toSchema();
+        // @formatter:on
+        final Syntax syntax2 = schema2.getSyntax("1.9.1.2.3");
+
+        assertThat(syntax1).isEqualTo(syntax2);
+    }
+
+    /**
+     * Equality between syntaxes.
+     */
+    @Test
+    public final void testBuilderSyntaxesEqualsFalse() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.3")
+                    .description("Security Label")
+                    .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+                    .implementation(new DirectoryStringSyntaxImpl())
+                    .addToSchema()
+                .toSchema();
+        // @formatter:on
+
+        final Syntax syntax1 = schema.getSyntax("1.9.1.2.3");
+
+        // @formatter:off
+        final Schema schema2 = new SchemaBuilder(Schema.getCoreSchema())
+                .buildSyntax("1.9.1.2.4")
+                    .description("Security Label")
+                    .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+                    .implementation(new DirectoryStringSyntaxImpl())
+                    .addToSchema()
+                .toSchema();
+        // @formatter:on
+        final Syntax syntax2 = schema2.getSyntax("1.9.1.2.4");
+
+        assertThat(syntax1).isNotEqualTo(syntax2);
+    }
+
+    /**
+     * Equality between builder and definition.
+     */
+    @Test
+    public final void testBuilderEqualityReturnsTrueBetweenBuilderAndDefinition() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildSyntax("1.9.1.2.3")
+            .description("Security Label")
+            .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+            .implementation(new DirectoryStringSyntaxImpl())
+            .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final Syntax syntax1 = schema.getSyntax("1.9.1.2.3");
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+
+        final String definition =
+                "( 1.9.1.2.3 DESC 'Security Label' X-ENUM ( 'top-secret' 'secret' 'confidential' ) )";
+
+        sb2.addSyntax(definition, false);
+        final Syntax syntax2 = sb2.toSchema().getSyntax("1.9.1.2.3");
+        assertThat(syntax1).isEqualTo(syntax2);
+    }
+
+    /**
+     * Equality between builder and definition fails.
+     */
+    @Test
+    public final void testBuilderEqualityReturnsFalseBetweenBuilderAndDefinition() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildSyntax("1.9.1.2.3")
+            .description("Security Label")
+            .extraProperties("X-ENUM", "top-secret", "secret", "confidential")
+            .implementation(new DirectoryStringSyntaxImpl())
+            .addToSchema()
+            .toSchema();
+        // @formatter:on
+
+        assertThat(schema.getWarnings()).isEmpty();
+        final Syntax syntax1 = schema.getSyntax("1.9.1.2.3");
+
+        final SchemaBuilder sb2 = new SchemaBuilder();
+        sb2.addSchema(Schema.getCoreSchema(), false);
+        final String definition =
+                "( 1.9.1.2.4 DESC 'Security Label II' X-ENUM ( 'top-secret' 'secret' 'confidential' ) )";
+
+        sb2.addSyntax(definition, false);
+
+        final Syntax syntax2 = sb2.toSchema().getSyntax("1.9.1.2.4");
+        assertThat(syntax1).isNotEqualTo(syntax2);
+    }
+
+    /**
+     * The builder allows to create chained syntaxes.
+     */
+    @Test
+    public final void testBuilderCreatesSyntaxesUsingChainingMethods() {
+        // @formatter:off
+        final Schema schema = new SchemaBuilder(Schema.getCoreSchema())
+            .buildSyntax("1.9.1.2.3")
+                .description("Security Label")
+                .extraProperties("X-TEST", "1", "2", "3")
+                .implementation(new DirectoryStringSyntaxImpl())
+                .addToSchema()
+            .buildSyntax("1.9.1.2.4")
+                .description("Security Label II")
+                .extraProperties("X-TEST", "private")
+                .addToSchema()
+            .buildSyntax("non-implemented-syntax-oid")
+                .description("Not Implemented in OpenDJ")
+                .extraProperties("X-SUBST", "1.3.6.1.4.1.1466.115.121.1.15")
+                .implementation(null)
+                .addToSchema()
+            .buildSyntax("1.3.6.1.4.1.4203.1.1.2")
+                .description("Authentication Password Syntax")
+                .extraProperties("X-ORIGIN", "RFC 4512")
+                .implementation(new OctetStringSyntaxImpl())
+                .addToSchemaOverwrite()
+            .toSchema();
+        // @formatter:on
+
+
+        // Warning should be found as the syntax implementation for s2 is not specified.
+        assertThat(schema.getWarnings()).isNotEmpty();
+        assertThat(schema.getDefaultSyntax().getOID()).isEqualTo("1.3.6.1.4.1.1466.115.121.1.40"); // OctetString OID.
+
+        // First
+        final Syntax s1 = schema.getSyntax("1.9.1.2.3");
+        assertThat(s1.getDescription()).isEqualTo("Security Label");
+        assertThat(s1.getApproximateMatchingRule().getNameOrOID()).isEqualTo("ds-mr-double-metaphone-approx");
+        assertThat(s1.getEqualityMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreMatch");
+        assertThat(s1.getOrderingMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreOrderingMatch");
+        assertThat(s1.getSubstringMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreSubstringsMatch");
+        assertThat(s1.getExtraProperties().get("X-TEST")).hasSize(3);
+
+        // Second
+        final Syntax s2 = schema.getSyntax("1.9.1.2.4");
+        assertThat(s2.getDescription()).isEqualTo("Security Label II");
+        assertThat(s2.getExtraProperties().get("X-TEST")).hasSize(1);
+        assertThat(s2.getApproximateMatchingRule()).isEqualTo(null);
+        assertThat(s2.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(s2.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(s2.getSubstringMatchingRule()).isEqualTo(null);
+
+        // Third
+        final Syntax s3 = schema.getSyntax("non-implemented-syntax-oid");
+        assertThat(s3.getDescription()).isEqualTo("Not Implemented in OpenDJ");
+        assertThat(s3.getExtraProperties().get("X-SUBST").size()).isEqualTo(1);
+        // The default syntax is substitute as directory string.
+        assertThat(s1.getApproximateMatchingRule().getNameOrOID()).isEqualTo("ds-mr-double-metaphone-approx");
+        assertThat(s1.getEqualityMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreMatch");
+        assertThat(s1.getOrderingMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreOrderingMatch");
+        assertThat(s1.getSubstringMatchingRule().getNameOrOID()).isEqualTo("caseIgnoreSubstringsMatch");
+
+        // Last
+        final Syntax s4 = schema.getSyntax("1.3.6.1.4.1.4203.1.1.2");
+        assertThat(s4.getDescription()).isEqualTo("Authentication Password Syntax");
+        assertThat(s4.getApproximateMatchingRule()).isEqualTo(null);
+        assertThat(s4.getEqualityMatchingRule().getNameOrOID()).isEqualTo("octetStringMatch");
+        assertThat(s4.getOrderingMatchingRule().getNameOrOID()).isEqualTo("octetStringOrderingMatch");
+        assertThat(s4.getSubstringMatchingRule()).isEqualTo(null);
+        assertThat(s4.getExtraProperties().get("X-ORIGIN").size()).isEqualTo(1);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxTest.java
new file mode 100644
index 0000000..0da8a74
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelephoneNumberSyntaxTest.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.Schema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Telephone number syntax tests. */
+@Test
+public class TelephoneNumberSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            { "+61 3 9896 7830", true },
+            { "+1 512 315 0280", true },
+            { "+1-512-315-0280", true },
+            { "3 9896 7830", false },
+            { "+1+512 315 0280", false },
+            { "+1x512x315x0280", false },
+            { "   ", false },
+            { "", false } };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        SchemaBuilder builder = new SchemaBuilder(getCoreSchema()).setOption(ALLOW_NON_STANDARD_TELEPHONE_NUMBERS,
+                false);
+        return builder.toSchema().getSyntax(SYNTAX_TELEPHONE_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelexSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelexSyntaxTest.java
new file mode 100644
index 0000000..9bb0495
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/TelexSyntaxTest.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_TELEX_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Telex syntax tests. */
+@Test
+public class TelexSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            { "123$france$456", true },
+            { "abcdefghijk$lmnopqr$stuvwxyz", true },
+            { "12345$67890$()+,-./:? ", true }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_TELEX_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxTest.java
new file mode 100644
index 0000000..d2da45c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UTCTimeSyntaxTest.java
@@ -0,0 +1,172 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_UTC_TIME_OID;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** UTC time syntax tests. */
+public class UTCTimeSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] {
+            // tests for the UTC time syntax.
+            { "060906135030+01", true }, { "0609061350Z", true }, { "060906135030Z", true },
+            { "061116135030Z", true }, { "061126135030Z", true }, { "061231235959Z", true },
+            { "060906135030+0101", true }, { "060906135030+2359", true },
+            { "060906135060+0101", true }, { "060906135061+0101", false },
+            { "060906135030+3359", false }, { "060906135030+2389", false },
+            { "062231235959Z", false }, { "061232235959Z", false }, { "06123123595aZ", false },
+            { "0a1231235959Z", false }, { "06j231235959Z", false }, { "0612-1235959Z", false },
+            { "061231#35959Z", false }, { "2006", false }, { "062106135030+0101", false },
+            { "060A06135030+0101", false }, { "061A06135030+0101", false },
+            { "060936135030+0101", false }, { "06090A135030+0101", false },
+            { "06091A135030+0101", false }, { "060900135030+0101", false },
+            { "060906335030+0101", false }, { "0609061A5030+0101", false },
+            { "0609062A5030+0101", false }, { "060906137030+0101", false },
+            { "060906135A30+0101", false }, { "060906135", false }, { "0609061350", false },
+            { "060906135070+0101", false }, { "06090613503A+0101", false },
+            { "06090613503", false }, { "0609061350Z0", false }, { "0609061350+0", false },
+            { "0609061350+000A", false }, { "0609061350+A00A", false },
+            { "060906135030Z0", false }, { "060906135030+010", false },
+            { "060906135030+010A", false }, { "060906135030+0A01", false },
+            { "060906135030+2501", false }, { "060906135030+0170", false },
+            { "060906135030+010A", false }, { "060906135030+A00A", false },
+            { "060906135030Q", false }, { "060906135030+", false }, };
+    }
+
+    /**
+     * Tests the {@code createUTCTimeValue} and {@code decodeUTCTimeValue}
+     * methods.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testCreateAndDecodeUTCTimeValue() throws Exception {
+        final Date d = new Date();
+        final String timeValue = UTCTimeSyntaxImpl.createUTCTimeValue(d);
+        final Date decodedDate = UTCTimeSyntaxImpl.decodeUTCTimeValue(timeValue);
+
+        // UTCTime does not have support for sub-second values, so we need
+        // to make
+        // sure that the decoded value is within 1000 milliseconds.
+        assertTrue(Math.abs(d.getTime() - decodedDate.getTime()) < 1000);
+    }
+
+    /**
+     * Tests the {@code decodeUTCTimeValue} method decodes
+     * 50-99 into 1950-1999. See RFC 3280 4.1.2.5.1.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecode50to99() throws Exception {
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+
+        // values from 50 through 99 inclusive shall have 1900 added to it
+        for (int yy = 50; yy <= 99; yy++) {
+            String utcString = String.format("%02d0819120000Z", yy);
+            Date decodedDate = UTCTimeSyntaxImpl.decodeUTCTimeValue(utcString);
+            cal.clear();
+            cal.setTime(decodedDate);
+            int year = cal.get(Calendar.YEAR);
+            assertEquals(year, yy + 1900);
+        }
+    }
+
+    /**
+     * Tests the {@code decodeUTCTimeValue} method decodes
+     * 00-49 into 2000-2049. See RFC 3280 4.1.2.5.1.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testDecode00to49() throws Exception {
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+
+        // values from 00 through 49 inclusive shall have 2000 added to it
+        for (int yy = 0; yy <= 49; yy++) {
+            String utcString = String.format("%02d0819120000Z", yy);
+            Date decodedDate = UTCTimeSyntaxImpl.decodeUTCTimeValue(utcString);
+            cal.clear();
+            cal.setTime(decodedDate);
+            int year = cal.get(Calendar.YEAR);
+            assertEquals(year, yy + 2000);
+        }
+    }
+
+    /**
+     * Tests the {@code createUTCTimeValue} method converts
+     * 1950-1999 into 50-99. See RFC 3280 4.1.2.5.1.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testCreate50to99() throws Exception {
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+
+        // values from 50 through 99 inclusive shall have 1900 added to it
+        for (int yy = 50; yy <= 99; yy++) {
+            cal.clear();
+            cal.set(1900 + yy, 7, 19, 12, 0, 0); // months are 0..11
+            Date date = cal.getTime();
+            String createdString = UTCTimeSyntaxImpl.createUTCTimeValue(date);
+            String expectedString = String.format("%02d0819120000Z", yy);
+            assertEquals(expectedString, createdString);
+        }
+    }
+
+    /**
+     * Tests the {@code createUTCTimeValue} method converts
+     * 2000-2049 into 00-49. See RFC 3280 4.1.2.5.1.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @Test
+    public void testCreate00to49() throws Exception {
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+
+        // values from 00 through 49 inclusive shall have 2000 added to it
+        for (int yy = 0; yy <= 49; yy++) {
+            cal.clear();
+            cal.set(2000 + yy, 7, 19, 12, 0, 0); // months are 0..11
+            Date date = cal.getTime();
+            String createdString = UTCTimeSyntaxImpl.createUTCTimeValue(date);
+            String expectedString = String.format("%02d0819120000Z", yy);
+            assertEquals(expectedString, createdString);
+        }
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_UTC_TIME_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..d36d388
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDEqualityMatchingRuleTest.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_UUID_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the UUIDEqualityMatchingRule. */
+public class UUIDEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            {"G2345678-9abc-def0-1234-1234567890ab"},
+            {"g2345678-9abc-def0-1234-1234567890ab"},
+            {"12345678/9abc/def0/1234/1234567890ab"},
+            {"12345678-9abc-def0-1234-1234567890a"},
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            { "12345678-9ABC-DEF0-1234-1234567890ab",
+              "12345678-9abc-def0-1234-1234567890ab", ConditionResult.TRUE },
+            { "12345678-9abc-def0-1234-1234567890ab",
+              "12345678-9abc-def0-1234-1234567890ab", ConditionResult.TRUE },
+            { "02345678-9abc-def0-1234-1234567890ab",
+              "12345678-9abc-def0-1234-1234567890ab", ConditionResult.FALSE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_UUID_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleTest.java
new file mode 100644
index 0000000..3ccf590
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDOrderingMatchingRuleTest.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_UUID_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Test the UUIDOrderingMatchingRule. */
+public class UUIDOrderingMatchingRuleTest extends OrderingMatchingRuleTest {
+    @Override
+    @DataProvider(name = "OrderingMatchingRuleInvalidValues")
+    public Object[][] createOrderingMatchingRuleInvalidValues() {
+        return new Object[][] {
+            { "G2345678-9abc-def0-1234-1234567890ab" },
+            { "g2345678-9abc-def0-1234-1234567890ab" },
+            { "12345678/9abc/def0/1234/1234567890ab" },
+            { "12345678-9abc-def0-1234-1234567890a" },
+        };
+    }
+
+    @Override
+    @DataProvider(name = "Orderingmatchingrules")
+    public Object[][] createOrderingMatchingRuleTestData() {
+        return new Object[][] {
+            { "12345678-9ABC-DEF0-1234-1234567890ab", "12345678-9abc-def0-1234-1234567890ab", 0 },
+            { "12345678-9abc-def0-1234-1234567890ab", "12345678-9abc-def0-1234-1234567890ab", 0 },
+            { "02345678-9abc-def0-1234-1234567890ab", "12345678-9abc-def0-1234-1234567890ab", -1 },
+            { "12345678-9abc-def0-1234-1234567890ab", "02345678-9abc-def0-1234-1234567890ab", 1 },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(OMR_UUID_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxTest.java
new file mode 100644
index 0000000..8d2aa27
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UUIDSyntaxTest.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_UUID_OID;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** UUID syntax tests. */
+@Test
+public class UUIDSyntaxTest extends AbstractSyntaxTestCase {
+    @Override
+    @DataProvider(name = "acceptableValues")
+    public Object[][] createAcceptableValues() {
+        return new Object[][] { { "12345678-9ABC-DEF0-1234-1234567890ab", true },
+            { "12345678-9abc-def0-1234-1234567890ab", true },
+            { "12345678-9abc-def0-1234-1234567890ab", true },
+            { "12345678-9abc-def0-1234-1234567890ab", true },
+            { "02345678-9abc-def0-1234-1234567890ab", true },
+            { "12345678-9abc-def0-1234-1234567890ab", true },
+            { "12345678-9abc-def0-1234-1234567890ab", true },
+            { "02345678-9abc-def0-1234-1234567890ab", true },
+            { "G2345678-9abc-def0-1234-1234567890ab", false },
+            { "g2345678-9abc-def0-1234-1234567890ab", false },
+            { "12345678/9abc/def0/1234/1234567890ab", false },
+            { "12345678-9abc-def0-1234-1234567890a", false }, };
+    }
+
+    @Override
+    protected Syntax getRule() {
+        return Schema.getCoreSchema().getSyntax(SYNTAX_UUID_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..6c26461
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UniqueMemberEqualityMatchingRuleTest.java
@@ -0,0 +1,53 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_UNIQUE_MEMBER_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Test the UniqueMemberEqualityMatchingRule. */
+@Test
+public class UniqueMemberEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+            {"1.3.6.1.4.1.1466.01"}
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            // non-bit string content on optional uid is tolerated
+            { "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'123'B",
+              "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'123'B", ConditionResult.TRUE },
+            { "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'0101'B",
+              "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'0101'B", ConditionResult.TRUE },
+            { "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'0101'B",
+              "1.3.6.1.4.1.1466.0=#04024869,o=Test,C=GB#'0101'B", ConditionResult.TRUE },
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_UNIQUE_MEMBER_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..b0f9a05
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/UserPasswordExactEqualityMatchingRuleTest.java
@@ -0,0 +1,44 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_USER_PASSWORD_EXACT_OID;
+
+import org.testng.annotations.DataProvider;
+
+/** Test the UserPasswordExactEqualityMatchingRule. */
+public class UserPasswordExactEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_USER_PASSWORD_EXACT_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/WordEqualityMatchingRuleTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/WordEqualityMatchingRuleTest.java
new file mode 100644
index 0000000..98540f5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/WordEqualityMatchingRuleTest.java
@@ -0,0 +1,57 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.schema;
+
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_WORD_OID;
+
+import org.forgerock.opendj.ldap.ConditionResult;
+import org.testng.annotations.DataProvider;
+
+/** Test the WordEqualityMatchingRule. */
+public class WordEqualityMatchingRuleTest extends MatchingRuleTest {
+    @Override
+    @DataProvider(name = "matchingRuleInvalidAttributeValues")
+    public Object[][] createMatchingRuleInvalidAttributeValues() {
+        return new Object[][] {
+         // all values are valid, return an empty table.
+        };
+    }
+
+    @Override
+    @DataProvider(name = "matchingrules")
+    public Object[][] createMatchingRuleTest() {
+        return new Object[][] {
+            {"first word", "first", ConditionResult.TRUE},
+            {"first,word", "first", ConditionResult.TRUE},
+            {"first  word", "first", ConditionResult.TRUE},
+            {"first#word", "first", ConditionResult.TRUE},
+            {"first.word", "first", ConditionResult.TRUE},
+            {"first/word", "first", ConditionResult.TRUE},
+            {"first$word", "first", ConditionResult.TRUE},
+            {"first+word", "first", ConditionResult.TRUE},
+            {"first-word", "first", ConditionResult.TRUE},
+            {"first=word", "first", ConditionResult.TRUE},
+            {"word", "first", ConditionResult.FALSE},
+            {"", "empty", ConditionResult.FALSE},
+            {"", "", ConditionResult.TRUE},
+        };
+    }
+
+    @Override
+    protected MatchingRule getRule() {
+        return Schema.getCoreSchema().getMatchingRule(EMR_WORD_OID);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java
new file mode 100644
index 0000000..9de0e0c
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java
@@ -0,0 +1,69 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.Promise;
+
+import static org.forgerock.util.promise.Promises.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Basic LDAP connection factory implementation to use for tests only.
+ */
+final class BasicLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
+    private final String host;
+    private final int port;
+
+    BasicLDAPConnectionFactory(final String host, final int port, final Options options) {
+        this.host = host;
+        this.port = port;
+    }
+
+    @Override
+    public void close() {
+        // nothing to do
+    }
+
+    @Override
+    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
+        return newResultPromise(mock(LDAPConnectionImpl.class));
+    }
+
+    @Override
+    public InetSocketAddress getSocketAddress() {
+        return new InetSocketAddress(host, port);
+    }
+
+    @Override
+    public String getHostName() {
+        return host;
+    }
+
+    @Override
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + host + ':' + port + ')';
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPListener.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPListener.java
new file mode 100644
index 0000000..5c0c305
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPListener.java
@@ -0,0 +1,75 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.util.Options;
+
+/**
+ * Basic LDAP listener implementation to use for tests only.
+ */
+public final class BasicLDAPListener implements LDAPListenerImpl {
+    private final ServerConnectionFactory<LDAPClientContext, Integer> connectionFactory;
+    private final InetSocketAddress socketAddress;
+
+    /**
+     * Creates a new LDAP listener implementation which does nothing.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory can be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @throws IOException
+     *             is never thrown with this do-nothing implementation
+     */
+    public BasicLDAPListener(final InetSocketAddress address,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options) throws IOException {
+        this.connectionFactory = factory;
+        this.socketAddress = address;
+    }
+
+    @Override
+    public void close() {
+        // nothing to do
+    }
+
+    @Override
+    public InetSocketAddress getSocketAddress() {
+        return socketAddress;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("LDAPListener(");
+        builder.append(getSocketAddress());
+        builder.append(')');
+        return builder.toString();
+    }
+
+    ServerConnectionFactory<LDAPClientContext, Integer> getConnectionFactory() {
+        return connectionFactory;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicTransportProvider.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicTransportProvider.java
new file mode 100644
index 0000000..1e5518d
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicTransportProvider.java
@@ -0,0 +1,56 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.util.Options;
+
+/**
+ * Provides an basic implementation of a transport provider doing nothing.
+ * This should be used for tests only.
+ * <p>
+ * To be used, this implementation must be declared in the
+ * provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}
+ * with this single line:
+ *
+ * <pre>
+ * com.forgerock.opendj.ldap.BasicTransportProvider
+ * </pre>.
+ */
+public class BasicTransportProvider implements TransportProvider {
+
+    @Override
+    public LDAPConnectionFactoryImpl getLDAPConnectionFactory(String host, int port, Options options) {
+        return new BasicLDAPConnectionFactory(host, port, options);
+    }
+
+    @Override
+    public LDAPListenerImpl getLDAPListener(
+            InetSocketAddress address, ServerConnectionFactory<LDAPClientContext, Integer> factory, Options options)
+            throws IOException {
+        return new BasicLDAPListener(address, factory, options);
+    }
+
+    @Override
+    public String getName() {
+        return "Basic";
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/ConnectionStateTest.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/ConnectionStateTest.java
new file mode 100644
index 0000000..fcff907
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/ConnectionStateTest.java
@@ -0,0 +1,269 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap.spi;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.responses.Responses.newGenericExtendedResult;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.Test;
+
+
+/**
+ * Tests for {@linkConnectionState}.
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionStateTest extends LDAPTestCase {
+    private static final LdapException ERROR = newLdapException(ResultCode.OTHER);
+    private static final LdapException LATE_ERROR = newLdapException(ResultCode.BUSY);
+    private static final ExtendedResult UNSOLICITED =
+            newGenericExtendedResult(ResultCode.OPERATIONS_ERROR);
+
+    @Test
+    public void testCloseEventInClosedState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionClosed();
+        state.notifyConnectionClosed();
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isNull();
+        verify(listener1).handleConnectionClosed();
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verify(listener2).handleConnectionClosed();
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testCloseEventInErrorState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionError(false, ERROR);
+        state.notifyConnectionClosed();
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener1).handleConnectionError(false, ERROR);
+        verify(listener1).handleConnectionClosed();
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verify(listener2).handleConnectionError(false, ERROR);
+        verify(listener2).handleConnectionClosed();
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testCloseEventInValidState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionClosed();
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isNull();
+        verify(listener1).handleConnectionClosed();
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verify(listener2).handleConnectionClosed();
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testErrorEventInClosedState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionClosed();
+        state.notifyConnectionError(false, ERROR);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isNull();
+        verify(listener1).handleConnectionClosed();
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verify(listener2).handleConnectionClosed();
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testErrorEventInErrorState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionError(false, ERROR);
+        state.notifyConnectionError(false, LATE_ERROR);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isFalse();
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener1).handleConnectionError(false, ERROR);
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener2).handleConnectionError(false, ERROR);
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testErrorEventInValidState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionError(false, ERROR);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isFalse();
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener1).handleConnectionError(false, ERROR);
+        verifyNoMoreInteractions(listener1);
+
+        // Listeners registered after event should be notified immediately.
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verify(listener2).handleConnectionError(false, ERROR);
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testRemoveEventListener() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener);
+        state.notifyConnectionError(false, ERROR);
+        verify(listener).handleConnectionError(false, ERROR);
+        state.removeConnectionEventListener(listener);
+        state.notifyConnectionClosed();
+        verifyNoMoreInteractions(listener);
+    }
+
+    @Test
+    public void testUnsolicitedNotificationEventInClosedState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionClosed();
+        state.notifyUnsolicitedNotification(UNSOLICITED);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isNull();
+        verify(listener1).handleConnectionClosed();
+        verifyNoMoreInteractions(listener1);
+    }
+
+    @Test
+    public void testUnsolicitedNotificationEventInErrorState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionError(false, ERROR);
+        state.notifyUnsolicitedNotification(UNSOLICITED);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isFalse();
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener1).handleConnectionError(false, ERROR);
+        verifyNoMoreInteractions(listener1);
+    }
+
+    @Test
+    public void testUnsolicitedNotificationEventInValidState() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener1);
+        state.notifyUnsolicitedNotification(UNSOLICITED);
+
+        assertThat(state.isValid()).isTrue();
+        assertThat(state.isClosed()).isFalse();
+        assertThat(state.getConnectionError()).isNull();
+        verify(listener1).handleUnsolicitedNotification(same(UNSOLICITED));
+        verifyNoMoreInteractions(listener1);
+
+        /*
+         * Listeners registered after event will not be notified (unsolicited
+         * notifications are not cached).
+         */
+        final ConnectionEventListener listener2 = mock(ConnectionEventListener.class);
+        state.addConnectionEventListener(listener2);
+        verifyNoMoreInteractions(listener2);
+    }
+
+    @Test
+    public void testValidState() {
+        final ConnectionState state = new ConnectionState();
+        assertThat(state.isValid()).isTrue();
+        assertThat(state.isClosed()).isFalse();
+        assertThat(state.getConnectionError()).isNull();
+    }
+
+    /**
+     * Tests that reentrant close from error listener is handled.
+     */
+    @Test
+    public void testReentrantClose() {
+        final ConnectionState state = new ConnectionState();
+        final ConnectionEventListener listener1 = mock(ConnectionEventListener.class);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                state.notifyConnectionClosed();
+                return null;
+            }
+        }).when(listener1).handleConnectionError(false, ERROR);
+
+        state.addConnectionEventListener(listener1);
+        state.notifyConnectionError(false, ERROR);
+
+        assertThat(state.isValid()).isFalse();
+        assertThat(state.isClosed()).isTrue();
+        assertThat(state.getConnectionError()).isSameAs(ERROR);
+        verify(listener1).handleConnectionError(false, ERROR);
+        verify(listener1).handleConnectionClosed();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/LDAPTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/LDAPTestCase.java
new file mode 100644
index 0000000..6e5f819
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/LDAPTestCase.java
@@ -0,0 +1,30 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.spi;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all ldap unit tests should extend. Ldap represents the
+ * classes found directly under the package com.forgerock.opendj.ldap.ldap.
+ */
+
+@Test(groups = { "precommit", "ldap", "sdk" })
+public abstract class LDAPTestCase extends ForgeRockTestCase {
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/AbstractLDIFTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/AbstractLDIFTestCase.java
new file mode 100644
index 0000000..6bd4903
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/AbstractLDIFTestCase.java
@@ -0,0 +1,33 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all LDIF unit tests should extend. LDIF represents the
+ * classes found directly under the package org.forgerock.opendj.ldif.
+ */
+
+@Test(groups = { "precommit", "types", "sdk" })
+public abstract class AbstractLDIFTestCase extends SdkTestCase {
+}
+
+
+
+
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriterTestCase.java
new file mode 100644
index 0000000..0dc42a5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionChangeRecordWriterTestCase.java
@@ -0,0 +1,388 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the ConnectionChangeRecordWriter functionality.
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionChangeRecordWriterTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * Provide a standard LDIF Change Record, valid, for tests below.
+     *
+     * @return a string containing a standard LDIF Change Record.
+     */
+    public final String[] getStandardLDIFChangeRecord() {
+        // @formatter:off
+        return new String[] {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter",
+            "cn: Samnatha Carter",
+            "givenName: Sam",
+            "objectClass: inetOrgPerson",
+            "telephoneNumber: 555 555-5555",
+            "mail: scarter@mail.org",
+            "entryDN: uid=scarter,ou=people,dc=example,dc=org",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Write a Change Record - AddRequest.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordAddRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord(Requests.newAddRequest(getStandardLDIFChangeRecord()));
+            verify(connection, times(1)).add(any(AddRequest.class));
+        }
+    }
+
+    /**
+     * The writeChangeRecord (AddRequest) doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteChangeRecordAddRequestDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord((AddRequest) null);
+        }
+    }
+
+    /**
+     * ConnectionChangeRecordWriter write a change record (in this example the
+     * ChangeRecord is an AddRequest).
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordContainingAddRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord(Requests.newChangeRecord(getStandardLDIFChangeRecord()));
+            Assert.assertTrue(Requests.newChangeRecord(getStandardLDIFChangeRecord()) instanceof AddRequest);
+            verify(connection, times(1)).add(any(AddRequest.class));
+        }
+    }
+
+    /**
+     * ConnectionChangeRecordWriter write a change record (in this example the
+     * ChangeRecord is a DeleteRequest).
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordContainingDeleteRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            // @formatter:off
+            ChangeRecord cr = Requests.newChangeRecord(
+                "dn: dc=example,dc=com",
+                "changetype: delete"
+            );
+            writer.writeChangeRecord(cr);
+            // @formatter:on
+            Assert.assertTrue(cr instanceof DeleteRequest);
+            verify(connection, times(1)).delete(any(DeleteRequest.class));
+        }
+    }
+
+    /**
+     * The writer allow only one LDIF ChangeRecord. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void testWriteChangeRecordDoesntAllowMultipleLDIF() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            // @formatter:off
+            writer.writeChangeRecord(Requests.newChangeRecord(
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:sn",
+                "sn: scarter",
+                "",
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:sn",
+                "sn: Amarr")
+            );
+            // @formatter:on
+        }
+    }
+
+    /**
+     * ConnectionChangeRecordWriter writes a ChangeRecord and an IOException
+     * occurs on the ChangeAccept call.
+     *
+     * @throws Exception
+     */
+    @SuppressWarnings("unchecked")
+    @Test(expectedExceptions = RuntimeException.class)
+    public final void testWriteChangeRecordChangeAcceptSendIOException() throws Exception {
+        Connection connection = mock(Connection.class);
+        ChangeRecord cr = mock(ChangeRecord.class);
+
+        when(cr.accept(any(ChangeRecordVisitor.class), any(ConnectionChangeRecordWriter.class)))
+                .thenAnswer(new Answer<IOException>() {
+                    @Override
+                    public IOException answer(final InvocationOnMock invocation) throws Throwable {
+                        // Execute handler and return future.
+                        final ChangeRecordVisitor<?, ?> handler =
+                                (ChangeRecordVisitor<?, ?>) invocation.getArguments()[0];
+                        if (handler != null) {
+                            // Data here if needed.
+                        }
+                        return new IOException("IOException_e_is_not_null");
+                    }
+                });
+
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord(cr);
+        }
+    }
+
+    /**
+     * ConnectionChangeRecordWriter writes a ChangeRecord and an
+     * LdapException occurs on the ChangeAccept call.
+     *
+     * @throws Exception
+     */
+    @SuppressWarnings("unchecked")
+    @Test(expectedExceptions = LdapException.class)
+    public final void testWriteChangeRecordChangeAcceptSendLdapException() throws Exception {
+        Connection connection = mock(Connection.class);
+        ChangeRecord cr = mock(ChangeRecord.class);
+
+        when(cr.accept(any(ChangeRecordVisitor.class), any(ConnectionChangeRecordWriter.class)))
+                .thenAnswer(new Answer<LdapException>() {
+                    @Override
+                    public LdapException answer(final InvocationOnMock invocation) throws Throwable {
+                        // Execute handler and return future.
+                        final ChangeRecordVisitor<?, ?> handler =
+                                (ChangeRecordVisitor<?, ?>) invocation.getArguments()[0];
+                        if (handler != null) {
+                            // Data here if needed.
+                        }
+                        return newLdapException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+                    }
+                });
+
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord(cr);
+        }
+    }
+
+    /**
+     * The writeChangeRecord (ChangeRecord) doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteChangeRecordChangeRecordDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord((ChangeRecord) null);
+        }
+    }
+
+    /**
+     * Use the ConnectionChangeRecordWriter to write a DeleteRequest.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordDeleteRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            ChangeRecord cr = Requests.newDeleteRequest(DN.valueOf("cn=scarter,dc=example,dc=com"));
+            writer.writeChangeRecord(cr);
+            verify(connection, times(1)).delete(any(DeleteRequest.class));
+        }
+    }
+
+    /**
+     * The writeChangeRecord (DeleteRequest) doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteChangeRecordDeleteRequestDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord((DeleteRequest) null);
+        }
+    }
+
+    /**
+     * Use the ConnectionChangeRecordWriter to write a ModifyDNRequest.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordModifyDNRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            // @formatter:off
+            ChangeRecord cr = Requests.newModifyDNRequest(
+                "cn=scarter,dc=example,dc=com",
+                "cn=Susan Jacobs");
+            //@formatter:on
+            writer.writeChangeRecord(cr);
+            verify(connection, times(1)).modifyDN(any(ModifyDNRequest.class));
+        }
+    }
+
+    /**
+     * The writeChangeRecord (ModifyDNRequest) doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteChangeRecordModifyDNRequestDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord((ModifyDNRequest) null);
+        }
+    }
+
+    /**
+     * Use the ConnectionChangeRecordWriter to write a ModifyRequest.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteChangeRecordModifyRequest() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            // @formatter:off
+            ChangeRecord cr = Requests.newModifyRequest(
+                    "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+                    "changetype: modify",
+                    "delete: telephonenumber",
+                    "telephonenumber: +1 408 555 1212"
+            );
+            writer.writeChangeRecord(cr);
+            // @formatter:on
+            verify(connection, times(1)).modify(any(ModifyRequest.class));
+        }
+    }
+
+    /**
+     * The writeChangeRecord (ModifyRequest) doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteChangeRecordModifyRequestDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeChangeRecord((ModifyRequest) null);
+        }
+    }
+
+    /**
+     * The writeComment do nothing. ConnectionChangeRecordWriter do not support
+     * Comment.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testWriteCommentDoNotSupportComment() throws Exception {
+        Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeComment("# A new comment");
+            verify(connection, Mockito.never()).add(any(String.class));
+            verify(connection, Mockito.never()).delete(any(String.class));
+            verify(connection, Mockito.never()).modify(any(String.class));
+            verify(connection, Mockito.never()).modifyDN(any(String.class), any(String.class));
+        }
+    }
+
+    /**
+     * The writeComment doesn't allow a null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testWriteCommentDoesntAllowNull() throws Exception {
+        final Connection connection = mock(Connection.class);
+        try (ConnectionChangeRecordWriter writer = new ConnectionChangeRecordWriter(connection)) {
+            writer.writeComment(null);
+        }
+    }
+
+    /**
+     * The ConnectionChangeRecordWriter doesn't allow a null connection.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testConnectionChangeRecordWriterDoesntAllowNull() throws Exception {
+        new ConnectionChangeRecordWriter(null);
+    }
+
+    /**
+     * Verify ConnectionChangeRecordWriter close function.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testConnectionChangeRecordWriterClose() throws Exception {
+        Connection connection = mock(Connection.class);
+        new ConnectionChangeRecordWriter(connection).close();
+        verify(connection, times(1)).close();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryReaderTestCase.java
new file mode 100644
index 0000000..733d3b5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryReaderTestCase.java
@@ -0,0 +1,265 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.util.NoSuchElementException;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.responses.Responses.*;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the ConnectionEntryReader functionality.
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionEntryReaderTestCase extends AbstractLDIFTestCase {
+
+    private static final SearchResultEntry ENTRY1 = newSearchResultEntry("cn=entry1");
+    private static final SearchResultEntry ENTRY2 = newSearchResultEntry("cn=entry2");
+    private static final SearchResultEntry ENTRY3 = newSearchResultEntry("cn=entry3");
+    private static final Result ERROR = newResult(ResultCode.BUSY);
+    private static final SearchResultReference REF =
+            newSearchResultReference("http://www.forgerock.com/");
+    private static final SearchRequest SEARCH = Requests.newSearchRequest("",
+            SearchScope.WHOLE_SUBTREE, "(objectClass=*)");
+    private static final Result SUCCESS = newResult(ResultCode.SUCCESS);
+
+    @Test
+    public final void testHasNextWhenError() throws Exception {
+        final ConnectionEntryReader reader = newReader(ERROR);
+        try {
+            reader.hasNext();
+            fail();
+        } catch (final LdapException e) {
+            assertThat(e.getResult()).isSameAs(ERROR);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadEntry() throws Exception {
+        final ConnectionEntryReader reader = newReader(ENTRY1, SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.isEntry()).isTrue();
+            assertThat(reader.isReference()).isFalse();
+            assertThat(reader.readEntry()).isSameAs(ENTRY1);
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadEntryWhenError() throws Exception {
+        final ConnectionEntryReader reader = newReader(ERROR);
+        try {
+            reader.readEntry();
+            fail();
+        } catch (final LdapException e) {
+            assertThat(e.getResult()).isSameAs(ERROR);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public final void testReadEntryWhenNoMore() throws Exception {
+        final ConnectionEntryReader reader = newReader(SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isFalse();
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadEntryWhenReference() throws Exception {
+        final ConnectionEntryReader reader = newReader(REF, SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isTrue();
+            try {
+                reader.readEntry();
+                fail();
+            } catch (final SearchResultReferenceIOException e) {
+                assertThat(e.getReference()).isSameAs(REF);
+            }
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadMultipleResults() throws Exception {
+        final ConnectionEntryReader reader = newReader(ENTRY1, ENTRY2, REF, ENTRY3, SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.readEntry()).isSameAs(ENTRY1);
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.readEntry()).isSameAs(ENTRY2);
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.readReference()).isSameAs(REF);
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.readEntry()).isSameAs(ENTRY3);
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadReference() throws Exception {
+        final ConnectionEntryReader reader = newReader(REF, SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.isEntry()).isFalse();
+            assertThat(reader.isReference()).isTrue();
+            assertThat(reader.readReference()).isSameAs(REF);
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadReferenceWhenEntry() throws Exception {
+        final ConnectionEntryReader reader = newReader(ENTRY1, SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isTrue();
+            assertThat(reader.readReference()).isNull();
+            assertThat(reader.readEntry()).isSameAs(ENTRY1);
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadReferenceWhenError() throws Exception {
+        final ConnectionEntryReader reader = newReader(ERROR);
+        try {
+            reader.readReference();
+            fail();
+        } catch (final LdapException e) {
+            assertThat(e.getResult()).isSameAs(ERROR);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public final void testReadReferenceWhenNoMore() throws Exception {
+        final ConnectionEntryReader reader = newReader(SUCCESS);
+        try {
+            assertThat(reader.hasNext()).isFalse();
+            reader.readReference();
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadResult() throws Exception {
+        final ConnectionEntryReader reader = newReader(SUCCESS);
+        try {
+            assertThat(reader.readResult()).isSameAs(SUCCESS);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test
+    public final void testReadResultWhenError() throws Exception {
+        final ConnectionEntryReader reader = newReader(ERROR);
+        try {
+            reader.readResult();
+            fail();
+        } catch (final LdapException e) {
+            assertThat(e.getResult()).isSameAs(ERROR);
+        } finally {
+            reader.close();
+        }
+    }
+
+    @Test(expectedExceptions = IllegalStateException.class)
+    public final void testReadResultWhenEntry() throws Exception {
+        final ConnectionEntryReader reader = newReader(ENTRY1, SUCCESS);
+        try {
+            reader.readResult();
+        } finally {
+            reader.close();
+        }
+    }
+
+    private ConnectionEntryReader newReader(final Object... responses) {
+        final Connection connection = mock(Connection.class);
+        // @formatter:off
+        when(connection.searchAsync(same(SEARCH), any(SearchResultHandler.class))).thenAnswer(
+            new Answer<LdapPromise<Result>>() {
+                @Override
+                public LdapPromise<Result> answer(final InvocationOnMock invocation) throws Throwable {
+                    // Execute handler and return future.
+                    final SearchResultHandler handler = (SearchResultHandler) invocation.getArguments()[1];
+                    if (handler != null) {
+                        for (final Object response : responses) {
+                            if (response instanceof SearchResultEntry) {
+                                handler.handleEntry((SearchResultEntry) response);
+                            } else if (response instanceof SearchResultReference) {
+                                handler.handleReference((SearchResultReference) response);
+                            }
+                        }
+                    }
+                    final Result result = (Result) responses[responses.length - 1];
+                    if (result.isSuccess()) {
+                        return newSuccessfulLdapPromise(result);
+                    } else {
+                        return newFailedLdapPromise(newLdapException(result));
+                    }
+                }
+            });
+        // @formatter:on
+        return new ConnectionEntryReader(connection, SEARCH);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryWriterTestCase.java
new file mode 100644
index 0000000..f0502a5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/ConnectionEntryWriterTestCase.java
@@ -0,0 +1,110 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011 ForgeRock AS.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the ConnectionEntryWriter functionality.
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionEntryWriterTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * ConnectionEntryWriter writes entry to the directory server.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testConnectionEntryWriterWritesEntry() throws Exception {
+        Connection connection = mock(Connection.class);
+
+        final Entry entry =
+                new LinkedHashMapEntry("cn=scarter,dc=example,dc=com").addAttribute("objectclass",
+                        "top").addAttribute("objectclass", "person").addAttribute("objectclass",
+                        "organizationalPerson").addAttribute("objectclass", "inetOrgPerson")
+                        .addAttribute("mail", "subgenius@example.com").addAttribute("sn", "carter");
+
+        when(connection.add(any(Entry.class))).thenAnswer(new Answer<Result>() {
+            @Override
+            public Result answer(final InvocationOnMock invocation) throws Throwable {
+                // Execute handler and return future.
+                final Entry handler = (Entry) invocation.getArguments()[0];
+                if (handler != null) {
+                    Assert.assertEquals(handler.getName().toString(), "cn=scarter,dc=example,dc=com");
+                    Assert.assertEquals(handler.getAttribute("sn").firstValueAsString(), "carter");
+                    Assert.assertEquals(handler.getAttributeCount(), 3);
+                }
+                return Responses.newResult(ResultCode.SUCCESS);
+            }
+        });
+
+        try (ConnectionEntryWriter writer = new ConnectionEntryWriter(connection)) {
+            writer.writeComment("This is a test for the ConnectionEntryWriter");
+            writer.writeEntry(entry);
+            verify(connection, times(1)).add(any(Entry.class));
+        }
+    }
+
+    /**
+     * The ConnectionEntryWriter doesn't allow a null comment.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testConnectionEntryWriterDoesntAllowNullComment() throws Exception {
+        try (ConnectionEntryWriter writer = new ConnectionEntryWriter(null)) {
+            writer.writeComment(null);
+        }
+    }
+
+    /**
+     * The ConnectionEntryWriter doesn't allow a null connection.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testConnectionEntryWriterDoesntAllowNull() throws Exception {
+        new ConnectionEntryWriter(null);
+    }
+
+    /**
+     * Verify ConnectionEntryWriter close function.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public final void testConnectionEntryWriterClose() throws Exception {
+        Connection connection = mock(Connection.class);
+        new ConnectionEntryWriter(connection).close();
+        verify(connection, times(1)).close();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/EntryGeneratorTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/EntryGeneratorTestCase.java
new file mode 100644
index 0000000..aa0f54b
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/EntryGeneratorTestCase.java
@@ -0,0 +1,591 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Utils;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class EntryGeneratorTestCase extends SdkTestCase {
+
+    private static final String BASIC_TEMPLATE_PATH = "org/forgerock/opendj/ldif/example.template";
+    private static final String SUBTEMPLATES_TEMPLATE_PATH = "org/forgerock/opendj/ldif/people_and_groups.template";
+    private String resourcePath;
+    private Schema schema;
+
+    @BeforeClass
+    public void setUp() throws Exception {
+        // path of directory in src/main/resources must be obtained from a file
+        // otherwise it may search in the wrong directory
+        resourcePath = new File(getTestFilePath(BASIC_TEMPLATE_PATH)).getParent();
+        schema = Schema.getDefaultSchema();
+    }
+
+    /**
+     * This test is a facility to print generated entries to stdout and should
+     * always be disabled.
+     * Turn it on locally if you need to see the output.
+     */
+    @Test(enabled = false)
+    public void printEntriesToStdOut() throws Exception {
+        String path = SUBTEMPLATES_TEMPLATE_PATH;
+        try (EntryGenerator generator = new EntryGenerator(getTestFilePath(path)).setResourcePath(resourcePath)) {
+            while (generator.hasNext()) {
+                System.out.println(generator.readEntry());
+            }
+        }
+    }
+
+    @Test
+    public void testCreateWithDefaultTemplateFile() throws Exception {
+        try (EntryGenerator generator = new EntryGenerator()) {
+            assertThat(generator.hasNext()).isTrue();
+        }
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Could not find template file unknown.*")
+    public void testCreateWithMissingTemplateFile() throws Exception {
+        try (EntryGenerator generator = new EntryGenerator("unknown/path")) {
+            generator.hasNext();
+        }
+    }
+
+    @Test
+    public void testCreateWithSetConstants() throws Exception {
+        try (EntryGenerator generator = new EntryGenerator().setConstant("numusers", 1)) {
+            generator.readEntry();
+            generator.readEntry();
+            assertThat(generator.readEntry().getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+            assertThat(generator.hasNext()).as("should have no more entries").isFalse();
+        }
+    }
+
+    @DataProvider(name = "generators")
+    public Object[][] createGenerators() throws Exception {
+        Object[][] generators = new Object[3][2];
+
+        String templatePath = getTestFilePath(BASIC_TEMPLATE_PATH);
+        generators[0][0] = new EntryGenerator(templatePath).setResourcePath(resourcePath);
+        generators[0][1] = 10000;
+
+        InputStream stream = new FileInputStream(
+                new File(templatePath));
+        generators[1][0] = new EntryGenerator(stream).setResourcePath(resourcePath);
+        generators[1][1] = 10000;
+
+        generators[2][0] = new EntryGenerator(
+                "define suffix=dc=example,dc=com",
+                "define maildomain=example.com",
+                "define numusers=2",
+                "",
+                "branch: [suffix]",
+                "objectClass: top",
+                "objectClass: domainComponent",
+                "",
+                "branch: ou=People,[suffix]",
+                "objectClass: top",
+                "objectClass: organizationalUnit",
+                "subordinateTemplate: person:[numusers]",
+                "",
+                "template: person",
+                "rdnAttr: uid",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalPerson",
+                "objectClass: inetOrgPerson",
+                "givenName: <first>",
+                "sn: <last>",
+                "cn: {givenName} {sn}",
+                "initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}",
+                "employeeNumber: <sequential:0>",
+                "uid: user.{employeeNumber}",
+                "mail: {uid}@[maildomain]",
+                "userPassword: password",
+                "telephoneNumber: <random:telephone>",
+                "homePhone: <random:telephone>",
+                "pager: <random:telephone>",
+                "mobile: <random:telephone>",
+                "street: <random:numeric:5> <file:streets> Street",
+                "l: <file:cities>",
+                "st: <file:states>",
+                "postalCode: <random:numeric:5>",
+                "postalAddress: {cn}${street}${l}, {st}  {postalCode}",
+                "description: This is the description for {cn}.")
+                .setResourcePath(resourcePath);
+        generators[2][1] = 2;
+
+        return generators;
+    }
+
+    /**
+     * Test the generated DNs.
+     *
+     * Expecting 2 entries and then numberOfUsers entries.
+     */
+    @Test(dataProvider = "generators")
+    public void testGeneratedDNs(EntryGenerator generator, int numberOfUsers) throws Exception {
+        try {
+            assertThat(generator.hasNext()).isTrue();
+            assertThat(generator.readEntry().getName().toString()).isEqualTo("dc=example,dc=com");
+            assertThat(generator.hasNext()).isTrue();
+            assertThat(generator.readEntry().getName().toString()).isEqualTo("ou=People,dc=example,dc=com");
+            for (int i = 0; i < numberOfUsers; i++) {
+                assertThat(generator.hasNext()).isTrue();
+                assertThat(generator.readEntry().getName().toString()).
+                    isEqualTo("uid=user." + i + ",ou=People,dc=example,dc=com");
+            }
+            assertThat(generator.hasNext()).as("should have no more entries").isFalse();
+        } finally {
+            Utils.closeSilently(generator);
+        }
+    }
+
+    /**
+     * Test the complete content of top entry.
+     */
+    @Test(dataProvider = "generators")
+    public void testTopEntry(EntryGenerator generator, int numberOfUsers) throws Exception {
+        try {
+            Entry topEntry = generator.readEntry();
+            assertThat(topEntry.getName().toString()).isEqualTo("dc=example,dc=com");
+
+            Attribute dcAttribute = topEntry.getAttribute(getDCAttributeType().getNameOrOID());
+            assertThat(dcAttribute).isNotNull();
+            assertThat(dcAttribute.firstValueAsString()).isEqualTo("example");
+
+            checkEntryObjectClasses(topEntry, "top", "domainComponent");
+        } finally {
+            Utils.closeSilently(generator);
+
+        }
+    }
+
+    /**
+     * Test the complete content of second entry.
+     */
+    @Test(dataProvider = "generators")
+    public void testSecondEntry(EntryGenerator generator, int numberOfUsers) throws Exception {
+        try {
+            generator.readEntry(); // skip top entry
+            Entry entry = generator.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("ou=People,dc=example,dc=com");
+
+            Attribute dcAttribute = entry.getAttribute(getOUAttributeType().getNameOrOID());
+            assertThat(dcAttribute).isNotNull();
+            assertThat(dcAttribute.firstValueAsString()).isEqualTo("People");
+
+            checkEntryObjectClasses(entry, "top", "organizationalUnit");
+        } finally {
+            Utils.closeSilently(generator);
+        }
+    }
+
+    /**
+     * Test the complete content of first user entry.
+     */
+    @Test(dataProvider = "generators")
+    public void testFirstUserEntry(EntryGenerator generator, int numberOfUsers) throws Exception {
+        try {
+            generator.readEntry(); // skip top entry
+            generator.readEntry(); // skip ou entry
+            Entry entry = generator.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+
+            checkPresenceOfAttributes(entry, "givenName", "sn", "cn", "initials",
+                    "employeeNumber", "uid", "mail", "userPassword", "telephoneNumber",
+                    "homePhone", "pager", "mobile", "street", "l", "st", "postalCode",
+                    "postalAddress", "description");
+            assertThat(entry.getAttribute("cn").firstValueAsString()).isEqualTo(
+                    entry.getAttribute("givenName").firstValueAsString() + " "
+                    + entry.getAttribute("sn").firstValueAsString());
+
+            checkEntryObjectClasses(entry, "top", "person", "organizationalPerson", "inetOrgPerson");
+        } finally {
+            Utils.closeSilently(generator);
+        }
+    }
+
+    private void checkEntryObjectClasses(Entry entry, String...objectClasses) {
+        Attribute ocAttribute = entry.getAttribute(getObjectClassAttributeType().getNameOrOID());
+        assertThat(ocAttribute).isNotNull();
+        Iterator<ByteString> it = ocAttribute.iterator();
+        for (String objectClass : objectClasses) {
+            assertThat(it.next().toString()).isEqualTo(objectClass);
+        }
+        assertThat(it.hasNext()).isFalse();
+    }
+
+    private void checkPresenceOfAttributes(Entry entry, String... attributes) {
+        for (String attribute : attributes) {
+            assertThat(entry.getAttribute(attribute)).isNotNull();
+        }
+    }
+
+    /**
+     * Test a template with subtemplates, ensuring all expected DNs are generated.
+     */
+    @Test
+    public void testTemplateWithSubTemplates() throws Exception {
+        int numberOfUsers = 10;
+        int numberOfGroups = 5;
+        int numberOfOUs = 10;
+        EntryGenerator generator = new EntryGenerator(
+                "define suffix=dc=example,dc=com",
+                "define maildomain=example.com",
+                "define numusers=" + numberOfUsers,
+                "define numous=" + numberOfOUs,
+                "define numgroup=" + numberOfGroups,
+                "",
+                "branch: [suffix]",
+                "subordinateTemplate: ous:[numous]",
+                "",
+                "template: ous",
+                "subordinateTemplate: People:1",
+                "subordinateTemplate: Groups:1",
+                "rdnAttr: ou",
+                "objectclass: top",
+                "objectclass: organizationalUnit",
+                "ou: Organization_<sequential:1>",
+                "",
+                "template: People",
+                "rdnAttr: ou",
+                "subordinateTemplate: person:[numusers]",
+                "objectclass: top",
+                "objectclass: organizationalUnit",
+                "ou: People",
+                "",
+                "template: Groups",
+                "subordinateTemplate: groupOfName:[numgroup]",
+                "rdnAttr: ou",
+                "objectclass: top",
+                "objectclass: organizationalUnit",
+                "ou: Groups",
+                "",
+                "template: person",
+                "rdnAttr: uid",
+                "objectClass: top",
+                "objectClass: inetOrgPerson",
+                "cn: <first> <last>",
+                "employeeNumber: <sequential:0>",
+                "uid: user.{employeeNumber}",
+                "",
+                "template: groupOfName",
+                "rdnAttr: cn",
+                "objectClass: top",
+                "objectClass: groupOfNames",
+                "cn: Group_<sequential:1>"
+        ).setResourcePath(resourcePath);
+
+        try {
+            assertThat(generator.readEntry().getName().toString()).isEqualTo("dc=example,dc=com");
+            int countUsers = 0;
+            int countGroups = 1;
+            for (int i = 1; i <= numberOfOUs; i++) {
+                String dnOU = "ou=Organization_" + i + ",dc=example,dc=com";
+                assertThat(generator.readEntry().getName().toString()).isEqualTo(dnOU);
+                assertThat(generator.readEntry().getName().toString()).isEqualTo("ou=People," + dnOU);
+                for (int j = countUsers; j < countUsers + numberOfUsers; j++) {
+                    assertThat(generator.readEntry().getName().toString()).isEqualTo(
+                            "uid=user." + j + ",ou=People," + dnOU);
+
+                }
+                countUsers += numberOfUsers;
+                assertThat(generator.readEntry().getName().toString()).isEqualTo("ou=Groups," + dnOU);
+                for (int j = countGroups; j < countGroups + numberOfGroups; j++) {
+                    assertThat(generator.readEntry().getName().toString()).isEqualTo(
+                            "cn=Group_" + j + ",ou=Groups," + dnOU);
+                }
+                countGroups += numberOfGroups;
+            }
+            assertThat(generator.hasNext()).isFalse();
+        } finally {
+            Utils.closeSilently(generator);
+        }
+    }
+
+    /**
+     * Test to show that reporting an error about an uninitialized variable when
+     * generating templates reports the correct line.
+     */
+    @Test
+    public void testMissingVariableErrorReport() throws Exception {
+        String[] lines = {
+        /* 0 */"template: template",
+        /* 1 */"a: {missingVar}",
+        /* 2 */"a: b",
+        /* 3 */"a: c",
+        /* 4 */"",
+        /* 5 */"template: template2", };
+
+        // Test must show "missingVar" missing on line 1.
+        // Previous behaviour showed "missingVar" on line 5.
+
+        TemplateFile templateFile = new TemplateFile(schema, null, resourcePath);
+        List<LocalizableMessage> warns = new ArrayList<>();
+
+        try {
+            templateFile.parse(lines, warns);
+            TestCaseUtils.failWasExpected(DecodeException.class);
+        } catch (DecodeException e) {
+            LocalizableMessage expected = ERR_ENTRY_GENERATOR_TAG_UNDEFINED_ATTRIBUTE.get("missingVar", 1);
+            assertThat(e.getMessage()).isEqualTo(expected.toString());
+        }
+    }
+
+    @DataProvider(name = "validTemplates")
+    public Object[][] createTestTemplates() {
+        return new Object[][] {
+            { "CurlyBracket",
+              new String[] { "template: templateWithEscape", "rdnAttr: uid", "uid: testEntry",
+                  "cn: I\\{Foo\\}F" }
+            },
+            { "AngleBracket",
+              new String[] { "template: templateWithEscape", "rdnAttr: uid", "uid: testEntry",
+                  "sn: \\<Bar\\>" }
+            },
+            { "SquareBracket",
+              new String[] { "template: templateWithEscape", "rdnAttr: uid", "uid: testEntry",
+                  "description: \\[TEST\\]" } },
+            { "BackSlash",
+              new String[] { "template: templateWithEscape", "rdnAttr: uid", "uid: testEntry",
+                  "description: Foo \\\\ Bar" } },
+            { "EscapedAlpha",
+              new String[] { "template: templateWithEscape", "rdnAttr: uid", "uid: testEntry",
+                  "description: Foo \\\\Bar" } },
+            { "Normal Variable",
+              new String[] { "template: templateNormal", "rdnAttr: uid", "uid: testEntry", "sn: {uid}" } },
+            { "Constant",
+              new String[] { "define foo=Test123", "", "template: templateConstant", "rdnAttr: uid",
+                  "uid: testEntry", "sn: {uid}", "cn: [foo]" }
+            },
+        };
+    }
+
+    /** Test for parsing escaped character in templates. */
+    @Test(dataProvider = "validTemplates")
+    public void testParsingEscapeCharInTemplate(String testName, String[] lines) throws Exception {
+        TemplateFile templateFile = new TemplateFile(schema, null, resourcePath);
+        List<LocalizableMessage> warns = new ArrayList<>();
+        templateFile.parse(lines, warns);
+        assertThat(warns).isEmpty();
+    }
+
+    @DataProvider(name = "templatesToTestEscapeChars")
+    public Object[][] createTemplatesToTestSpecialChars() {
+        return new Object[][] {
+            {
+                "Curly",
+                new String[] {
+                    "branch: dc=test", "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "cn: I\\{ Foo \\}F" },
+                "cn", // Attribute to test
+                "I{ Foo }F", // Expected value
+            },
+            {
+                "Angle",
+                new String[] {
+                    "branch: dc=test",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "sn: \\< Bar \\>" },
+                "sn", // Attribute to test
+                "< Bar >", // Expected value
+            },
+            {
+                "Square",
+                new String[] {
+                    "branch: dc=test",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "description: \\[TEST\\]" },
+                "description", // Attribute to test
+                "[TEST]", // Expected value
+            },
+            {
+                "BackSlash",
+                new String[] {
+                    "branch: dc=test",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "displayName: Foo \\\\ Bar" },
+                "displayname", // Attribute to test
+                "Foo \\ Bar", // Expected value
+            },
+            {
+                "MultipleSquare",
+                new String[] {
+                    "define top=dc=com",
+                    "define container=ou=group",
+                    "",
+                    "branch: dc=test,[top]",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "manager: cn=Bar,[container],dc=test,[top]",
+                    "" },
+                "manager", // Attribute to test
+                "cn=Bar,ou=group,dc=test,dc=com", // Expected value
+            },
+            {
+                "MixedSquare",
+                new String[] {
+                    "define top=dc=com",
+                    "define container=ou=group",
+                    "", "branch: dc=test,[top]",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "description: test [container] \\[[top]\\]",
+                    "", },
+                "description", // Attribute to test
+                "test ou=group [dc=com]", // Expected value
+            },
+            {
+                "NoConstantBecauseEscaped",
+                new String[] {
+                    "define top=dc=com",
+                    "define container=ou=group",
+                    "", "branch: dc=test,[top]",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "description: test \\[top]",
+                    "", },
+                "description", // Attribute to test
+                "test [top]", // Expected value
+            },
+            {
+                "NoConstantBecauseStrangeChar",
+                new String[] {
+                    "define top=dc=com",
+                    "define container=ou=group",
+                    "",
+                    "branch: dc=test,[top]",
+                    "subordinateTemplate: templateWithEscape:1",
+                    "",
+                    "template: templateWithEscape",
+                    "rdnAttr: uid",
+                    "objectclass: inetOrgPerson",
+                    "uid: testEntry",
+                    "description: test [group \\[top]",
+                    "", },
+                "description", // Attribute to test
+                "test [group [top]", // Expected value
+            },
+        /*
+         * If adding a test, please copy and reuse template code down below {
+         * "", new String[]{ "template: templateWithEscape", "rdnAttr: uid",
+         * "uid: testEntry", "cn: I\\{Foo\\}F"}, "", // Attribute to test "", //
+         * Expected value }
+         */
+        };
+    }
+
+    /**
+     * Test for escaped characters in templates.
+     */
+    @Test(dataProvider = "templatesToTestEscapeChars", dependsOnMethods = { "testParsingEscapeCharInTemplate" })
+    public void testEscapeCharsFromTemplate(String testName, String[] lines, String attrName, String expectedValue)
+            throws Exception {
+        try (EntryGenerator generator = new EntryGenerator(lines).setResourcePath(resourcePath)) {
+            Entry topEntry = generator.readEntry();
+            Entry entry = generator.readEntry();
+
+            assertThat(topEntry).isNotNull();
+            assertThat(entry).isNotNull();
+            assertThat(entry.getAttribute(attrName).firstValueAsString()).isEqualTo(expectedValue);
+        }
+    }
+
+    /**
+     * Test template that combines escaped characters and variables.
+     */
+    @Test(dependsOnMethods = { "testParsingEscapeCharInTemplate" })
+    public void testCombineEscapeCharInTemplate() throws Exception {
+        String[] lines = {
+            "branch: dc=test",
+            "subordinateTemplate: templateWithEscape:1",
+            "",
+            "template: templateWithEscape",
+            "rdnAttr: uid",
+            "objectclass: inetOrgPerson",
+            "uid: testEntry",
+            "sn: Bar",
+            // The value below combines variable, randoms and escaped chars.
+            // The resulting value is "Foo <?>{1}Bar" where ? is a letter
+            // from [A-Z].
+            "cn: Foo \\<<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>\\>\\{1\\}{sn}",
+            "" };
+        try (EntryGenerator generator = new EntryGenerator(lines).setResourcePath(resourcePath)) {
+            Entry topEntry = generator.readEntry();
+            Entry entry = generator.readEntry();
+
+            assertThat(topEntry).isNotNull();
+            assertThat(entry).isNotNull();
+            assertThat(entry.getAttribute("cn").firstValueAsString()).matches("Foo <[A-Z]>\\{1\\}Bar");
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
new file mode 100644
index 0000000..f6b75c0
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
@@ -0,0 +1,2489 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2014 Manuel Gaupp
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.Action;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the LDIFChangeRecordReader functionality.
+ */
+@SuppressWarnings("javadoc")
+public final class LDIFChangeRecordReaderTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * Provide a standard LDIF Change Record, valid, for tests below.
+     *
+     * @return a string containing a standard LDIF Change Record.
+     */
+    public final String[] getStandardLDIFChangeRecord() {
+        // @formatter:off
+        return new String[] {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter",
+            "cn: Samnatha Carter",
+            "givenName: Sam",
+            "objectClass: inetOrgPerson",
+            "telephoneNumber: 555 555-5555",
+            "mail: scarter@mail.org",
+            "entryDN: uid=scarter,ou=people,dc=example,dc=org",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Test to read an LDIFChangeRecord excluding all operational attributes. In
+     * this case, force to false, all attributes must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesFalse() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAllOperationalAttributes(false);
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryUUID")).isTrue();
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("modifyTimestamp")).isTrue();
+        assertThat(addRequest.containsAttribute("modifiersName")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+    }
+
+    /**
+     * All operational attributes are excluded (true). Therefore, they musn't
+     * appear in the request.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesTrue() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAllOperationalAttributes(true);
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        // Operational attributes are successfully excluded:
+        assertThat(addRequest.containsAttribute("entryUUID")).isFalse();
+        assertThat(addRequest.containsAttribute("entryDN")).isFalse();
+        assertThat(addRequest.containsAttribute("modifyTimestamp")).isFalse();
+        assertThat(addRequest.containsAttribute("modifiersName")).isFalse();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        // - 4 operational
+        assertThat(addRequest.getAttributeCount()).isEqualTo(6);
+    }
+
+    /**
+     * All user attributes are excluded (false). The reader must fully return
+     * the ldif-changes. The changetype line doesn't appear.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesFalse() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAllUserAttributes(false);
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("givenName")).isTrue();
+        assertThat(addRequest.containsAttribute("mail")).isTrue();
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+    }
+
+    /**
+     * All user attributes are excluded (true). The reader must return the
+     * ldif-changes without the user attributes. The changetype line doesn't
+     * appear.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesTrue() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAllUserAttributes(true);
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("sn")).isFalse();
+        assertThat(addRequest.containsAttribute("givenName")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isFalse();
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(4);
+    }
+
+    /**
+     * Test to read an entry with attribute exclusions. Three attributes
+     * excluded, entry must contain the others.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAttributeWithMatch() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("sn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("entryDN"));
+
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isFalse();
+        assertThat(addRequest.containsAttribute("sn")).isFalse();
+        assertThat(addRequest.containsAttribute("cn")).isFalse();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(7);
+    }
+
+    /**
+     * Test to read an entry with attribute exclusions. One non-existent
+     * attribute is defined. Record must be complete.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAttributeWithNoMatch() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAttribute(AttributeDescription.valueOf("vip"));
+
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.containsAttribute("mail")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.getAttribute("vip")).isNull();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+    }
+
+    /**
+     * SetExcludeAttribute doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeAttributeDoesntAllowNull() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setExcludeAttribute(null);
+        reader.close();
+    }
+
+    /**
+     * Test to read an entry with attribute including.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeAttributeWithMatch() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        reader.setIncludeAttribute(AttributeDescription.valueOf("entryDN"));
+
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isFalse();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(3);
+    }
+
+    /**
+     * Test to read an ldifChangeRecord with attribute including.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeAttributeWithNoMatch() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+
+        reader.setIncludeAttribute(AttributeDescription.valueOf("manager"));
+
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isFalse();
+        assertThat(addRequest.containsAttribute("sn")).isFalse();
+        assertThat(addRequest.containsAttribute("cn")).isFalse();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isFalse();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(0);
+    }
+
+    /**
+     * SetIncludeAttribute doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeAttributeDoesntAllowNull() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader("version: 1",
+                        "dn: uid=scarter,ou=People,dc=example,dc=com");
+        reader.setIncludeAttribute(null);
+        reader.close();
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFChangeRecordReader.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeBranchWithMatch() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+        reader.setIncludeBranch(DN.valueOf("dc=example,dc=com"));
+
+        final ChangeRecord cr = reader.readChangeRecord();
+        reader.close();
+
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFChangeRecordReader. The branch is not
+     * included, throw an NoSuchElementException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetIncludeBranchWithNoMatch() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+        reader.setIncludeBranch(DN.valueOf("dc=example,dc=org"));
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * SetIncludeBranch doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeBranchDoesntAllowNull() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader("version: 1",
+                        "dn: uid=scarter,ou=People,dc=example,dc=com");
+        reader.setIncludeBranch(null);
+        reader.close();
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordReader.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeBranchWithMatch() throws Exception {
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+        reader.setExcludeBranch(DN.valueOf("dc=example,dc=org"));
+        ChangeRecord cr = null;
+
+        cr = reader.readChangeRecord();
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(addRequest.containsAttribute("entryDN")).isTrue();
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.containsAttribute("changetype")).isFalse();
+        assertThat(addRequest.containsAttribute("mail")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+        reader.close();
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordReader.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetExcludeBranchWithNoMatch() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
+        reader.setExcludeBranch(DN.valueOf("dc=example,dc=com"));
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecordReader setSchemaValidationPolicy. Validate the Change
+     * Record depending of the selected policy. ChangeRecord is here NOT allowed
+     * because it contains a uid attribute which is not allowed by the
+     * SchemaValidationPolicy.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testSetSchemaValidationPolicyDefaultRejectsEntry() throws Exception {
+        // @formatter:off
+        String[] strChangeRecord = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter",
+            "objectClass: person",
+            "objectClass: top",
+            "cn: Aaccf Amar",
+            "sn: Amar",
+            "uid: user.0"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * SetExcludeBranch doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeBranchDoesntAllowNull() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader("version: 1",
+                        "dn: uid=scarter,ou=People,dc=example,dc=com");
+        reader.setExcludeBranch(null);
+        reader.close();
+    }
+
+    /**
+     * SetSchema doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetSchemaDoesntAllowNull() throws Exception {
+
+        final LDIFChangeRecordReader reader =
+                new LDIFChangeRecordReader("version: 1",
+                        "dn: uid=scarter,ou=People,dc=example,dc=com");
+        reader.setSchema(null);
+        reader.close();
+    }
+
+    /**
+     * LDIFChangeRecordReader setSchemaValidationPolicy. Validate the
+     * ChangeRecord depending of the selected policy. ChangeRecord is here
+     * allowed because it fills the case of validation.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetSchemaValidationPolicyDefaultAllowsEntry() throws Exception {
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter",
+            "objectClass: person",
+            "objectClass: top",
+            "cn: Aaccf Amar",
+            "sn: Amar"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        final ChangeRecord cr = reader.readChangeRecord();
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(3);
+
+        reader.close();
+
+    }
+
+    /**
+     * Test an LDIFRecordChange with an empty pair key. Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadAddRecordWithEmptyPairKeyChangeType() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            ":add" // if empty spaces, ko.
+        );
+        // @formatter:on
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecordReader used with a wrong changetype. Must return an
+     * exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadAddRecordWithWrongChangeType() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: oops", // wrong
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Tests reading a valid add change record with a changetype.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testReadAddRecordWithChangeType() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
+        AddRequest addRequest = (AddRequest) record;
+        assertThat(addRequest.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
+        assertThat(addRequest.containsAttribute("dc", "example")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(2);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Tests reading a valid add change record without a changetype.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testReadAddRecordWithoutChangeType() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
+        AddRequest addRequest = (AddRequest) record;
+        assertThat(addRequest.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
+        assertThat(addRequest.containsAttribute("dc", "example")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(2);
+        reader.close();
+    }
+
+    /**
+     * Tests reading a valid modify change record.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testReadModifyRecord() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: modify",
+            "add: description",
+            "-",
+            "add: description",
+            "description: value1",
+            "-",
+            "add: description",
+            "description: value1",
+            "description: value2",
+            "-",
+            "delete: description",
+            "-",
+            "delete: description",
+            "description: value1",
+            "-",
+            "delete: description",
+            "description: value1",
+            "description: value2",
+            "-",
+            "replace: description",
+            "-",
+            "replace: description",
+            "description: value1",
+            "-",
+            "replace: description",
+            "description: value1",
+            "description: value2",
+            "-",
+            "increment: description",
+            "description: 1"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
+        ModifyRequest modifyRequest = (ModifyRequest) record;
+
+        Iterator<Modification> changes = modifyRequest.getModifications().iterator();
+        Modification modification;
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.ADD);
+        assertThat(modification.getAttribute()).isEqualTo(new LinkedAttribute("description"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.ADD);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.ADD);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1", "value2"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.DELETE);
+        assertThat(modification.getAttribute()).isEqualTo(new LinkedAttribute("description"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.DELETE);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.DELETE);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1", "value2"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.REPLACE);
+        assertThat(modification.getAttribute()).isEqualTo(new LinkedAttribute("description"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.REPLACE);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.REPLACE);
+        assertThat(modification.getAttribute()).isEqualTo(
+                new LinkedAttribute("description", "value1", "value2"));
+
+        modification = changes.next();
+        assertThat(modification.getModificationType()).isEqualTo(ModificationType.INCREMENT);
+        assertThat(modification.getAttribute()).isEqualTo(new LinkedAttribute("description", "1"));
+
+        assertThat(changes.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Tests reading a valid moddn change record.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testReadModdnRecordWithoutNewSuperior() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: dc=eggsample",
+            "deleteoldrdn: true"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("dc=eggsample"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+        assertThat(modifyDNRequest.getNewSuperior()).isNull();
+        reader.close();
+    }
+
+    /**
+     * Tests reading a valid moddn change record.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testReadModdnRecordWithNewSuperior() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: dc=eggsample",
+            "deleteoldrdn: true",
+            "newsuperior: dc=org"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("dc=eggsample"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+        assertThat((Object) modifyDNRequest.getNewSuperior()).isEqualTo(DN.valueOf("dc=org"));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a malformed record invokes the rejected record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerMalformedFirstRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: baddn",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+        reader.setRejectedLDIFListener(listener);
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleMalformedRecord(
+                eq(1L),
+                eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example")),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a malformed record invokes the rejected record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerMalformedSecondRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example",
+            "",
+            "dn: baddn",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+        reader.setRejectedLDIFListener(listener);
+        reader.readChangeRecord(); // Skip good record.
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleMalformedRecord(
+                eq(7L),
+                eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example")),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a skipped record invokes the rejected record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerSkipsRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+        reader.setRejectedLDIFListener(listener).setExcludeBranch(DN.valueOf("dc=com"));
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleSkippedRecord(
+                eq(1L),
+                eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example")),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a record which does not conform to the schema invokes the
+     * rejected record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerRejectsBadSchemaRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example",
+            "xxx: unknown attribute"
+        );
+        reader.setRejectedLDIFListener(listener)
+             .setSchemaValidationPolicy(
+                 SchemaValidationPolicy.ignoreAll()
+                     .checkAttributesAndObjectClasses(Action.REJECT));
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleSchemaValidationFailure(
+                eq(1L),
+                eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example", "xxx: unknown attribute")),
+                anyListOf(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a record which does not conform to the schema invokes the
+     * warning record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerWarnsBadSchemaRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example",
+            "xxx: unknown attribute"
+        );
+        reader.setRejectedLDIFListener(listener)
+             .setSchemaValidationPolicy(
+                 SchemaValidationPolicy.ignoreAll()
+                     .checkAttributesAndObjectClasses(Action.WARN));
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        AddRequest addRequest = (AddRequest) record;
+        assertThat((Object) addRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
+        assertThat(addRequest.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
+        assertThat(addRequest.containsAttribute("dc", "example")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(2);
+
+        verify(listener).handleSchemaValidationWarning(
+                eq(1L),
+                eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example", "xxx: unknown attribute")),
+                anyListOf(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Read an example containing an invalid url. Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadFileContainingInvalidURLThrowsError() throws Exception {
+        // Obtain the name of a file which is guaranteed not to exist.
+        final File file = File.createTempFile("sdk", null);
+        final String url = file.toURI().toURL().toString();
+        file.delete();
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "# Add a new entry",
+            "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: add",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Fiona Jensen",
+            "sn: Jensen",
+            "uid: fiona",
+            "telephonenumber: +1 408 555 1212",
+            "jpegphoto:< " + url
+        );
+        // @formatter:on
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Read a complete LDIFChangeRecord containing serie of changes.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testReadFileContainingSerieOfChanges() throws Exception {
+        final File file = File.createTempFile("sdk", ".png");
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "version: 1",
+                "# Add a new entry",
+                "dn: cn=Fiona Jensen,",
+                " ou=Marketing, dc=airius, dc=com", // data continued from previous line
+                "changetype: add",
+                "objectclass: top",
+                "objectclass: person",
+                "objectclass: organizationalPerson",
+                "cn: Fiona Jensen",
+                "sn: Jensen",
+                "uid: fiona",
+                "telephonenumber: +1 408 555 1212",
+                "jpegphoto:< " + url,
+                "",
+                "# Delete an existing entry",
+                "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
+                "changetype: delete",
+                "",
+                "# Modify an entry's relative distinguished name",
+                "dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com",
+                "changetype: modrdn",
+                "newrdn: cn=Paula Jensen",
+                "deleteoldrdn: 1",
+                "",
+                "# Rename an entry and move all of its children to a new location in",
+                "# the directory tree (only implemented by LDAPv3 servers).",
+                "dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com",
+                "changetype: modrdn",
+                "newrdn: ou=Product Development Accountants",
+                "deleteoldrdn: 0",
+                "newsuperior: ou=Accounting, dc=airius, dc=com",
+                ""
+        );
+        // @formatter:on
+        assertThat(reader.hasNext()).isTrue();
+        // 1st record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        AddRequest addReq = (AddRequest) record;
+        assertThat((Object) addReq.getName()).isEqualTo(
+                DN.valueOf("cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com"));
+        // 2nd record
+        record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        DeleteRequest delReq = (DeleteRequest) record;
+        assertThat((Object) delReq.getName()).isEqualTo(
+                DN.valueOf("cn=Robert Jensen, ou=Marketing, dc=airius, dc=com"));
+        assertThat(reader.hasNext()).isTrue();
+        // 3rd record
+        record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modDNReq = (ModifyDNRequest) record;
+
+        assertThat((Object) modDNReq.getNewRDN()).isEqualTo(RDN.valueOf("cn=Paula Jensen"));
+        assertThat((Object) modDNReq.getName()).isEqualTo(
+                DN.valueOf("cn=Paul Jensen, ou=Product Development, dc=airius, dc=com"));
+        assertThat(reader.hasNext()).isTrue();
+        // 4th record
+        record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        modDNReq = (ModifyDNRequest) record;
+        assertThat((Object) modDNReq.getName()).isEqualTo(
+                DN.valueOf("ou=PD Accountants, ou=Product Development, dc=airius, dc=com"));
+        assertThat(reader.hasNext()).isFalse();
+
+        file.delete();
+        reader.close();
+    }
+
+    /**
+     * Test to read an entry containing a delete control.\ Control syntax is
+     * like => control : OID (criticality - optional) :(value - optional). <br>
+     * ex: control: 1.2.840.113556.1.4.805 true :cn <br>
+     * control: 1.2.840.113556.1.4.805 true ::dGVzdGluZw== <br>
+     * control: 1.2.840.113556.1.4.805 ::dGVzdGluZw== <br>
+     * etc...
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithDeleteControl() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                    "# Delete an entry. The operation will attach the LDAPv3",
+                    "# Tree Delete Control defined in [9]. The criticality",
+                    "# field is \"true\" and the controlValue field is",
+                    "# absent, as required by [9].",
+                    "dn: ou=Product Development, dc=airius, dc=com",
+                    "control: 1.2.840.113556.1.4.805 true :cn",
+                    "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.2.840.113556.1.4.805");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControl() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.1 false :cn",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a invalid control. Must throw an error.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseChangeRecordEntryWithAnInvalidControl() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.2.3.4 0 value",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlWithoutSpacesBetweenCriticalityValue()
+            throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.1 false:cn",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlContainingWhiteSpaces() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.1 true : sn",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("sn");
+        reader.close();
+    }
+
+    /**
+     * Record is containing a control with a base64 value.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlContainingBase64Value() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "# This record contains a base64 value",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "control: 2.16.840.1.113730.3.4.3 true::ZGVzY3JpcHRpb24=",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        assertThat(record.getName().toString())
+                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("description");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a malformed base64 value.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseChangeRecordEntryWithAddControlMalformedBase64() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# This record contains a control",
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "control: 2.16.840.1.113730.3.4.3 true:: malformedBase64",
+                "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
+     * Record is containing a control with a URL.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlContainingURL() throws Exception {
+        final File file = File.createTempFile("sdk", ".png");
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "# This record contains a base64 value",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "control: 2.16.840.1.113730.3.4.3 true:<" + url,
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        assertThat(record.getName().toString())
+                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
+        // URL is fine, but the content is empty ;)
+        assertThat(record.getControls().get(0).getValue().toString()).isEmpty();
+        file.delete();
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a malformed URL.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseChangeRecordEntryWithAddControlMalformedURL() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# This record contains a control",
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "control: 2.16.840.1.113730.3.4.3 true:<malformedURL",
+                "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control without value.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlWithoutValue() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.1 false ", // space added
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue()).isNull();
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control without criticality.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlWithoutCriticality() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control:1.3.6.1.1.13.1 :description",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("description");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record providing by our LDIFChangeRecordWriter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithAddControlProvidedByChangeRecordWriter()
+            throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# This record contains a control",
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "control: 2.16.840.1.113730.3.4.3 true:: MAkCAQ8BAf8BAf8=",
+                "changetype: delete"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        assertThat(record.getName().toString())
+                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
+        assertThat(record.getControls().get(0).getValue().toBase64String()).isEqualTo(
+                "MAkCAQ8BAf8BAf8=");
+        reader.close();
+    }
+
+    /**
+     * Test to read an record containing a add control without value.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseChangeRecordEntryWithMultipleControls() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.1 false",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing",
+                "",
+                "# Modify an entry's relative distinguished name",
+                "dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com",
+                "control: 1.3.6.1.1.13.13 false: cn", // with spaces between boolean & value
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing",
+                "",
+                "# Modify an entry's relative distinguished name",
+                "dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com",
+                "control:1.3.6.1.1.13.13.16 :sn", // wihtout spaces between control and oid
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // 1st
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record.getName().toString()).isEqualTo("ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
+        assertThat(record.getControls().get(0).getValue()).isNull();
+        //2nd
+        record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("cn=Paul Jensen,ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.13");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
+        //3rd
+        record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getName().toString()).isEqualTo("cn=Paula Jensen,ou=Product Development,dc=airius,dc=com");
+        assertThat(record.getControls()).isNotEmpty();
+        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.13.16");
+        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("sn");
+        reader.close();
+    }
+
+    /**
+     * Tests that change records containing an empty control are rejected.
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseChangeRecordEntryRejectedWhenControlIsEmpty() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control:",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test an add request malformed, changetype is erroneous (wrongchangetype)
+     * Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseAddChangeRecordEntryLastLDIFLineIsNull() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "wrongchangetype: add", // wrong
+                "uid:Carter"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Tests reading a valid delete change record.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testParseDeleteChangeRecordEntry() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(DeleteRequest.class);
+        DeleteRequest deleteRequest = (DeleteRequest) record;
+        assertThat((Object) deleteRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
+        reader.close();
+    }
+
+    /**
+     * Testing a valid LDIFChangeRecord with the delete type. The LDIF is
+     * containing additional lines after the 'changetype' when none were
+     * expected. Exception must be throw.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseDeleteChangeRecordEntryMalformedDelete() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# Delete an existing entry",
+                "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
+                "changetype: delete",
+                "-",
+                "add: telephonenumber",
+                "telephonenumber: 555-4321"
+        );
+        // @formatter:on
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
+     * Read an LDIFChangeRecord for deleting selected values of a multi-valued
+     * attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyChangeRecordEntryDeleteMultipleValuableAttributes() throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# Add new entry containing multiple attributes",
+                "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+                "changetype: modify",
+                "delete: telephonenumber",
+                "telephonenumber: +1 408 555 1212",
+                "telephonenumber: +1 408 555 1213",
+                "telephonenumber: +1 408 555 1214"
+        );
+        // @formatter:on
+        assertThat(reader.hasNext()).isTrue();
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyRequest.class);
+        ModifyRequest req = (ModifyRequest) record;
+        assertThat((Object) req.getName()).isEqualTo(
+                DN.valueOf("cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com"));
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Read an LDIFChangeRecord for deleting selected values of a multi-valued
+     * attribute. The 2nd attribute is malformed : Schema validation failure.
+     * ERR_LDIF_MALFORMED_ATTRIBUTE_NAME : The provided value is not a valid
+     * telephone number because it is empty or null
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyChangeRecordEntryDeleteMultipleValuableAttributesMalformedLDIF()
+            throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "# Add new entry containing multiple attributes",
+                "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+                "changetype: modify",
+                "delete: telephonenumber",
+                "telephonenumber: +1 408 555 1212",
+                "telephonenumber:", // wrong!
+                "telephonenumber: +1 408 555 1214",
+                "-"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * BER encoding is required for this LDIFChangeRecord. After adding the user
+     * certificate to the core schema, LDIFCRR is correctly read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyChangeRecordBEREncodingRequired() throws Exception {
+        // @formatter:off
+        String validcert1 = // a valid certificate but wrong can be used => no errors
+                "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
+                + "BAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRpb25lMRwwGgYDVQQLExNQcm9kdWN0IERl"
+                + "dmVsb3BtZW50MRQwEgYDVQQDEwtCYWJzIEplbnNlbjAeFw0xMjA1MDIxNjM0MzVa"
+                + "Fw0xMjEyMjExNjM0MzVaMFYxCzAJBgNVBAYTAlVTMRMwEQYDVQQHEwpDdXBlcnRp"
+                + "b25lMRwwGgYDVQQLExNQcm9kdWN0IERldmVsb3BtZW50MRQwEgYDVQQDEwtCYWJz"
+                + "IEplbnNlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApysa0c9qc8FB8gIJ"
+                + "8zAb1pbJ4HzC7iRlVGhRJjFORkGhyvU4P5o2wL0iz/uko6rL9/pFhIlIMbwbV8sm"
+                + "mKeNUPitwiKOjoFDmtimcZ4bx5UTAYLbbHMpEdwSpMC5iF2UioM7qdiwpAfZBd6Z"
+                + "69vqNxuUJ6tP+hxtr/aSgMH2i8ECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB"
+                + "hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE"
+                + "FLlZD3aKDa8jdhzoByOFMAJDs2osMB8GA1UdIwQYMBaAFLlZD3aKDa8jdhzoByOF"
+                + "MAJDs2osMA0GCSqGSIb3DQEBBQUAA4GBAE5vccY8Ydd7by2bbwiDKgQqVyoKrkUg"
+                + "6CD0WRmc2pBeYX2z94/PWO5L3Fx+eIZh2wTxScF+FdRWJzLbUaBuClrxuy0Y5ifj"
+                + "axuJ8LFNbZtsp1ldW3i84+F5+SYT+xI67ZcoAtwx/VFVI9s5I/Gkmu9f9nxjPpK7"
+                + "1AIUXiE3Qcck";
+
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn:uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: userCertificate;binary", // with or without the binary its working
+            String.format("userCertificate;binary::%s", validcert1)
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        Schema schema = Schema.getCoreSchema();
+        reader.setSchema(schema);
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        ModifyRequest modifyRequest = (ModifyRequest) reader.readChangeRecord();
+        assertThat(modifyRequest.getName().toString()).isEqualTo(
+                "uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(modifyRequest.getModifications().get(0).getModificationType().toString())
+                .isEqualTo("add");
+        assertThat(modifyRequest.getModifications().get(0).getAttribute().firstValueAsString())
+                .contains("OpenSSL Generated Certificate");
+        reader.close();
+    }
+
+    /**
+     * LDIFChangeREcord reader try to add an unexpected binary option to the sn.
+     * sn is a included in the core schema. Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyChangeRecordBEREncodingNotRequired() throws Exception {
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn:uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: sn;binary",
+            "sn;binary:: 5bCP56yg5Y6f"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+
+        Schema schema = Schema.getCoreSchema();
+        reader.setSchema(schema);
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test a 'modify' change record, respecting the default schema.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyChangeRecordEntryReplaceOk() throws Exception {
+
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: uid",
+            "uid: Samantha Carter"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyRequest.class);
+
+        ModifyRequest modifyRequest = (ModifyRequest) record;
+        assertThat(modifyRequest.getName().toString()).isEqualTo(
+                "uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(modifyRequest.getModifications().get(0).getModificationType().toString())
+                .isEqualTo("replace");
+        assertThat(modifyRequest.getModifications().get(0).getAttribute().firstValueAsString())
+                .isEqualTo("Samantha Carter");
+        reader.close();
+    }
+
+    /**
+     * Test a 'modify' change record, without respecting the default schema. The
+     * 'badAttribute' is not recognized by the schema. Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyChangeRecordEntryReplaceKOPolicyReject() throws Exception {
+
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: badAttribute",
+            "badAttribute: scarter"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Change Record throw an exception because the added attribute is not valid
+     * ('badAttribute') relative to the default schema. Here, we use a
+     * Action.warn instead of a Action.REJECT (default)
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyChangeRecordEntryReplaceKOPolicyWarn() throws Exception {
+
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: badAttribute",
+            "badAttribute: scarter"
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy()
+                .checkAttributesAndObjectClasses(Action.WARN));
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Change Record throw an exception because the space added just before uid
+     * provokes an LocalizedIllegalArgumentException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyChangeRecordEntryReplaceLocalizedIllegalArgumentException()
+            throws Exception {
+
+        // @formatter:off
+        final String[] strChangeRecord = {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: uid",
+            " uid:Samantha Carter" // the space before provokes an LocalizedIllegalArgumentException
+        };
+        // @formatter:on
+
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+
+    }
+
+    /**
+     * Read a malformed Change Record : changetype is wrong.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testParseModifyChangeRecordEntryWithWrongChangetype() {
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "oops:uidNumber" // wrong
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Read a malformed Change Record. 'pair.key' is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testParseModifyChangeRecordEntryWithNullPairKey() {
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            ":uidNumber" // wrong
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Read a well formed Change Record.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyChangeRecordEntryIncrement() throws Exception {
+        // @formatter:off
+        final ChangeRecord cr = LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "increment:uidNumber",
+            "uidNumber: 1"
+        );
+        // @formatter:on
+
+        assertThat(cr).isInstanceOf(ModifyRequest.class);
+        ModifyRequest modifyRequest = (ModifyRequest) cr;
+
+        assertThat(modifyRequest.getName().toString()).isEqualTo(
+                "uid=scarter,ou=People,dc=example,dc=com");
+
+        assertThat(modifyRequest.getModifications().get(0).getModificationType().toString())
+                .isEqualTo("increment");
+    }
+
+    /**
+     * Read an LDIF Record changes. Trying to modify DN using base64 newrdn.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyDNChangeRecordEntryRecordBase64NewRDN() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn::ZGM9cGVvcGxlLGRjPWV4YW1wbGUsZGM9b3Jn",
+                "changetype: modrdn",
+                "newrdn::ZGM9cGVvcGxl",
+                "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(DN.valueOf("dc=people,dc=example,dc=org"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("dc=people"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+        reader.close();
+    }
+
+    /**
+     * Modifying a DN and delete the old one.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyDNChangeRecordEntry() throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(
+                DN.valueOf("cn=scarter,dc=example,dc=com"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("cn=Susan Jacobs"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+        assertThat((Object) modifyDNRequest.getNewSuperior()).isEqualTo(null);
+        reader.close();
+    }
+
+    /**
+     * Try to change the dn, but the new rdn is missing. Unable to parse LDIF
+     * modify DN record starting at line 1 with distinguished name
+     * "cn=scarter,dc=example,dc=com" because there was no new RDN.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryMalformedMissedNewRDN() throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn"
+        );
+        // @formatter:on
+
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecord.parseModifyDNChangeRecordEntry and try to add an empty
+     * new rdn throw an error.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryKeyMalformedEmptyNewRDN() throws Exception {
+
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn:",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecord.parseModifyDNChangeRecordEntry and try to add a
+     * malformed rdn throw an error.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryKeyValueMalformedRDN() throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn:oops", // wrong
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecord.parseModifyDNChangeRecordEntry and try to add a
+     * malformed rdn throw an error. (deleteoldrdn value is wrong).
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN() throws Exception {
+
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn:cn=Susan Jacobs",
+            "deleteoldrdn: cn=scarter,dc=example,dc=com" // wrong
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecord.parseModifyDNChangeRecordEntry and try to add a
+     * malformed rdn throw an error. (pair.key != deleteoldrdn)
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN2() throws Exception {
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn:cn=Susan Jacobs",
+            "deleteold: 1" // wrong
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Try to change the DN but deleteoldrdn is missing. Must throw an exception
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN3() throws Exception {
+
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn:cn=Susan Jacobs"
+            // missing deleteoldrn: 1/0||true/false||yes/no
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFCRR delete old rdn and add a new superior to the new one.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyRecordEntryDeleteOldRDNFalse() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: cn=scarter,ou=People,dc=example,dc=com",
+            "changeType: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteOldRdn: 0",
+            "newSuperior:ou=Manager,dc=example,dc=org"
+        );
+        // @formatter:on
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(
+                DN.valueOf("cn=scarter,ou=People,dc=example,dc=com"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("cn=Susan Jacobs"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isFalse();
+        assertThat((Object) modifyDNRequest.getNewSuperior().toString()).isEqualTo(
+                "ou=Manager,dc=example,dc=org");
+        reader.close();
+    }
+
+    /**
+     * LDIFCRR delete old rdn and add a new superior.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testParseModifyRecordEntryNewSuperior() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: cn=scarter,ou=People,dc=example,dc=com",
+            "changeType: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteOldRdn: 1",
+            "newSuperior:ou=Manager,dc=example,dc=org"
+        );
+        // @formatter:on
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(ModifyDNRequest.class);
+        ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+
+        assertThat((Object) modifyDNRequest.getName()).isEqualTo(
+                DN.valueOf("cn=scarter,ou=People,dc=example,dc=com"));
+        assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(RDN.valueOf("cn=Susan Jacobs"));
+        assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+        assertThat((Object) modifyDNRequest.getNewSuperior().toString()).isEqualTo(
+                "ou=Manager,dc=example,dc=org");
+        reader.close();
+    }
+
+    /**
+     * LDIFCRR delete old rdn and add a new superior willingly malformed. Syntax
+     * is wrong "newSuperior:". Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyRecordEntryNewSuperiorMalformed() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: cn=scarter,ou=People,dc=example,dc=com",
+            "changeType: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteOldRdn: 1",
+            "newSuperior:" // wrong
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFCRR delete old rdn and add a new superior willingly malformed. Syntax
+     * is wrong "newSuperior: Susan Jacobs". Must throw an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseModifyRecordEntryNewSuperiorMalformed2() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "dn: cn=scarter,ou=People,dc=example,dc=com",
+            "changeType: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteOldRdn: 1",
+            "newSuperior: Susan Jacobs" // wrong
+        );
+        // @formatter:on
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Mock an inputstream for verifying LDIFChangeRecordReader close().
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = IOException.class)
+    public void testChangeRecordReaderClosesAfterReading() throws Exception {
+
+        final FileInputStream mockIn = mock(FileInputStream.class);
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(mockIn);
+
+        doThrow(new IOException()).when(mockIn).read();
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+            verify(mockIn, times(1)).close();
+        }
+    }
+
+    /**
+     * Read an ldif-changes using a List<String>.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testChangeRecordReaderUseListConstructor() throws Exception {
+        // @formatter:off
+        List<String> cr = Arrays.asList(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example"
+        );
+        // @formatter:on
+
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(cr);
+        ChangeRecord rc = reader.readChangeRecord();
+        assertThat(rc).isNotNull();
+        assertThat(rc.getName().toString()).isEqualTo("dc=example,dc=com");
+        reader.close();
+    }
+
+    /**
+     * Try to read an LDIFChangeREcord without changetype. Must throw an
+     * exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testChangeRecordReaderHasNoChange() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "# Add a new entry without changes !",
+            "dn: dc=example,dc=com"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Try to read a null ldif-changes using a List<String>. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testChangeRecordReaderDoesntAllowNull() throws Exception {
+        List<String> cr = Collections.emptyList();
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(cr);
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Try to read a null ldif-changes using an empty String. Exception
+     * expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testChangeRecordReaderLDIFLineDoesntAllowNull() throws Exception {
+        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(new String());
+        try {
+            reader.readChangeRecord();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFChangeRecordReader cause NullPointerException when InputStream is
+     * null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testChangeRecordReaderInpuStreamDoesntAllowNull() throws Exception {
+        new LDIFChangeRecordReader((InputStream) null).close();
+    }
+
+    /**
+     * LDIFChangeRecordReader cause NullPointerException when
+     * valueOfLDIFChangeRecord is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFChangeRecordDoesntAllowNull() throws Exception {
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord("");
+    }
+
+    /**
+     * {@link LDIFChangeRecordReader#valueOfLDIFChangeRecord(String...)} cause
+     * an exception due to the presence of multiple change record.
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFChangeRecordDoesntAllowMultipleChangeRecords() throws Exception {
+        final File file = File.createTempFile("sdk", ".png");
+        final String url = file.toURI().toURL().toString();
+        file.delete();
+
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "# Add a new entry",
+            "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: add",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Fiona Jensen",
+            "sn: Jensen",
+            "uid: fiona",
+            "telephonenumber: +1 408 555 1212",
+            "jpegphotojpegphoto:< " + url,
+            "",
+            "# Delete an existing entry",
+            "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * {@link LDIFChangeRecordReader#valueOfLDIFChangeRecord(String...)} cause
+     * an exception due to badly formed ldif. In this case, DN is missing.
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFChangeRecordMalformedLDIFDNIsMissing() throws Exception {
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+            "version: 1",
+            "# Add a new entry",
+            "changetype: add",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Fiona Jensen",
+            "sn: Jensen",
+            "uid: fiona",
+            "telephonenumber: +1 408 555 1212"
+        );
+        // @formatter:on
+        // DN is missing above.
+    }
+
+    /**
+     * Try to read a malformed LDIF : The provided LDIF content did not contain
+     * any LDIF change records. Coverage on AbstractLDIFReader -
+     * readLDIFRecordDN.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFChangeRecordMalformedLDIFContainingOnlyVersion() throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+                "version: 1"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Try to read a malformed LDIF : Unable to parse LDIF entry starting at
+     * line 1 because the line ":wrong" does not include an attribute name.
+     * Coverage on AbstractLDIFReader - readLDIFRecordDN.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFChangeRecordMalformedLDIFContainingVersionAndWrongLine()
+            throws Exception {
+
+        // @formatter:off
+        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
+                "version: 1",
+                ":wrong"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Try to read a standard Change Record LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testValueOfLDIFChangeRecordStandardLDIF() throws Exception {
+        // @formatter:off
+        final ChangeRecord cr =
+                LDIFChangeRecordReader.valueOfLDIFChangeRecord(getStandardLDIFChangeRecord());
+        // @formatter:on
+
+        AddRequest addRequest = (AddRequest) cr;
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(addRequest.containsAttribute("sn")).isTrue();
+        assertThat(addRequest.containsAttribute("cn")).isTrue();
+        assertThat(addRequest.getAttributeCount()).isEqualTo(10);
+    }
+
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriterTestCase.java
new file mode 100644
index 0000000..96e6642
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordWriterTestCase.java
@@ -0,0 +1,1405 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.controls.PersistentSearchChangeType;
+import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * This class tests the LDIFChangeRecordWriter functionality.
+ */
+@SuppressWarnings("javadoc")
+public class LDIFChangeRecordWriterTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * Provide a standard LDIF Change Record, valid, for tests below. 1 dn + 1
+     * changetype + 11 attributes.
+     *
+     * @return a string containing a standard LDIF Change Record.
+     */
+    public final String[] getAddLDIFChangeRecord() {
+        // @formatter:off
+        return new String[] {
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter",
+            "cn: Samantha Carter",
+            "givenName: Sam",
+            "objectClass: inetOrgPerson",
+            "telephoneNumber: 555 555-5555",
+            "mail: scarter@mail.org",
+            "entryDN: uid=scarter,ou=people,dc=example,dc=org",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config",
+            "description::V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUgIQ=="
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Test to write a record excluding all operational attributes
+     * setExcludeAllOperationalAttributes is forced to true.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesTrue() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAllOperationalAttributes(true);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        for (String line : actual) {
+            assertThat(line).doesNotContain("entryUUID");
+            assertThat(line).doesNotContain("entryDN");
+        }
+        assertThat(actual.size()).isEqualTo(10);
+    }
+
+    /**
+     * Test to write a record excluding all operational attributes
+     * setExcludeAllOperationalAttributes is false. All lines must be written
+     * plus an empty line.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesFalse() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAllOperationalAttributes(false);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(14);
+        assertThat(actual.get(13)).isEqualTo("");
+    }
+
+    /**
+     * Test to write a record excluding user attributes true. dn, changetype,
+     * operational attributes and empty line must be written.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesTrue() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAllUserAttributes(true);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(7);
+        assertThat(actual.get(6)).isEqualTo("");
+    }
+
+    /**
+     * Test to write a record excluding user attributes Default case - the
+     * record must be written.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesFalse() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAllUserAttributes(false);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(14);
+        assertThat(actual.get(13)).isEqualTo("");
+    }
+
+    /**
+     * Test setExcludeAttribute method of LDIFChangeRecordWriter Throws a
+     * NullPointerException if the attributeDescription is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeAttributeDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        try {
+            writer.setExcludeAttribute(null);
+        } finally {
+            writer.close();
+        }
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWrongDN() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dn = DN.valueOf("dc=example.com");
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // Even if DN is wrong then record must be write.
+        assertThat(actual.size()).isEqualTo(14);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dn = DN.valueOf("dc=example,dc=com");
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // No values expected - we have excluded the branch.
+        Assert.assertFalse(actual.size() > 0);
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dn = DN.valueOf("dc=example,dc=org");
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // The record must be written
+        assertThat(actual.size()).isEqualTo(14);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFChangeRecordWriter Throws a
+     * NullPointerException if the excludeBranch is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeBranchDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        try {
+            writer.setExcludeBranch(null);
+        } finally {
+            writer.close();
+        }
+    }
+
+    /**
+     * Test to write an LDIFChangeRecordWriter with attribute exclusions.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAttributeWithMatch() throws Exception {
+        final AttributeDescription attribute = AttributeDescription.valueOf("cn");
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAttribute(attribute);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(13);
+        for (String line : actual) {
+            // we have excluded this attribute especially.
+            assertThat(line).doesNotContain("cn: Samantha Carter");
+        }
+    }
+
+    /**
+     * Test to write an LDIFChangeRecordWriter with attribute exclusions. In
+     * this case, vip attribute is not present in the example. All lines must be
+     * written.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAttributeWithNoMatch() throws Exception {
+        final AttributeDescription attribute = AttributeDescription.valueOf("vip");
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setExcludeAttribute(attribute);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(14);
+        for (String line : actual) {
+            // we have excluded this attribute especially.
+            assertThat(line).doesNotContain("vip");
+        }
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFChangeRecordWriter. Inserting
+     * attribute cn (common name) & sn (surname).
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeAttributeWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+
+        writer.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        writer.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: add");
+        assertThat(actual.get(2)).contains("sn: Carter");
+        assertThat(actual.get(3)).contains("cn: ");
+        assertThat(actual.get(4)).contains("");
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFChangeRecordWriter. Inserting
+     * attribute cn (common name) & sn (surname)
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeAttributeWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+
+        writer.setIncludeAttribute(AttributeDescription.valueOf("vip"));
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: add");
+        assertThat(actual.get(2)).contains("");
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFChangeRecordWriter Throws a
+     * NullPointerException if the attributeDescription is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeAttributeDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        try {
+            writer.setIncludeAttribute(null);
+        } finally {
+            writer.close();
+        }
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFChangeRecordWriter DN included is
+     * "dc=example,dc=com", which is not the one from the record. Record must
+     * not be written.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */    @Test
+    public void testSetIncludeBranchWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dn = DN.valueOf("dc=example,dc=org");
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setIncludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // No result expected
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFChangeRecordWriter verifying right
+     * data are present.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeBranchWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dn = DN.valueOf("dc=example,dc=com");
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.setIncludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // Must contains all the attributes
+        assertThat(actual.get(0)).contains("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(14);
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFChangeRecordWriter Throws a
+     * NullPointerException if the includeBranch is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeBranchDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        try {
+            writer.setIncludeBranch(null);
+        } finally {
+            writer.close();
+        }
+    }
+
+    /**
+     * Test to write a record adding the user friendly Comment.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(enabled = false)
+    public void testSetAddUserFriendlyComments() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final CharSequence comment = "A simple comment";
+
+        writer.setAddUserFriendlyComments(true);
+        writer.writeComment0(comment);
+        writer.close();
+    }
+
+    /**
+     * Test WriteComment method of LDIFChangeRecordWriter using the wrap
+     * function.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunction() throws Exception {
+        final CharSequence comment = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        int wrapColumn = 15;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // The line length <= writer.wrapColumn
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+        }
+    }
+
+    /**
+     * Test WriteComment method of LDIFChangeRecordWriter using the wrap
+     * function. set wrap to 0.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunctionShortComment() throws Exception {
+        final CharSequence comment = "Lorem ipsum dolor";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        int wrapColumn = 30;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+        }
+    }
+
+    /**
+     * Test WriteComment method of LDIFChangeRecordWriter using the wrap
+     * function. The comment doesn't contain any empty spaces.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunctionNoEmptySpace() throws Exception {
+        final CharSequence comment = "Lorem ipsumdolorsitamet,consecteturadipisicingelit";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        int wrapColumn = 15;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // The line length <= writer.wrapColumn
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+        }
+    }
+
+    /**
+     * Write an ChangeRecord add type LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteChangeRecord() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ChangeRecord changeRequest = Requests.newChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter"
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: add");
+        assertThat(actual.get(2)).isEqualTo("sn: Carter");
+        assertThat(actual.get(3)).isEqualTo("");
+    }
+
+    /**
+     * Write an AddRequestChange LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final AddRequest changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(14);
+    }
+
+    /**
+     * Write an ChangeRecord LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddRequestNoBranchExcluded() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dnAdd = DN.valueOf("uid=scarter,ou=People,dc=example,dc=com");
+
+        // @formatter:off
+        final ChangeRecord changeRequest = Requests.newAddRequest(dnAdd)
+                .addAttribute("sn", "Carter");
+        // @formatter:on
+
+        final DN dn = DN.valueOf("dc=example,dc=org");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(4);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: add");
+        assertThat(actual.get(2)).isEqualTo("sn: Carter");
+    }
+
+    /**
+     * Write an ChangeRecord LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddRequestBranchExcluded() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        final DN dnAdd = DN.valueOf("uid=scarter,ou=People,dc=example,dc=com");
+
+        // @formatter:off
+        final ChangeRecord changeRequest = Requests.newAddRequest(dnAdd)
+                .addAttribute("sn", "Carter");
+        // @formatter:on
+
+        final DN dn = DN.valueOf("dc=example,dc=com");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Test to write a change record containing an URL.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddRequestJpegAttributeOk() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        final File file = File.createTempFile("sdk", ".jpeg");
+        final String url = file.toURI().toURL().toString();
+
+        final DN dnAdd = DN.valueOf("uid=scarter,ou=People,dc=example,dc=com");
+
+        // @formatter:off
+        final ChangeRecord changeRequest = Requests.newAddRequest(dnAdd)
+                .addAttribute("sn", "Carter")
+                .addAttribute("jpegphoto", url);
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        file.delete();
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(5);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: add");
+        assertThat(actual.get(2)).isEqualTo("sn: Carter");
+        assertThat(actual.get(3)).contains("jpegphoto: file:/");
+
+    }
+
+    /**
+     * Write an AddRequestChange LDIF. The dn/sn is base64 encoded, and contain
+     * ascii chars. If they aren't containing ascii, they will not be
+     * translated.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddBinaryRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final AddRequest changeRequest = Requests.newAddRequest(
+            "dn:: dWlkPXJvZ2FzYXdhcmE=",
+            "changetype: add",
+            "sn::cm9nYXNhd2FyYQ=="
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=rogasawara");
+        assertThat(actual.get(2)).isEqualTo("sn: rogasawara");
+        assertThat(actual.get(3)).isEqualTo("");
+        assertThat(actual.size()).isEqualTo(4);
+    }
+
+    /**
+     * Write an AddRequestChange LDIF. The dn/sn is base64 encoded, and contain
+     * ascii chars. If they aren't containing ascii, they will not be
+     * translated. In this case dn: uid=rogasawara,ou=営業部,o=Airius
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteAddBinaryNonAsciiRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final AddRequest changeRequest = Requests.newAddRequest(
+            "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz",
+            "changetype: add",
+            "sn::cm9nYXNhd2FyYQ=="
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0))
+                .isEqualTo("dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz");
+        assertThat(actual.get(2)).isEqualTo("sn: rogasawara");
+        assertThat(actual.get(3)).isEqualTo("");
+        assertThat(actual.size()).isEqualTo(4);
+    }
+
+    /**
+     * Write an DeleteRequest LDIF. Branch is excluded. The record musn't be
+     * written.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteDeleteRequestBranchExcluded() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        // @formatter:off
+        final DeleteRequest changeRequest = (DeleteRequest) Requests.newChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+        final DN dn = DN.valueOf("dc=example,dc=com");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Write an DeleteRequest LDIF. Branch is not excluded.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteDeleteRequestBranchNotExcluded() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        // @formatter:off
+        final DeleteRequest changeRequest = (DeleteRequest) Requests.newChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+        final DN dn = DN.valueOf("dc=example,dc=org");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(3);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: delete");
+        assertThat(actual.get(2)).isEqualTo("");
+
+    }
+
+    /**
+     * Write a delete request.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteDeleteRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final DeleteRequest changeRequest = (DeleteRequest) Requests.newChangeRecord(
+            "# Delete an existing entry",
+            "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: cn=Robert Jensen,ou=Marketing,dc=airius,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: delete");
+    }
+
+    /**
+     * A delete Record with a control.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteDeleteRequestContainingControl() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        final DN dn = DN.valueOf("uid=scarter,ou=People,dc=example,dc=com");
+
+        // @formatter:off
+        final DeleteRequest changeRequest = Requests.newDeleteRequest(dn)
+            .addControl(PersistentSearchRequestControl.newControl(
+                true, true, true, // isCritical, changesOnly, returnECs
+                PersistentSearchChangeType.ADD,
+                PersistentSearchChangeType.DELETE,
+                PersistentSearchChangeType.MODIFY,
+                PersistentSearchChangeType.MODIFY_DN
+            )
+            );
+        // @formatter:on
+
+        writer.writeComment("This record contains a control");
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("# This record contains a control");
+        assertThat(actual.get(1)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(2)).contains("control: 2.16.840.1.113730.3.4.3 true");
+        assertThat(actual.get(3)).isEqualTo("changetype: delete");
+        assertThat(actual.get(4)).isEqualTo("");
+    }
+
+    /**
+     * Write a delete request with illegal argu;ent : the following example is
+     * containing additional lines after the changetype when none were expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testWriteDeleteRequestIllegalArguments() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final DeleteRequest changeRequest = (DeleteRequest) Requests.newChangeRecord(
+            "# Delete an existing entry",
+            "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: delete",
+            "dn: cn=Robert , ou=Marketing, dc=airius, dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0))
+                .isEqualTo("dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: delete");
+    }
+
+    /**
+     * Write an ChangeRecord Moddn LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModdnRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyDNRequest changeRequest =
+            Requests.newModifyDNRequest("uid=scarter,ou=People,dc=example,dc=com", "cn=carter");
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(5);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: modrdn");
+        assertThat(actual.get(2)).isEqualTo("newrdn: cn=carter");
+        assertThat(actual.get(3)).isEqualTo("deleteoldrdn: 0");
+
+    }
+
+    /**
+     * Write an ChangeRecord Moddn LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModdnRequestNewSuperior() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        // @formatter:off
+        final ModifyDNRequest changeRequest = (ModifyDNRequest) Requests.newChangeRecord(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: cn=carter",
+            "deleteoldrdn: true",
+            "newsuperior:   ou=People,dc=example,dc=org"
+        );
+        // @formatter:on
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(6);
+        assertThat(actual.get(3)).isEqualTo("deleteoldrdn: 1");
+        assertThat(actual.get(4)).isEqualTo("newsuperior: ou=People,dc=example,dc=org");
+    }
+
+    /**
+     * Write a Moddn request.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModdnRequestDeleterdnFalse() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        // @formatter:off
+        final ModifyDNRequest changeRequest = (ModifyDNRequest) Requests.newChangeRecord(
+            "version: 1",
+            "",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: cn=carter",
+            "deleteoldrdn: false"
+        );
+        // @formatter:on
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isEqualTo(5);
+        assertThat(actual.get(3)).isEqualTo("deleteoldrdn: 0");
+    }
+
+    /**
+     * Write a modify request.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = Requests.newModifyRequest(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000"
+        );
+
+        // @formatter:on
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // version number is skipped.
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: modify");
+        assertThat(actual.get(2)).isEqualTo("add: work-phone");
+        assertThat(actual.get(3)).isEqualTo("work-phone: 650/506-7000");
+        assertThat(actual.get(4)).isEqualTo("-");
+    }
+
+    /**
+     * Write a modify request containing a control.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequestUsingControl() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = Requests.newModifyRequest("cn=scarter,dc=example,dc=com")
+                .addControl(PreReadRequestControl.newControl(true, "mail"))
+                .addModification(
+                        ModificationType.REPLACE, "mail", "modified@example.com");
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(7);
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(1)).contains("control: 1.3.6.1.1.13.1 true:");
+        assertThat(actual.get(2)).isEqualTo("changetype: modify");
+        assertThat(actual.get(3)).isEqualTo("replace: mail");
+        assertThat(actual.get(4)).isEqualTo("mail: modified@example.com");
+        assertThat(actual.get(5)).isEqualTo("-");
+    }
+
+    /**
+     * Write an ModifyRequest LDIF.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequestNoModifications() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = Requests.newModifyRequest(
+            "version: 1",
+            "",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify"
+        );
+        // @formatter:on
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        // No changes, nothing to do, the record is not written.
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Write a modify request using an exclusion filter attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequestFilterAttributesExcluded() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = (ModifyRequest) Requests.newChangeRecord(
+            "version: 1",
+            "",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify",
+            "replace: work-phone",
+            "work-phone: 555-555-1155"
+        );
+        // @formatter:on
+
+        writer.setExcludeAttribute(AttributeDescription.valueOf("work-phone"));
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(3);
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: modify");
+        assertThat(actual.get(2)).isEqualTo("");
+    }
+
+    /**
+     * Write a modify request using branch exclusion.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequestBranchExcludedNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = (ModifyRequest) Requests.newChangeRecord(
+            "version: 1",
+            "",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify",
+            "replace: work-phone",
+            "work-phone: 555-555-1155"
+        );
+        // @formatter:on
+
+        final DN dn = DN.valueOf("dc=example,dc=org");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(6); // all line plus a ""
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: modify");
+        assertThat(actual.get(5)).isEqualTo("");
+    }
+
+    /**
+     * Write a modify request using branch exclusion.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyRequestBranchExcludedMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyRequest changeRequest = (ModifyRequest) Requests.newChangeRecord(
+            "version: 1",
+            "",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify",
+            "replace: work-phone",
+            "work-phone: 555-555-1155"
+        );
+        // @formatter:on
+
+        final DN dn = DN.valueOf("dc=example,dc=com");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Write a modifyDN request with a branch exclusion.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyDNRequestBranchExcludedNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyDNRequest changeRequest = (ModifyDNRequest) Requests.newChangeRecord(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteoldrdn: no"
+        );
+        // @formatter:on
+        final DN dn = DN.valueOf("dc=example,dc=org");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(5);
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(3)).isEqualTo("deleteoldrdn: 0");
+    }
+
+    /**
+     * Write a modifyDN request with a branch exclusion.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyDNRequestBranchExcludedMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyDNRequest changeRequest = (ModifyDNRequest) Requests.newChangeRecord(
+            "version: 1",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: cn=Susan Jacobs",
+            "deleteoldrdn: no"
+        );
+        // @formatter:on
+
+        final DN dn = DN.valueOf("dc=example,dc=com");
+        writer.setExcludeBranch(dn);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Write a modifyDN request.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteModifyDNRequest() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        // @formatter:off
+        final ModifyDNRequest changeRequest =
+                Requests.newModifyDNRequest("cn=scarter,dc=example,dc=com", "cn=Susan Jacobs")
+                .setDeleteOldRDN(true);
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(5);
+        assertThat(actual.get(0)).isEqualTo("dn: cn=scarter,dc=example,dc=com");
+        assertThat(actual.get(1)).isEqualTo("changetype: modrdn");
+        assertThat(actual.get(2)).isEqualTo("newrdn: cn=Susan Jacobs");
+        assertThat(actual.get(3)).isEqualTo("deleteoldrdn: 1");
+    }
+
+    /**
+     * Write a full example containing multiple change records.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteMultipleChangeRecords() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        final ChangeRecord changeRequest = Requests.newAddRequest(getAddLDIFChangeRecord());
+
+        // @formatter:off
+        final ModifyDNRequest changeRequest2 =
+                Requests.newModifyDNRequest("cn=scarter,dc=example,dc=com", "cn=Susan Jacobs")
+                .setDeleteOldRDN(false);
+        // @formatter:on
+
+        // @formatter:off
+        final ModifyRequest changeRequest3 = Requests.newModifyRequest(
+            "version: 1",
+            "",
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: modify",
+            "replace: work-phone",
+            "work-phone: 555-555-1155"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final AddRequest changeRequest4 = Requests.newAddRequest(
+            "version: 1",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter"
+        );
+        // @formatter:on
+
+        final File file = File.createTempFile("sdk", null);
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final AddRequest changeRequest5 = Requests.newAddRequest(
+            "version: 1",
+            "# Add a new record",
+            "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
+            "changetype: add",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Fiona Jensen",
+            "sn: Jensen",
+            "uid: fiona",
+            "telephonenumber: +1 408 555 1212",
+            "jpegphoto:< " + url
+        );
+        // @formatter:on
+
+        writer.writeChangeRecord(changeRequest);
+        writer.writeChangeRecord(changeRequest2);
+        writer.writeChangeRecord(changeRequest3);
+        writer.writeChangeRecord(changeRequest4);
+        writer.writeComment("A comment...");
+        writer.writeChangeRecord(changeRequest5);
+        writer.close();
+
+        assertThat(actual.size()).isGreaterThan(10);
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.get(actual.size() - 1)).isEqualTo("");
+
+        file.delete();
+    }
+
+    /**
+     * Write a record containing multiple changes.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteMultipleChangesRecord() throws Exception {
+        // @formatter:off
+        final ChangeRecord changeRequest = Requests.newChangeRecord(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000",
+            "work-phone: 650/506-7001",
+            "-",
+            "delete: home-fax",
+            "-",
+            "replace: home-phone",
+            "home-phone: 415/697-8899"
+        );
+        // @formatter:on
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(actual.size()).isGreaterThan(10);
+    }
+
+    /**
+     * Test to write a simple comment with the LDFChangeRecordWriter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteComment() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        writer.writeComment("TLDIFChangeRecordWriter, this is a comment.");
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("# TLDIFChangeRecordWriter, this is a comment.");
+    }
+
+    /**
+     * Verify the LDIFWriteChangeRecord write and correctly flush and close.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWriteChangeRecordFlushClose() throws Exception {
+        final OutputStream mockOutput = mock(OutputStream.class);
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(mockOutput);
+        try {
+            writer.writeComment("TLDIFChangeRecordWriter, this is a comment.");
+            writer.flush();
+            writer.flush();
+            verify(mockOutput, times(2)).flush();
+        } finally {
+            writer.close();
+            verify(mockOutput, times(1)).close();
+        }
+    }
+
+    /**
+     * Test the LDIFWriteChangeRecord using an output file verifying write is
+     * correctly invoked.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteEntryOutputStreamUsingByteArrayOutputStream() throws Exception {
+        final OutputStream out = new ByteArrayOutputStream();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(out);
+
+        // @formatter:off
+        final String[] lines = {
+            "dn: cn=scarter,dc=example,dc=com",
+            "changetype: add",
+            "sn: Carter"
+        };
+        // @formatter:on
+
+        final AddRequest changeRequest = Requests.newAddRequest(lines);
+        writer.writeChangeRecord(changeRequest);
+        writer.close();
+
+        BufferedReader reader = new BufferedReader(
+                new InputStreamReader(
+                        new ByteArrayInputStream(out.toString().getBytes())));
+
+        assertThat(reader.readLine()).isEqualTo(lines[0]);
+        assertThat(reader.readLine()).isEqualTo(lines[1]);
+        assertThat(reader.readLine()).isEqualTo(lines[2]);
+        assertThat(reader.readLine()).isEqualTo("");
+        assertThat(reader.readLine()).isNull();
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryReaderTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryReaderTestCase.java
new file mode 100644
index 0000000..1fdbcf5
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryReaderTestCase.java
@@ -0,0 +1,1662 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import static org.testng.Assert.assertNotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Matcher;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.Action;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyListOf;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the LDIFEntryReader functionality.
+ */
+@SuppressWarnings("javadoc")
+public final class LDIFEntryReaderTestCase extends AbstractLDIFTestCase {
+    /**
+     * Provide a standard entry for the tests below.
+     *
+     * @return well formed LDIF entry
+     */
+    public final String[] getStandardEntry() {
+        // @formatter:off
+        return new String[] {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "postalAddress: Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode: 50369",
+            "uid: user.0",
+            "description: This is the description for Aaccf Amar.",
+            "userPassword: {SSHA}hpbT8dLi8xgYy2kl4aP6QKGzsFdhESWpPmDTEw==",
+            "employeeNumber: 0",
+            "initials: ASA",
+            "givenName: Aaccf",
+            "pager: +1 779 041 6341",
+            "mobile: +1 010 154 3228",
+            "cn: Aaccf Amar",
+            "telephoneNumber: +1 685 622 6202",
+            "sn: Amar",
+            "street: 01251 Chestnut Street",
+            "homePhone: +1 225 216 5900",
+            "mail: user.0@maildomain.net",
+            "l: Panama City", "st: DE",
+            "pwdChangedTime: 20120903142126.219Z",
+            "entryDN: uid=user.0,ou=people,dc=example,dc=org",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Number of attributes of the standard entry.
+     */
+    public final int nbStandardEntryAttributes = new LinkedHashMapEntry(getStandardEntry())
+            .getAttributeCount();
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryReader. Excluding the
+     * "dc=example,dc=com" Entry is not read and function return a
+     * NoSuchElementException exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetExcludeBranchWithNoMatch() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeBranch(DN.valueOf("dc=example,dc=com"));
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryReader. Excluding the
+     * "dc=example,dc=org", which is not in the standard ldif entry. Entry must
+     * be fully read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeBranchWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeBranch(DN.valueOf("dc=example,dc=org"));
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+    }
+
+    /**
+     * Test the setExcludeBranch with a null parameter.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeBranchDoesntAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeBranch(null);
+        reader.close();
+    }
+
+    /**
+     * Test to read an entry excluding user attributes. Default case - all the
+     * lines must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesFalse() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeAllUserAttributes(false);
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        assertThat(entry.getAttribute("entryDN")).isNotNull();
+        assertThat(entry.getAttribute("description")).isNotNull();
+    }
+
+    /**
+     * Test to read an entry excluding user attributes Only the operational
+     * attributes must be read (entryDN, entryUUID, modifyTimestamp,
+     * modifiersName...) (e.g : 4 in the standard entry)
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesTrue() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeAllUserAttributes(true);
+
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttribute("dn")).isNull();
+        assertThat(entry.getAttribute("sn")).isNull();
+        assertThat(entry.getAttribute("uid")).isNull();
+        assertThat(entry.getAttribute("description")).isNull();
+
+        assertThat(entry.getAttribute("entryDN")).isNotEmpty();
+        assertThat(entry.getAttribute("entryUUID")).isNotEmpty();
+        assertThat(entry.getAttribute("modifyTimestamp")).isNotNull();
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+    }
+
+    /**
+     * Test to read an entry with attribute exclusions. In this test, the
+     * attribute description 'vip' doesn't exist... the entry must be fully
+     * read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAttributeWithNoMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeAttribute(AttributeDescription.valueOf("vip"));
+
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        // no attribute 'vip'
+        assertThat(entry.getAttribute("vip")).isNull();
+    }
+
+    /**
+     * Test to read an entry with attribute exclusions. Three attributes
+     * excluded, entry must contain the others.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAttributeWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+
+        reader.setExcludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("sn"));
+        reader.setExcludeAttribute(AttributeDescription.valueOf("entryDN"));
+
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttribute("entryDN")).isNull();
+        assertThat(entry.getAttribute("sn")).isNull();
+        assertThat(entry.getAttribute("cn")).isNull();
+
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes - 3);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+    }
+
+    /**
+     * {@link LDIFEntryReader#setExcludeAttribute(AttributeDescription)}
+     * does not allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeAttributeDoesntAllowNull() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setExcludeAttribute(null);
+        reader.close();
+    }
+
+    /**
+     * Test to read an entry excluding all operational attributes
+     * setExcludeAllOperationalAttributes to false (default case) All attributes
+     * must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesFalse() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+
+        reader.setExcludeAllOperationalAttributes(false);
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        assertThat(entry.getAttribute("entryDN")).isNotNull();
+        assertThat(entry.getAttribute("entryUUID")).isNotNull();
+        assertThat(entry.getAttribute("modifyTimestamp")).isNotNull();
+    }
+
+    /**
+     * Test to read an entry excluding all operational attributes
+     * setExcludeAllOperationalAttributes is forced to true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesTrue() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+
+        reader.setExcludeAllOperationalAttributes(true);
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttribute("entryDN")).isNull();
+        assertThat(entry.getAttribute("entryUUID")).isNull();
+        assertThat(entry.getAttribute("modifyTimestamp")).isNull();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isLessThan(nbStandardEntryAttributes);
+    }
+
+    /**
+     * Test SetExcludeFilter method of LDIFEntryReader. Throws a
+     * NullPointerException if the excludeFilter is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testsetExcludeFilterDoesntAllowNull() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+
+        reader.setExcludeFilter(null);
+        reader.close();
+    }
+
+    /**
+     * Test testSetExcludeFilter method of LDIFEntryReader. StandardEntry has an
+     * objectclass : person, not vip. The filter must exclude all entries with
+     * an objectclass = vip. In this case, entry must be fully read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetExcludeFilterWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final Filter filter = Filter.equality("objectclass", "vip");
+        final Matcher excludeFilter = filter.matcher();
+
+        reader.setExcludeFilter(excludeFilter);
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("objectclass").toString()).isNotEqualTo("vip");
+    }
+
+    /**
+     * Test testSetExcludeFilter method of LDIFEntryReader. StandardEntry has an
+     * objectclass : person. The filter must exclude all entries with an
+     * objectclass = person. Entry musn't be read.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetExcludeFilterWithNoMatch() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final Filter filter = Filter.equality("objectclass", "person");
+        final Matcher excludeFilter = filter.matcher();
+
+        reader.setExcludeFilter(excludeFilter);
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test the setIncludeAttribute Attributes included must be the only ones
+     * present in the entry. First line dn must be present.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeAttributeWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        reader.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        reader.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        final Entry entry = reader.readEntry();
+
+        assertThat(entry).isNotNull();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(entry.getAttribute("cn")).isNotNull();
+        assertThat(entry.getAttribute("sn")).isNotNull();
+        assertThat(entry.getAttribute("description")).isNull();
+
+        reader.close();
+    }
+
+    /**
+     * Test the setIncludeAttribute Attributes included must be the only ones
+     * present in the entry. In this case, the attribute "manager" doesn't
+     * exist. Only dn line must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeAttributeWithNoMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeAttribute(AttributeDescription.valueOf("manager"));
+        final Entry entry = reader.readEntry();
+
+        assertThat(entry).isNotNull();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(0);
+        assertThat(entry.getAttribute("description")).isNull();
+
+        reader.close();
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFEntryReader Throws a
+     * NullPointerException if the includeAttribute is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeAttributeDoesntAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeAttribute(null);
+        reader.close();
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryReader. "dc=example,dc=org" not
+     * existing in the standard ldif entry. Entry must not be read.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetIncludeBranchWithNoMatch() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeBranch(DN.valueOf("dc=example,dc=org"));
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryReader. "dc=example,dc=com" is
+     * the branch of the standard entry. Entry must be fully read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeBranchWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeBranch(DN.valueOf("dc=example,dc=com"));
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry).isNotNull();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryReader. Throws a
+     * NullPointerException if the includeBranch is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeBranchDoesntAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeBranch(null);
+        reader.close();
+    }
+
+    /**
+     * LDIFEntryReader setIncludeFilter with an equality filter on the
+     * objectclass: vip, Entry musn't be read.
+     *
+     * @throws Exception
+     *             NoSuchElementException launched if entry is not read
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testSetIncludeFilterWithNoMatch() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final Filter filter = Filter.equality("objectclass", "vip");
+        final Matcher includeFilter = filter.matcher();
+        reader.setIncludeFilter(includeFilter);
+        Entry entry = null;
+        try {
+            entry = reader.readEntry();
+        } finally {
+            reader.close();
+        }
+        assertThat(entry).isNull();
+
+    }
+
+    /**
+     * LDIFEntryReader setIncludeFilter with an equality filter on the
+     * objectclass: person, Entry must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetIncludeFilterWithMatch() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final Filter filter = Filter.equality("objectclass", "person");
+        final Matcher includeFilter = filter.matcher();
+        reader.setIncludeFilter(includeFilter);
+        Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("cn")).isNotNull();
+        assertThat(entry.getAttribute("sn")).isNotNull();
+
+    }
+
+    /**
+     * LDIFEntryReader setIncludeFilter doesn't allow null.
+     *
+     * @throws Exception
+     *             NullPointerException expected
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeFilterDoesntAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setIncludeFilter(null);
+        reader.close();
+    }
+
+    /**
+     * Tests reading a malformed record invokes the rejected record listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedLDIFListenerMalformedFirstRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        LDIFEntryReader reader =
+                new LDIFEntryReader("dn: baddn", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example");
+
+        reader.setRejectedLDIFListener(listener);
+
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleMalformedRecord(
+                eq(1L),
+                eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example")),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a malformed LDIF invokes the rejected LDIF listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedLDIFListenerMalformedSecondRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFEntryReader reader = new LDIFEntryReader(
+                "dn: dc=example,dc=com",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: domainComponent",
+                "dc: example",
+                "",
+                "dn: baddn",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: domainComponent",
+                "dc: example"
+        );
+        // @formatter:on
+
+        reader.setRejectedLDIFListener(listener);
+
+        reader.readEntry(); // Skip good record.
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleMalformedRecord(
+                eq(7L),
+                eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example")),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a LDIF which does not conform to the schema invokes the
+     * rejected LDIF listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedRecordListenerRejectsBadSchemaRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        // @formatter:off
+        LDIFEntryReader reader = new LDIFEntryReader(
+            "dn: dc=example,dc=com",
+            "changetype: add",
+            "objectClass: top",
+            "objectClass: domainComponent",
+            "dc: example",
+            "xxx: unknown attribute");
+        reader.setRejectedLDIFListener(listener)
+             .setSchemaValidationPolicy(
+                 SchemaValidationPolicy.ignoreAll()
+                     .checkAttributesAndObjectClasses(Action.REJECT));
+        // @formatter:on
+
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleSchemaValidationFailure(
+                eq(1L),
+                eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example", "xxx: unknown attribute")),
+                anyListOf(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * Tests reading a LDIF which does not conform to the schema invokes the
+     * rejected LDIF listener.
+     *
+     * @throws Exception
+     *             if an unexpected error occurred.
+     */
+    @Test
+    public void testRejectedLDIFListenerWarnsBadSchemaRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        LDIFEntryReader reader =
+                new LDIFEntryReader("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example", "xxx: unknown attribute");
+        reader.setRejectedLDIFListener(listener).setSchemaValidationPolicy(
+                SchemaValidationPolicy.ignoreAll().checkAttributesAndObjectClasses(Action.WARN));
+
+        assertThat(reader.hasNext()).isTrue();
+
+        Entry entry = reader.readEntry();
+
+        assertThat(entry.getName().toString()).isEqualTo("dc=example,dc=com");
+        assertThat(entry.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
+        assertThat(entry.containsAttribute("dc", "example")).isTrue();
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+
+        verify(listener).handleSchemaValidationWarning(
+                eq(1L),
+                eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add", "objectClass: top",
+                        "objectClass: domainComponent", "dc: example", "xxx: unknown attribute")),
+                anyListOf(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * LDIFEntryReader setRejectedLDIFListener skips the record.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testRejectedLDIFListenerSkipsRecord() throws Exception {
+        RejectedLDIFListener listener = mock(RejectedLDIFListener.class);
+
+        LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setRejectedLDIFListener(listener).setExcludeBranch(DN.valueOf("dc=com"));
+
+        assertThat(reader.hasNext()).isFalse();
+
+        verify(listener).handleSkippedRecord(eq(1L), eq(Arrays.asList(getStandardEntry())),
+                any(LocalizableMessage.class));
+        reader.close();
+    }
+
+    /**
+     * LDIFEntryReader setIncludeFilter allows null.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetRejectedLDIFListenerDoesAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setRejectedLDIFListener(null);
+        Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+
+    }
+
+    /**
+     * LDIFEntryReader setSchemaValidationPolicy. Validate the entry depending
+     * of the selected policy. Entry is here NOT allowed because it contains a
+     * uid attribute which is not allowed by the SchemaValidationPolicy.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testSetSchemaValidationPolicyDefaultRejectsEntry() throws Exception {
+        // @formatter:off
+        String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com", "objectClass: person",
+            "objectClass: top", "cn: Aaccf Amar", "sn: Amar", "uid: user.0"
+        };
+        // @formatter:on
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader setSchemaValidationPolicy. Validate the entry depending
+     * of the selected policy. Entry is here allowed because it fills the case
+     * of the validation.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetSchemaValidationPolicyDefaultAllowsEntry() throws Exception {
+        // @formatter:off
+        String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com", "objectClass: person",
+            "objectClass: top", "cn: Aaccf Amar", "sn: Amar"
+        };
+        // @formatter:on
+
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        final Entry entry = reader.readEntry();
+        reader.close();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+    }
+
+    /**
+     * LDIFEntryReader SetValidationPolicy doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetSchemaValidationPolicyDoesntAllowNull() throws Exception {
+
+        // @formatter:off
+        String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com", "objectClass: person",
+            "objectClass: top", "cn: Aaccf Amar", "sn: Amar", "uid: user.0"
+        };
+        // @formatter:on
+
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(null);
+        reader.close();
+    }
+
+    /**
+     * Test the setSchemaSetValidationPolicy. Adding a new schema and insuring
+     * the validationPolicy allows the new attribute/class Adding a new schema
+     * explained in the admin-guide (chapter 15. Managing Schema). The new
+     * attribute is accepted by the policy schema. Entry must be read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetSchemaSetSchemaValidationPolicyDefaultAllowsEntryWithNewAttribute()
+            throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: top", "cn: Aaccf Amar",
+            "sn: Amar",
+            "objectClass: myCustomObjClass",
+            "myCustomAttribute: Testing..."
+        };
+        // @formatter:on
+
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        // Adding the new schema containing the customclass
+        scBuild.addObjectClass("( temporary-fake-oc-id NAME 'myCustomObjClass"
+                + "' SUP top AUXILIARY MAY myCustomAttribute )", false);
+        scBuild.addAttributeType("( temporary-fake-attr-id NAME 'myCustomAttribute' EQUALITY case"
+                + "IgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstrings"
+                + "Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )", false);
+        // Adding default core schema
+        scBuild.addSchema(Schema.getCoreSchema(), false);
+        Schema schema = scBuild.toSchema();
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        reader.setSchema(schema);
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        Entry entry = null;
+        try {
+            entry = reader.readEntry();
+            // cn + sn + myCustomAttribute + objectClass
+            assertThat(entry.getAttributeCount()).isEqualTo(4);
+            assertThat(entry.getName().toString()).isEqualTo(
+                    "uid=user.0,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttribute("sn").firstValue().toString()).isEqualTo("Amar");
+            assertThat(entry.getAttribute("cn").firstValueAsString()).isEqualTo("Aaccf Amar");
+            // entry.getAttribute("new attribute") : access by that way doesn't work...
+            // TODO BUG jira/browse/OPENDJ-157
+            assertThat(
+                    entry.getAttribute(AttributeDescription.valueOf("myCustomAttribute", schema))
+                            .firstValueAsString()).isEqualTo("Testing...");
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test the setSchemaSetValidationPolicy : throw an exception if
+     * unrecognized attributes are found. ex. Entry
+     * "uid=user.0,ou=People,dc=example,dc=com" doesn't respect the schema
+     * because it contains an unrecognized object class "myCustomObjClass".
+     * Entry musn't be read.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testSetSchemaSetSchemaValidationPolicyDefaultDoesntAllowEntryWithNewAttribute()
+            throws Exception {
+
+        // @formatter:off
+        String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com", "objectClass: person",
+            "objectClass: organizationalperson", "objectClass: top", "cn: Aaccf Amar",
+            "sn: Amar", "objectClass: myCustomObjClass", "myCustomAttribute: Testing..."
+        };
+        // @formatter:on
+
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+
+        final SchemaBuilder scBuild = new SchemaBuilder();
+        scBuild.addSchema(Schema.getCoreSchema(), false);
+
+        reader.setSchema(scBuild.toSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader setSchema doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetSchemaDoesntAllowNull() throws Exception {
+
+        final LDIFEntryReader reader = new LDIFEntryReader(getStandardEntry());
+        reader.setSchema(null); // must throw a NullPointerException
+        reader.close();
+    }
+
+    /**
+     * Tests readEntry method of LDIFEntryReader class.See
+     * https://opends.dev.java.net/issues/show_bug.cgi?id=4545 for more details.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testEmpty() throws Exception {
+        final String path = TestCaseUtils.createTempFile("");
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        try {
+            Assert.assertFalse(reader.hasNext());
+            Assert.assertFalse(reader.hasNext());
+            reader.readEntry();
+        } finally {
+            Assert.assertFalse(reader.hasNext());
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry with no empty spaces.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testReadEntryWithNoSpaces() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "# Entry of SCarter",
+            "dn:uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass:person",
+            "objectClass:inetorgperson",
+            "objectClass:organizationalperson",
+            "objectClass:top",
+            "postalAddress:Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode:50369",
+            "uid:scarter",
+            "description::U2hvcnQgZGVzY3JpcHRpb24gb2YgU2NhcnRlcg=="
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        Entry entry = null;
+        try {
+            assertThat(reader.hasNext());
+            entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo(
+                    "uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttribute("uid").firstValueAsString()).isEqualTo("scarter");
+            assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                    "Short description of Scarter");
+        } finally {
+            reader.close();
+        }
+
+    }
+
+    /**
+     * Test to read an entry containing spaces before the attribute.
+     */
+    @Test
+    public void testReadEntryWithAttributesSpacesAtStart() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "#   Entry of SCarter",
+            "dn:   uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass:   person",
+            "objectClass:   inetorgperson",
+            "objectClass:   organizationalperson",
+            "objectClass:   top",
+            "postalAddress:   Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode:   50369",
+            "uid:    scarter",
+            "description::    U2hvcnQgZGVzY3JpcHRpb24gb2YgU2NhcnRlcg==",
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        Entry entry = null;
+        try {
+            assertThat(reader.hasNext());
+            entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo(
+                    "uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttribute("uid").firstValueAsString()).isEqualTo("scarter");
+            assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                    "Short description of Scarter");
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing spaces at the end of the attribute. ldif
+     * do not admit spaces at end ;)
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadEntryWithAttributesSpacesAtEnd() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "#   Entry of SCarter   ",
+            "dn:   uid=scarter,ou=People,dc=example,dc=com    ",
+            "objectClass:   person    ",
+            "objectClass:   inetorgperson    ",
+            "objectClass:   organizationalperson    ",
+            "objectClass:   top  ",
+            "postalAddress:   Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369  ",
+            "postalCode:   50369 ",
+            "uid:    scarter  ",
+            "description::    U2hvcnQgZGVzY3JpcHRpb24gb2YgU2NhcnRlcg==   ",
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        Entry entry = null;
+        try {
+            assertThat(reader.hasNext());
+            entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo(
+                    "uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttribute("uid").firstValueAsString()).isEqualTo("scarter");
+            assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                    "Short description of Scarter");
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Tests readEntry method of LDIFEntryReader class.See
+     * https://opends.dev.java.net/issues/show_bug.cgi?id=4545 for more details.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public void testReadEntry() throws Exception {
+        final String path = TestCaseUtils.createTempFile(getStandardEntry());
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        try {
+            Assert.assertTrue(reader.hasNext());
+            final Entry entry = reader.readEntry();
+            assertNotNull(entry);
+            Assert.assertEquals(entry.getName(), DN
+                    .valueOf("uid=user.0,ou=People,dc=example,dc=com"));
+            Assert.assertFalse(reader.hasNext());
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing duplicates values
+     * ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE &&
+     * WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testLDIFEntryReaderEntryWithDuplicateAttributes() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=user.0,ou=People,dc=example,dc=com",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "postalAddress: Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode: 50369",
+            "description: This is the description for Aaccf Amar.",
+            "userPassword: ;", // empty value allowed
+            "telephoneNumber: +1 685 622 6202", "sn: Amar",
+            // entryUUID : ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "entryUUID: ad55a34a-763f-358f-93f9-da45f9ecd9e4",
+            // WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE :
+            "objectClass: person",
+            "objectClass: person"
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader - Try to read a full example of entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLDIFEntryReaderFullEntry() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "version: 1",
+            "dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Barbara Jensen",
+            "cn: Barbara J Jensen",
+            "cn: Babs Jensen",
+            "sn: Jensen",
+            "uid: bjensen",
+            "telephonenumber: +1 408 555 1212",
+            "description: A big sailing fan.",
+            "", // if a space here, second entry is not read
+            "dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Bjorn Jensen",
+            "sn: Jensen",
+            "telephonenumber: +1 408 555 1212"
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+
+        try (LDIFEntryReader reader = new LDIFEntryReader(in)) {
+            assertThat(reader.hasNext());
+            // 1st entry
+            Entry entry = reader.readEntry();
+            assertThat(entry.getName()
+                            .toString()).isEqualTo("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com");
+            assertThat(entry.getAttributeCount()).isEqualTo(6);
+            // 2nd
+            entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com");
+            assertThat(entry.getAttributeCount()).isEqualTo(4);
+
+            assertThat(reader.hasNext()).isFalse();
+        }
+    }
+
+
+    /**
+     * Tries to read an entry composed by multi-valued attributes. The multi-valued attributes contains an interesting
+     * case where two of them represents the same value, one in uppercase and the other in lower case.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLDIFEntryReaderMultiplesAttributeValuesDifferentLetterCase() throws Exception {
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: cn=Character Set,cn=Password Validators,cn=config",
+            "objectClass: ds-cfg-character-set-password-validator",
+            "objectClass: ds-cfg-password-validator",
+            "objectClass: top",
+            "ds-cfg-enabled: true",
+            "ds-cfg-java-class: org.opends.server.extensions.CharacterSetPasswordValidator",
+            "ds-cfg-allow-unclassified-characters: true",
+            "ds-cfg-character-set: 1:abcdefghijklmnopqrstuvwxyz",
+            "ds-cfg-character-set: 1:ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+            "ds-cfg-character-set: 1:0123456789",
+            "ds-cfg-character-set: 1:~!@#$%^&*()-_=+[]{}|;:,.<>/?",
+            "cn: Character Set"
+        };
+        // @formatter:on
+        final String path = TestCaseUtils.createTempFile(strEntry);
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        try {
+            assertThat(reader.hasNext());
+            final Entry entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo(
+                    "cn=Character Set,cn=Password Validators,cn=config");
+            // List the attributes : objectClass ds-cfg-enabled ds-cfg-java-class
+            // ds-cfg-allow-unclassified-characters ds-cfg-character-set cn
+            assertThat(entry.getAttributeCount()).isEqualTo(6);
+            assertThat(entry.getAttribute("ds-cfg-character-set")).isNotEmpty();
+            assertThat(entry.getAttribute("ds-cfg-character-set").toArray().length).isEqualTo(4);
+            assertThat(reader.hasNext()).isFalse();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Testing to read an entry which containing empty required attributes.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testValueOfLDIFEntryReadStandardEntryMissingValues() throws Exception {
+
+        // @formatter:off
+        final String[] strEntry = {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "cn: Aaccf Amar",
+            "sn:"
+        };
+        // @formatter:on
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+
+    }
+
+    /**
+     * Testing to read an entry containing BER value
+     * schemaValidationPolicy.checkAttributeValues().needsChecking() &&
+     * attributeDescription.containsOption("binary") reply 'because it has an
+     * unexpected binary option for attribute sn : 'sn;binary'.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testValueOfLDIFEntryBERUnexpectedBinaryOption() throws Exception {
+
+        // @formatter:off
+        final String[] strEntry = {
+            "version: 1",
+            "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz",
+            "# dn:: ou=<JapaneseOU>,o=Airius",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Horatio Jensen",
+            "cn: Horatio N Jensen",
+            "sn: Jensen",
+            "uid: hjensen",
+            "sn;binary:: 5bCP56yg5Y6f"
+        };
+        // @formatter:on
+
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        Schema schema = Schema.getCoreSchema();
+        reader.setSchema(schema);
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+
+    }
+
+    /**
+     * Testing to read an entry containing a fatal continuation line at start.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testValueOfLDIFEntryFatalContinuationLineAtStart() throws Exception {
+
+        // @formatter:off
+        final String[] strEntry = {
+            " This is a fatal continuation line at start",
+            "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz",
+            "# dn:: ou=<JapaneseOU>,o=Airius",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Horatio Jensen",
+            "cn: Horatio N Jensen",
+            "sn: Jensen",
+            "uid: hjensen"
+        };
+        // @formatter:on
+
+        final LDIFEntryReader reader = new LDIFEntryReader(strEntry);
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+
+    }
+
+    /**
+     * LDIFEntryReader entry containing a reference to an external file.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testValueOfLDIFEntryReadEntryContainingURL() throws Exception {
+        final File file = File.createTempFile("sdk", ".jpeg");
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(
+                "#A single comment",
+                " continued in the second line",
+                "version: 1",
+                "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz",
+                "# dn:: ou=<JapaneseOU>,o=Airius",
+                "objectclass: top",
+                "objectclass: person",
+                "objectclass: organizationalPerson",
+                "cn: Horatio Jensen",
+                "cn: Horatio N Jensen",
+                "sn: Jensen",
+                "uid: hjensen",
+                "telephonenumber: +1 408 555 1212",
+                "jpegphoto:< " + url,
+                "#This is a end line comment", "# Followed by another"
+        );
+        // @formatter:on
+
+        try {
+            Entry entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isNotEqualTo("b3U95Za25qWt6YOoLG89QWlyaXVz");
+            assertThat(entry.getAttributeCount()).isEqualTo(6);
+            assertThat(entry.getAttribute("jpegphoto")).isNotEmpty();
+            assertThat(entry.getAttribute("cn").firstValueAsString()).isEqualTo("Horatio Jensen");
+        } finally {
+            file.delete();
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader entry containing a malformed URL.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testValueOfLDIFEntryReadEntryContainingMalformedURL() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(
+                "version: 1",
+                "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz",
+                "# dn:: ou=<JapaneseOU>,o=Airius",
+                "objectclass: top",
+                "objectclass: person",
+                "objectclass: organizationalPerson",
+                "cn: Horatio Jensen",
+                "cn: Horatio N Jensen",
+                "sn: Jensen",
+                "uid: hjensen",
+                "telephonenumber: +1 408 555 1212",
+                "jpegphoto:< invalidProtocol",
+                " ",
+                " ");
+        // @formatter:on
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry missing key value.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadEntryParseColonPositionThrowException() throws Exception {
+
+        // @formatter:off
+        final String path = TestCaseUtils.createTempFile(
+                "#Entry made for testing",
+                ": cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com",
+                "objectclass: top",
+                "objectclass: person",
+                "objectclass: organizationalPerson"
+        );
+        // @formatter:on
+
+        final FileInputStream in = new FileInputStream(path);
+        final LDIFEntryReader reader = new LDIFEntryReader(in);
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing base64 encoded attribute.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadEntryBase64EncodedMalformedBase64Attribute() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(Arrays.asList(
+            "version: 1",
+            "dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Gern Jensen",
+            "cn: Gern O Jensen",
+            "sn: Jensen",
+            "uid: gernj",
+            "telephonenumber: +1 408 555 1212",
+            "description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl"
+            + "IGlzIGJhc2UtNjQtZW5aaaaaaaaaaajb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG"
+            + "VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg"
+            + "b3V0IG1vcmUu"
+        ));
+        // @formatter:on
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing base64 encoded attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testReadEntryBase64Encoded() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(Arrays.asList(
+            "version: 1",
+            "dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Gern Jensen",
+            "cn: Gern O Jensen",
+            "sn: Jensen",
+            "uid: gernj",
+            "telephonenumber: +1 408 555 1212",
+            "description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl"
+            + "IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG"
+            + "VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg"
+            + "b3V0IG1vcmUu")
+        );
+        // @formatter:on
+
+        try {
+            assertThat(reader.hasNext());
+            final Entry entry = reader.readEntry();
+            assertThat(entry).isNotNull();
+            assertThat(entry.getAttributeCount()).isEqualTo(6);
+            // Verifying second occurrence of is not taken into account
+            assertThat(entry.getAttribute("cn").firstValueAsString()).isEqualTo("Gern Jensen");
+            // Verifying decoding is enabled on description attribute
+            assertThat(entry.getAttribute("description").firstValueAsString())
+                    .isNotSameAs(
+                            "V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl"
+                                    + "IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG"
+                                    + "VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg"
+                                    + "b3V0IG1vcmUu");
+            assertThat(entry.getAttribute("description").firstValueAsString()).contains(
+                    "What a careful reader you are!");
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing base64 encoded attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testReadEntryBase64EncodedDN() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(Arrays.asList(
+            "dn::  dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz", // adding space before ok, after : ko
+            "# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Gern Jensen",
+            "cn: Gern O Jensen",
+            "sn: Jensen",
+            "uid: gernj"
+        ));
+        // @formatter:on
+        try {
+            assertThat(reader.hasNext());
+            final Entry entry = reader.readEntry();
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(entry.getName().toString()).isEqualTo("uid=rogasawara,ou=営業部,o=Airius");
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test to read an entry containing base64 encoded DN. DN base64 encoded is
+     * malformed. Must throw an error.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testReadEntryBase64EncodedDNMalformedThrowsError() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(Arrays.asList(
+            "dn:: dWlkPXJvZ2FzYXdh!!!OOOpppps!!!25qWt6YOoLG89QWlyaXVz",
+            "# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius",
+            "objectclass: top",
+            "objectclass: person",
+            "objectclass: organizationalPerson",
+            "cn: Gern Jensen",
+            "cn: Gern O Jensen",
+            "sn: Jensen",
+            "uid: gernj"
+        ));
+        // @formatter:on
+
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Test LDIFEntryReader reading a LDIF entry via EntryAsArray.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testLDIFEntryReaderEntryAsArray() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader(Arrays.asList(getStandardEntry()));
+
+        try {
+            assertThat(reader.hasNext());
+            assertThat(reader.readEntry().getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader cause NullPointerException when InputStream is null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testLDIFEntryReaderInpuStreamDoesntAllowNull() throws Exception {
+        final LDIFEntryReader reader = new LDIFEntryReader((InputStream) null);
+        reader.close();
+    }
+
+    /**
+     * LDIFEntryReader read cause IOException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = IOException.class)
+    public void testReadEntryThrowsIOException() throws Exception {
+
+        final FileInputStream mockIn = mock(FileInputStream.class);
+        final LDIFEntryReader reader = new LDIFEntryReader(mockIn);
+
+        doThrow(new IOException()).when(mockIn).read();
+        try {
+            reader.readEntry();
+        } finally {
+            reader.close();
+            verify(mockIn, times(1)).close();
+        }
+    }
+
+    /**
+     * LDIFEntryReader ValueOfLDIFEntry - Multiple change records found.
+     * Exception LocalizedIllegalArgumentException expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFEntryMultipleChangeRecordFound() throws Exception {
+
+        // @formatter:off
+        LDIFEntryReader.valueOfLDIFEntry(
+            "#This is an example test",
+            "dn: CN=John Smith,OU=Legal,DC=example,DC=com",
+            "changetype: modify",
+            "replace:employeeID",
+            "employeeID: 1234",
+            "",
+            "dn: CN=Jane Smith,OU=Accounting,DC=example,DC=com",
+            "changetype: modify",
+            "replace:employeeID",
+            "employeeID: 5678"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * LDIFEntryReader ValueOfLDIFEntry throws exception when a single comment
+     * inserted.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFEntryThrowsExceptionIfOnlyAComment() throws Exception {
+        LDIFEntryReader.valueOfLDIFEntry("#This is an example test");
+    }
+
+    /**
+     * Test of valueOfLDIFEntry using malformed LDIF. Must return an
+     * LocalizedIllegalArgumentException In this case, dn is missing.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testValueOfLDIFEntryMalformedEntry() throws Exception {
+
+        // @formatter:off
+        LDIFEntryReader.valueOfLDIFEntry(
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson"
+        );
+        // @formatter:on
+    }
+
+    /**
+     * Test of valueOfLDIFEntry using well formed entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testValueOfLDIFEntryWellFormedEntry() throws Exception {
+        // @formatter:off
+        final Entry entry = LDIFEntryReader.valueOfLDIFEntry(
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson"
+        );
+        // @formatter:on
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(1);
+    }
+
+    /**
+     * Test LDIFEntryReader valueOfLDIFEntry on the standard entry and verify if
+     * all the attributes are well read.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testValueOfLDIFEntryReadStandardEntry() throws Exception {
+        final Entry entry = LDIFEntryReader.valueOfLDIFEntry(getStandardEntry());
+
+        assertThat(entry).isNotNull();
+        assertThat(entry.getName()).isNotNull();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("sn").firstValue().toString()).isEqualTo("Amar");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+    }
+
+    /**
+     * LDIFReader valueOfLDIFEntry doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testValueOfLDIFEntryDoesntAllowNull() throws Exception {
+        LDIFEntryReader.valueOfLDIFEntry((String[]) null);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryWriterTestCase.java
new file mode 100644
index 0000000..633e582
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFEntryWriterTestCase.java
@@ -0,0 +1,826 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldif;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Matcher;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+/**
+ * This class tests the LDIFEntryWriter functionality.
+ */
+public final class LDIFEntryWriterTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * Standard entry used for the following tests.
+     *
+     * @return an Entry with pre-defined attributes
+     */
+    private static Entry getStandardEntry() {
+        final Entry entry = new LinkedHashMapEntry("cn=John Doe,ou=people,dc=example,dc=com");
+        entry.addAttribute("objectClass", "top", "person", "inetOrgPerson");
+        entry.addAttribute("cn", "John Doe");
+        entry.addAttribute("sn", "Doe");
+        entry.addAttribute("age", "29");
+        entry.addAttribute("givenName", "John");
+        entry.addAttribute("description", "one two", "three four",
+                "This is a very very long description, Neque porro quisquam est qui dolorem ipsum"
+                + "quia dolor sit amet, consectetur, adipisci velit...");
+        entry.addAttribute("typeOnly");
+        entry.addAttribute("mail", "email@example.com");
+        entry.addAttribute("localized;lang-fr", "\u00e7edilla");
+        entry.addAttribute("entryUUID", "ad55a34a-763f-358f-93f9-da86f9ecd9e4");
+        entry.addAttribute("entryDN", "uid=bjensen,ou=people,dc=example,dc=com");
+        return entry;
+    }
+
+    /**
+     * Test setExcludeAttribute method of LDIFEntryWriter Throws a
+     * NullPointerException if the attributeDescription is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeAttributeDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAttribute(null);
+        writer.close();
+    }
+
+    /**
+     * Test to write an entry with attribute exclusions.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAttributeWithMatch() throws Exception {
+        final AttributeDescription attribute = AttributeDescription.valueOf("cn");
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAttribute(attribute);
+
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        assertThat(actual.size()).isGreaterThan(0);
+        for (String line : actual) {
+            // we have excluded this attribute especially
+            assertThat(line).doesNotContain("cn: John Doe");
+        }
+    }
+
+    /**
+     * Test to write an entry with attribute exclusions. In this test, the
+     * attribute description 'vip' doesn't exist then the entry must be written
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAttributeWithNoMatch() throws Exception {
+        final AttributeDescription attribute = AttributeDescription.valueOf("vip");
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAttribute(attribute);
+
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        assertThat(actual.size()).isGreaterThan(0);
+        for (String line : actual) {
+            // we have excluded this attribute especially
+            assertThat(line).doesNotContain("vip");
+        }
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryWriter Throws a
+     * NullPointerException if the excludeBranch is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetExcludeBranchDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeBranch(null);
+        writer.close();
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWrongDN() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        DN dn = DN.valueOf("dc=example.com");
+
+        writer.setExcludeBranch(dn);
+        writer.writeEntry(getStandardEntry());
+        writer.flush();
+        writer.close();
+        // Even if DN is wrong then entry is expected
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        DN dn = DN.valueOf("dc=example,dc=com");
+
+        writer.setExcludeBranch(dn);
+        writer.writeEntry(getStandardEntry());
+        writer.flush();
+        writer.close();
+        // No values expected - we have excluded the branch
+        Assert.assertFalse(actual.size() > 0);
+    }
+
+    /**
+     * Test SetExcludeBranch method of LDIFEntryWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeBranchWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        DN dn = DN.valueOf("dc=example,dc=org");
+
+        writer.setExcludeBranch(dn);
+        writer.writeEntry(getStandardEntry());
+        writer.flush();
+        writer.close();
+        // The entry must be written
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test SetExcludeFilter method of LDIFEntryWriter Throws a
+     * NullPointerException if the excludeFilter is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testsetExcludeFilterDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeFilter(null);
+        writer.close();
+    }
+
+    /**
+     * Test testSetExcludeFilter method of LDIFEntryWriter. StandardEntry has an
+     * objectclass : person
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeFilterWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        final Filter filter = Filter.equality("objectclass", "vip");
+        final Matcher excludeFilter = filter.matcher();
+
+        writer.setExcludeFilter(excludeFilter);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // objectclass is 'person' in the example, result must be > 0
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test testSetExcludeFilter method of LDIFEntryWriter StandardEntry has an
+     * objectclass : person
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeFilterWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        final Filter filter = Filter.equality("objectclass", "person");
+        final Matcher excludeFilter = filter.matcher();
+
+        writer.setExcludeFilter(excludeFilter);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // the entry correspond to the filter - must be excluded
+        assertThat(actual).isEmpty();
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFEntryWriter Throws a
+     * NullPointerException if the attributeDescription is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeAttributeDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeAttribute(null);
+        writer.close();
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFEntryWriter. Inserting attribute
+     * cn (common name) & sn (surname)
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeAttributeWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        writer.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.get(1)).contains("cn: ");
+        assertThat(actual.get(2)).contains("sn: ");
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFEntryWriter in this example, the
+     * field "manager" is not present in the StandardEntry. Then the entry must
+     * only write the first line : dn: cn=John Doe,ou=people,dc=example,dc=com
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeAttributeWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeAttribute(AttributeDescription.valueOf("manager"));
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // 1st line is containing DN
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        // empty second
+        assertThat(actual.get(1)).isEmpty();
+        // verifying no more than 2 lines written
+        assertThat(actual.size()).isLessThanOrEqualTo(2);
+    }
+
+    /**
+     * Test SetIncludeAttribute method of LDIFEntryWriter. Attempted insertions
+     * repeating attributes. An attribute mustn't be written twice or +.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeAttributeWithRepeatedAttributes() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        writer.setIncludeAttribute(AttributeDescription.valueOf("sn"));
+        writer.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        writer.setIncludeAttribute(AttributeDescription.valueOf("cn"));
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.get(1)).contains("cn: ");
+        assertThat(actual.get(2)).contains("sn: ");
+        // 3 lines of result + 1 empty line
+        assertThat(actual.size()).isLessThanOrEqualTo(4);
+    }
+
+    /**
+     * Test to write an entry excluding all operational attributes
+     * setExcludeAllOperationalAttributes to false (default case)
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesFalse() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        int opAttributes = 0;
+
+        writer.setExcludeAllOperationalAttributes(false);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        for (String line : actual) {
+            if (line.contains("entryUUID") || line.contains("entryDN")) {
+                opAttributes++;
+            }
+        }
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+
+        assertThat(opAttributes).isEqualTo(2);
+
+    }
+
+    /**
+     * Test to write an entry excluding all operational attributes
+     * setExcludeAllOperationalAttributes is forced to true Result should be dn:
+     * cn=John Doe,ou=people,dc=example,dc=com plus an empty line.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllOperationalAttributesTrue() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAllOperationalAttributes(true);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        for (String line : actual) {
+            assertThat(line).doesNotContain("entryUUID");
+            assertThat(line).doesNotContain("entryDN");
+        }
+
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test to write an entry excluding user attributes Default case - full
+     * entry must be written.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesFalse() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAllUserAttributes(false);
+        writer.writeEntry(getStandardEntry());
+        writer.flush();
+        writer.close();
+
+        assertThat(actual.get(0)).isEqualTo("dn: " + getStandardEntry().getName());
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test to write an entry excluding user attributes result should be dn:
+     * cn=John Doe,ou=people,dc=example,dc=com plus an empty line.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetExcludeAllUserAttributesTrue() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        writer.setExcludeAllUserAttributes(true);
+        writer.writeEntry(getStandardEntry());
+        writer.flush();
+        writer.close();
+
+        for (String line : actual) {
+            assertThat(line).doesNotContain("sn");
+            assertThat(line).doesNotContain("mail");
+        }
+        assertThat(actual.get(0).contains("dn: cn=John Doe,ou=people,dc=example,dc=com"));
+        assertThat(actual.size()).isLessThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryWriter Throws a
+     * NullPointerException if the includeBranch is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeBranchDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeBranch(null);
+        writer.close();
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryWriter verifying right data are
+     * present.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeBranchWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        DN dn = DN.valueOf("dc=example,dc=com");
+        writer.setIncludeBranch(dn);
+
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // Must contains all the attributes
+        assertThat(actual.get(0)).contains(getStandardEntry().getName().toString());
+        assertThat(actual.size()).isGreaterThan(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test SetIncludeBranch method of LDIFEntryWriter DN included is
+     * "dc=opendj,dc=org", which is not the one from the standard entry Entry
+     * must not be written.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeBranchWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        DN dn = DN.valueOf("dc=opendj,dc=org");
+        writer.setIncludeBranch(dn);
+
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // No result expected
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Test SetIncludeFilter method of LDIFEntryWriter. This example use
+     * Filter.equality("objectclass", "vip"); which is not the one from the
+     * standard entry.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeFilterWithNoMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        final Filter filter = Filter.equality("objectclass", "vip");
+        final Matcher includeFilter = filter.matcher();
+
+        writer.setIncludeFilter(includeFilter);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        assertThat(actual.size()).isEqualTo(0);
+    }
+
+    /**
+     * Test SetIncludeFilter method of LDIFEntryWriter. This example use
+     * Filter.equality("objectclass", "person"); which is the one from the
+     * standard entry.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testSetIncludeFilterWithMatch() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        final Filter filter = Filter.equality("objectclass", "person");
+        final Matcher includeFilter = filter.matcher();
+
+        writer.setIncludeFilter(includeFilter);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        // Entry must be written
+        assertThat(actual).isNotNull();
+        assertThat(actual.size()).isGreaterThanOrEqualTo(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Test SetIncludeFilter method of LDIFEntryWriter Throws a
+     * NullPointerException if the schema is null.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testSetIncludeFilterDoesntAllowNull() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.setIncludeFilter(null);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+    }
+
+    /**
+     * Test WriteComment method of LDIFEntryWriter using the wrap function.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunction() throws Exception {
+        final CharSequence comment = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        int wrapColumn = 15;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // The line length <= writer.wrapColumn
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+        }
+    }
+
+    /**
+     * Test WriteComment method of LDIFEntryWriter using the wrap function. set
+     * wrap to 0.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunctionShortComment() throws Exception {
+        final CharSequence comment = "Lorem ipsum dolor";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        int wrapColumn = 30;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+        }
+    }
+
+    /**
+     * Test WriteComment method of LDIFEntryWriter using the wrap function. The
+     * comment doesn't contain any empty spaces.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteCommentUsingTheWrapFunctionNoEmptySpace() throws Exception {
+        final CharSequence comment = "Lorem ipsumdolorsitamet,consecteturadipisicingelit";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        int wrapColumn = 15;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeComment(comment);
+        writer.close();
+
+        for (String line : actual) {
+            // The line length <= writer.wrapColumn
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+            // Each line started with #
+            assertThat(line.startsWith("#")).isTrue();
+        }
+    }
+
+    /**
+     * Test WriteComment method of LDIFEntryWriter.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteComment() throws Exception {
+        final CharSequence comment1 = "This is a new comment";
+        final CharSequence comment2 = "Another one";
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.writeComment(comment1);
+        writer.writeComment(comment2);
+        writer.close();
+
+        // Verifying comments are well written in the LDIF Entry
+        Assert.assertEquals(actual.size(), 2);
+        assertThat(actual.get(0)).isEqualTo("# " + comment1);
+        assertThat(actual.get(1)).isEqualTo("# " + comment2);
+    }
+
+    /**
+     * Test to write an entry adding the user friendly Comment.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(enabled = false)
+    public void testSetAddUserFriendlyComments() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        final CharSequence comment = "A simple comment";
+
+        writer.setAddUserFriendlyComments(true);
+        writer.writeComment0(comment);
+        writer.close();
+    }
+
+    /**
+     * Tests writeEntry method of LDIFEntryWriter class. Using the
+     * getStandardEntry. Attribute description tested, containing a long text.
+     * Wrap need to be used is this case.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteEntryUsingStandardEntry() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        final int wrapColumn = 15;
+        writer.setWrapColumn(wrapColumn);
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        int countDesc = 0;
+
+        // Entry must be written - check wrap on description
+        assertThat(actual).isNotNull();
+        for (String line : actual) {
+            if (line.contains("description")) {
+                countDesc++;
+            }
+            assertThat(line.length()).isLessThanOrEqualTo(wrapColumn);
+        }
+        assertThat(countDesc).isEqualTo(getStandardEntry().getAttribute("description").size());
+        assertThat(actual.size()).isGreaterThanOrEqualTo(getStandardEntry().getAttributeCount());
+    }
+
+    /**
+     * Tests writeEntry method of LDIFEntryWriter class.See
+     * https://opends.dev.java.net/issues/show_bug.cgi?id=4545 for more details.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteEntry() throws Exception {
+        final Entry entry = new LinkedHashMapEntry("cn=John Doe,ou=people,dc=example,dc=com");
+        entry.addAttribute("objectClass", "top", "person", "inetOrgPerson");
+        entry.addAttribute("cn", "John Doe");
+        entry.addAttribute("sn", "Doe");
+        entry.addAttribute("givenName", "John");
+        entry.addAttribute("description", "one two", "three four", "five six");
+        entry.addAttribute("typeOnly");
+        entry.addAttribute("localized;lang-fr", "\u00e7edilla");
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+        writer.writeEntry(entry);
+        writer.close();
+
+        final String[] expected =
+                new String[] { "dn: cn=John Doe,ou=people,dc=example,dc=com", "objectClass: top",
+                    "objectClass: person", "objectClass: inetOrgPerson", "cn: John Doe", "sn: Doe",
+                    "givenName: John", "description: one two", "description: three four",
+                    "description: five six", "typeOnly: ", "localized;lang-fr:: w6dlZGlsbGE=", "", };
+
+        Assert.assertEquals(actual.size(), expected.length);
+        for (int i = 0; i < expected.length; i++) {
+            Assert.assertEquals(actual.get(i), expected[i], "LDIF output was " + actual);
+        }
+    }
+
+    /**
+     * Testing the WriteEntry function using the mock for testing more
+     * IOExceptions and verify if they are correctly handled.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(expectedExceptions = IOException.class)
+    public void testWriteEntryUsingMockOutputThrowsIOException() throws Exception {
+
+        OutputStream mockOutput = mock(OutputStream.class);
+        doThrow(new IOException()).when(mockOutput).write(any(byte[].class));
+        doThrow(new IOException()).when(mockOutput).write(any(byte[].class), anyInt(), anyInt());
+
+        LDIFEntryWriter writer = new LDIFEntryWriter(mockOutput);
+        try {
+            CharSequence comment = "This is a new comment";
+            writer.writeComment(comment);
+        } finally {
+            writer.close();
+        }
+    }
+
+    /**
+     * Verify flush/close are also forwarded to the stream.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteEntryUsingMockOutputForFlushAndClose() throws Exception {
+        OutputStream mockOutput = mock(OutputStream.class);
+        LDIFEntryWriter writer = new LDIFEntryWriter(mockOutput);
+        try {
+            writer.flush();
+            writer.flush();
+            verify(mockOutput, times(2)).flush();
+        } finally {
+            writer.close();
+            verify(mockOutput, times(1)).close();
+        }
+    }
+
+    /**
+     * Test the WriteEntry using an output file verifying write is correctly
+     * invoked.
+     *
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test
+    public void testWriteEntryOutputStreamUsingMock() throws Exception {
+        final OutputStream out = mock(OutputStream.class);
+        final LDIFEntryWriter writer = new LDIFEntryWriter(out);
+
+        writer.writeEntry(getStandardEntry());
+        writer.close();
+
+        verify(out, times(1)).write(any(byte[].class), anyInt(), anyInt());
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java
new file mode 100644
index 0000000..b8d8a94
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFTestCase.java
@@ -0,0 +1,2823 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.ldap.schema.Syntax;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.ldap.CoreMessages;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
+import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+
+/**
+ * This class tests the LDIF functionality.
+ */
+@SuppressWarnings("javadoc")
+public class LDIFTestCase extends AbstractLDIFTestCase {
+
+    /**
+     * Provide a standard entry for the tests below.
+     *
+     * @return well formed LDIF entry
+     */
+    public final String[] getStandardEntry() {
+        // @formatter:off
+        return new String[] {
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "postalAddress: Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
+            "postalCode: 50369",
+            "uid: user.0",
+            "description: This is the description for Aaccf Amar.",
+            "userPassword: {SSHA}hpbT8dLi8xgYy2kl4aP6QKGzsFdhESWpPmDTEw==",
+            "employeeNumber: 0",
+            "initials: ASA",
+            "givenName: Aaccf",
+            "pager: +1 779 041 6341",
+            "mobile: +1 010 154 3228",
+            "cn: Aaccf Amar",
+            "telephoneNumber: +1 685 622 6202",
+            "sn: Amar",
+            "street: 01251 Chestnut Street",
+            "homePhone: +1 225 216 5900",
+            "mail: user.0@maildomain.net",
+            "l: Panama City", "st: DE",
+            "pwdChangedTime: 20120903142126.219Z",
+            "entryDN: uid=user.0,ou=people,dc=example,dc=org",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        };
+        // @formatter:on
+    }
+
+    /**
+     * Number of attributes of the standard entry.
+     */
+    public final int nbStandardEntryAttributes = new LinkedHashMapEntry(getStandardEntry())
+            .getAttributeCount();
+
+    /**
+     * Testing LDIF Search with match.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithMatch() throws Exception {
+        final EntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)");
+
+        final EntryReader resultReader = LDIF.search(reader, sr);
+        final Entry entry = resultReader.readEntry();
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(nbStandardEntryAttributes);
+
+        reader.close();
+        resultReader.close();
+    }
+
+    /**
+     * Testing LDIF Search with no match.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithNoMatch() throws Exception {
+        final EntryReader reader = new LDIFEntryReader(getStandardEntry());
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=org", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)");
+        final EntryReader resultReader = LDIF.search(reader, sr);
+        // No result found in the reader.
+        assertThat(resultReader.hasNext()).isFalse();
+        resultReader.close();
+    }
+
+    /**
+     * LDIF Search with null parameters throw a NullPointerException.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifSearchDoesntAllowNull() throws Exception {
+
+        LDIF.search(null, null);
+    }
+
+    /**
+     * LDIF Search doesn't allow a null search request.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifSearchDoesntAllowNullSearchRequest() throws Exception {
+
+        final EntryReader reader = new LDIFEntryReader(getStandardEntry());
+        try {
+            LDIF.search(reader, null);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIF Search with null reader is allowed but throws a null pointer
+     * exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifSearchAllowsNullReader() throws Exception {
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)");
+
+        final EntryReader resultReader = LDIF.search(null, sr);
+        resultReader.readEntry();
+        resultReader.close();
+    }
+
+    /**
+     * Tests the search request using a schema and no specifying attribute
+     * description.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchNoAttributeDescription() throws Exception {
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "uid=*");
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(entry.getAttribute("uid")).isNotNull();
+        assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
+        assertThat(entry.getAttribute("uid").firstValueAsString()).isNotNull();
+
+        resultReader.close();
+    }
+
+    /**
+     * Tests the search request using a schema and no specifying attribute
+     * description. TypesOnly = true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchNoAttributeDescriptionTypeOnly()
+            throws Exception {
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "uid=*")
+                        .setTypesOnly(true);
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(entry.getAttribute("uid")).isNotNull();
+        assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
+        assertThat(entry.getAttribute("uid")).isEmpty();
+
+        resultReader.close();
+    }
+
+    /**
+     * LDIF search with schema use : all attributes.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchFullAttributes() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "*");
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(entry.getAttribute("uid")).isNotNull();
+        assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
+        assertThat(entry.getAttribute("uid").firstValueAsString()).isEqualTo("user.0");
+
+        resultReader.close();
+    }
+
+    /**
+     * LDIF Search with schema use : all attributes. Types only.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public final void testLdifSearchWithSchemaMatchFullAttributesTypeOnly() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "*").setTypesOnly(true);
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(entry.getAttribute("objectClass")).isNotNull();
+        assertThat(entry.getAttribute("uid")).isNotNull();
+        assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
+        assertThat(entry.getAttribute("uid")).isEmpty();
+        // The following assert throws an exception because it contains only
+        // attribute descriptions (and not values) are to be returned.
+        assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
+
+        resultReader.close();
+    }
+
+    /**
+     * Testing the new search request with the + operator == operational
+     * attributes only.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchOnlyOperationalAttributes() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0",
+            "entryDN: uid=user.0,ou=People,dc=example,dc=com",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "+");
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getCoreSchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("entryDN").firstValueAsString()).isEqualTo(
+                "uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("entryUUID")).isNotEmpty();
+        assertThat(entry.getAttribute("modifyTimestamp")).isNotEmpty();
+        assertThat(entry.getAttribute("modifiersName")).isNotEmpty();
+
+        resultReader.close();
+    }
+
+    /**
+     * Testing the new search request with the + operator == operational
+     * attributes only. Combined here with the TypeOnly = true.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchOnlyOperationalAttributesTypeOnly()
+            throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0",
+            "entryDN: uid=user.0,ou=People,dc=example,dc=com",
+            "entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4",
+            "modifyTimestamp: 20120903142126Z",
+            "modifiersName: cn=Internal Client,cn=Root DNs,cn=config"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "+").setTypesOnly(true);
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getCoreSchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("entryDN")).isEmpty();
+        assertThat(entry.getAttribute("entryUUID")).isEmpty();
+        assertThat(entry.getAttribute("modifyTimestamp")).isEmpty();
+        assertThat(entry.getAttribute("modifiersName")).isEmpty();
+
+        resultReader.close();
+    }
+
+    /**
+     * LDIF search with schema use filter on uid attribute.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaMatchSpecifiedAttribute() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "uid");
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(1);
+
+        resultReader.close();
+    }
+
+    /**
+     * LDIF search with schema use filter on uid attribute.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public final void testLdifSearchWithSchemaMatchSpecifiedAttributeTypeOnly() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "uid").setTypesOnly(true);
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(1);
+        assertThat(entry.getAttribute("uid").getAttributeDescription()).isNotNull();
+        assertThat(entry.getAttribute("uid")).isEmpty();
+        // The following assert throws an exception because no values contained in, only type.
+        assertThat(entry.getAttribute("uid").firstValueAsString()).isNull();
+        resultReader.close();
+    }
+
+    /**
+     * LDIF search with schema use with filter not contained in this entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifSearchWithSchemaNoMatchSpecifiedAttribute() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "email");
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(0);
+
+        resultReader.close();
+    }
+
+    /**
+     * The attribute description contains an internal white space : must throw
+     * an exception.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public final void testLdifSearchWithSchemaThrowsException() throws Exception {
+
+        // @formatter:off
+        final EntryReader reader = new LDIFEntryReader(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson",
+            "objectClass: organizationalperson",
+            "objectClass: top",
+            "uid: user.0"
+        );
+        // @formatter:on
+
+        final SearchRequest sr =
+                Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=user.0)", "wro ng"); // wrong syntax filter
+
+        final EntryReader resultReader = LDIF.search(reader, sr, Schema.getEmptySchema());
+        resultReader.readEntry();
+        resultReader.close();
+    }
+
+    /**
+     * Tries to search for an entry in ldif using ignore case exact matching rule and lower case search filter.
+     *
+     * @throws IOException
+     */
+    @Test
+    public final void testSearchForEntryInLDIFUsingIgnoreMatchingRuleSucceedWithLowerCaseFilter() throws IOException {
+        Schema schema = newSchemaBuilder(getCaseIgnoreMatchingRule(), getDirectoryStringSyntax());
+
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+                "dn: cn=JPEG,cn=Syntaxes,cn=config",
+                "objectClass: top",
+                "objectClass: ds-cfg-attribute-syntax",
+                "cn: JPEG",
+                "ds-cfg-java-class: org.opends.server.schema.JPEGSyntax",
+                "ds-cfg-enabled: true"
+        ).setSchema(schema);
+        // @formatter:on
+
+        final SearchRequest sr = Requests.newSearchRequest("cn=config", SearchScope.WHOLE_SUBTREE,
+                "(ds-cfg-java-class=org.opends.server.schema.jpegsyntax)", "*");
+        final EntryReader resultReader = LDIF.search(input, sr, schema);
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("cn=JPEG,cn=Syntaxes,cn=config");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("ds-cfg-java-class").firstValueAsString()).isEqualTo(
+                "org.opends.server.schema.JPEGSyntax");
+        input.close();
+    }
+
+    /**
+     * Tries to search for an entry in ldif using case exact matching rule and lower case search filter.
+     *
+     * @throws IOException
+     */
+    @Test(expectedExceptions = NoSuchElementException.class)
+    public final void testSearchForEntryInLDIFUsingExactMatchingRuleFailsWithLowerCaseFilter() throws IOException {
+        Schema schema = newSchemaBuilder(getCaseExactMatchingRule(), getDirectoryStringSyntax());
+
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+                "dn: cn=JPEG,cn=Syntaxes,cn=config",
+                "objectClass: top",
+                "objectClass: ds-cfg-attribute-syntax",
+                "cn: JPEG",
+                "ds-cfg-java-class: org.opends.server.schema.JPEGSyntax",
+                "ds-cfg-enabled: true"
+        ).setSchema(schema);
+        // @formatter:on
+
+        final SearchRequest sr = Requests.newSearchRequest("cn=config", SearchScope.WHOLE_SUBTREE,
+                "(ds-cfg-java-class=org.opends.server.schema.jpegsyntax)", "*");
+        final EntryReader resultReader = LDIF.search(input, sr, schema);
+        resultReader.readEntry();
+    }
+
+    /**
+     * Tries to search for an entry in ldif using case exact matching rule and the right search filter.
+     *
+     * @throws IOException
+     */
+    @Test
+    public final void testSearchForEntryInLDIFUsingExactMatchingRuleSucceedWithRightFilter() throws IOException {
+        Schema schema = newSchemaBuilder(getCaseExactMatchingRule(), getDirectoryStringSyntax());
+
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+                "dn: cn=JPEG,cn=Syntaxes,cn=config",
+                "objectClass: top",
+                "objectClass: ds-cfg-attribute-syntax",
+                "cn: JPEG",
+                "ds-cfg-java-class: org.opends.server.schema.JPEGSyntax",
+                "ds-cfg-enabled: true"
+        ).setSchema(schema);
+        // @formatter:on
+
+        final SearchRequest sr = Requests.newSearchRequest("cn=config", SearchScope.WHOLE_SUBTREE,
+                "(ds-cfg-java-class=org.opends.server.schema.JPEGSyntax)", "*");
+        final EntryReader resultReader = LDIF.search(input, sr, schema);
+        final Entry entry = resultReader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("cn=JPEG,cn=Syntaxes,cn=config");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("ds-cfg-java-class").firstValueAsString()).isEqualTo(
+                "org.opends.server.schema.JPEGSyntax");
+        input.close();
+    }
+
+    private Schema newSchemaBuilder(MatchingRule defaultMatchingRule, Syntax defaultSyntax) {
+        return new SchemaBuilder(Schema.getCoreSchema())
+            .setOption(DEFAULT_MATCHING_RULE_OID, defaultMatchingRule.getOID())
+            .setOption(DEFAULT_SYNTAX_OID, defaultSyntax.getOID())
+            .toSchema().asNonStrictSchema();
+    }
+
+    /**
+     * Verifying LDIF collection reader.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifNewEntryCollectionReader() throws Exception {
+
+        // @formatter:off
+        Entry e = new LinkedHashMapEntry(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson"
+        );
+        Entry e1 = new LinkedHashMapEntry("dn: uid=user.1,ou=People,dc=example,dc=com", "objectClass: person");
+        Entry e2 = new LinkedHashMapEntry("dn: uid=user.2,ou=People,dc=example,dc=com", "objectClass: person");
+        // @formatter:on
+
+        Collection<Entry> collection = new ArrayList<>();
+        collection.add(e);
+        collection.add(e1);
+        collection.add(e2);
+
+        final EntryReader resultReader = LDIF.newEntryCollectionReader(collection);
+        Entry entry = null;
+        int cCount = 0;
+        while (resultReader.hasNext()) {
+            entry = resultReader.readEntry();
+            assertThat(entry.getName().toString()).isNotNull();
+            assertThat(entry.getAttributeCount()).isGreaterThanOrEqualTo(1);
+            cCount++;
+        }
+        assertThat(cCount).isEqualTo(3);
+        resultReader.close();
+    }
+
+    /**
+     * The LDIF entry collection reader allows a null parameter.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifNewEntryCollectionDoesntAllowNull() throws Exception {
+        try (EntryReader resultReader = LDIF.newEntryCollectionReader(null)) {
+            resultReader.readEntry();
+        }
+    }
+
+    /**
+     * Tests the LDIF entry iterator reader.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifNewEntryIteratorReader() throws Exception {
+
+        // @formatter:off
+        final Entry e = new LinkedHashMapEntry(
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "objectClass: inetorgperson"
+        );
+        final Entry e1 = new LinkedHashMapEntry(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: person"
+        );
+        // @formatter:on
+
+        final SortedMap<DN, Entry> sourceEntries = new TreeMap<>();
+        sourceEntries.put(DN.valueOf("uid=user.0,ou=People,dc=example,dc=com"), e);
+        sourceEntries.put(DN.valueOf("uid=user.1,ou=People,dc=example,dc=com"), e1);
+        final Iterator<Entry> sourceIterator = sourceEntries.values().iterator();
+
+        final EntryReader resultReader = LDIF.newEntryIteratorReader(sourceIterator);
+        int cCount = 0;
+        while (resultReader.hasNext()) {
+            final Entry entry = resultReader.readEntry();
+            assertThat(entry.getName().toString()).isNotNull();
+            assertThat(entry.getName().toString()).contains("ou=People,dc=example,dc=com");
+            assertThat(entry.getAttributeCount()).isGreaterThanOrEqualTo(1);
+            cCount++;
+        }
+        assertThat(cCount).isEqualTo(2);
+        resultReader.close();
+    }
+
+    /**
+     * LDIF entry iterator reader doesn't allow null.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifNewEntryIteratorReaderDoesntAllowsNull() throws Exception {
+        try (EntryReader resultReader = LDIF.newEntryIteratorReader(null)) {
+            resultReader.readEntry();
+        }
+    }
+
+    /**
+     * Tests the LDIF copy over a change record writer from an LDIF change record reader.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifCopyToChangeRecord() throws Exception {
+
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "# Entry to delete",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+        final java.util.List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        try {
+            LDIF.copyTo(reader, writer);
+            // Comment is skipped.
+            assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(actual.get(1)).isEqualTo("changetype: delete");
+            assertThat(actual.get(2)).isEqualTo("");
+            assertThat(actual.size()).isEqualTo(3); // 2 lines + 1 empty
+        } finally {
+            reader.close();
+            writer.close();
+        }
+    }
+
+    /**
+     * The LDIF copy doesn't allow null parameters.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifCopyToChangeRecordDoesntAllowNull() throws Exception {
+
+        LDIF.copyTo((ChangeRecordReader) null, (ChangeRecordWriter) null);
+    }
+
+    /**
+     * LDIF copy doesn't allow a null writer.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifCopyToChangeRecordDoesntAllowNullWriter() throws Exception {
+
+        // @formatter:off
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+            "# Entry to delete",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+        LDIF.copyTo(reader, null);
+    }
+
+    /**
+     * LDIF copy doesn't allow a null reader.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifCopyToChangeRecordDoesntAllowNullReader() throws Exception {
+        final java.util.List<String> actual = new ArrayList<>();
+        final LDIFChangeRecordWriter writer = new LDIFChangeRecordWriter(actual);
+
+        LDIF.copyTo(null, writer);
+    }
+
+    /**
+     * Tests the LDIF copy from an entry reader to a entry writer.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifCopyToEntryWriter() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(
+            "# Entry to delete",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        try {
+            LDIF.copyTo(reader, writer);
+            // Comment is skipped.
+            assertThat(actual.get(0)).isEqualTo("dn: uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(actual.get(1)).isEqualTo("changetype: delete");
+            assertThat(actual.get(2)).isEqualTo("");
+            assertThat(actual.size()).isEqualTo(3); // 2 lines + 1 empty
+        } finally {
+            reader.close();
+            writer.close();
+        }
+    }
+
+    /**
+     * LDIF copyTo - EntryWriter doesn't allow a null writer.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifCopyToEntryWriterDoesntAllowNullWriter() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader reader = new LDIFEntryReader(
+            "# Entry to delete",
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+        LDIF.copyTo(reader, null);
+    }
+
+    /**
+     * LDIF copyTo - EntryWriter doesn't allow a null reader.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifCopyToEntryWriterDoesntAllowNullReader() throws Exception {
+        final List<String> actual = new ArrayList<>();
+        final LDIFEntryWriter writer = new LDIFEntryWriter(actual);
+
+        LDIF.copyTo(null, writer);
+    }
+
+    /**
+     * Testing the diff function. The following example is extracted from the admin guide.
+     *
+     * @see <a
+     *      href=http://opendj.forgerock.org/doc/admin-guide/index.html#ldif-diff
+     *      -1 result”>Admin Guide</a>
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesModsOnSameDN() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+            "dn: uid=newuser,ou=People,dc=example,dc=com",
+            "uid: newuser",
+            "objectClass: person",
+            "objectClass: organizationalPerson",
+            "objectClass: inetOrgPerson",
+            "objectClass: top",
+            "cn: New User",
+            "sn: User",
+            "ou: People",
+            "mail: newuser@example.com",
+            "userPassword: changeme" // diff here
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+            "dn: uid=newuser,ou=People,dc=example,dc=com",
+            "uid: newuser",
+            "objectClass: person",
+            "objectClass: organizationalPerson",
+            "objectClass: inetOrgPerson",
+            "objectClass: top",
+            "cn: New User",
+            "sn: User",
+            "ou: People",
+            "mail: newuser@example.com",
+            "userPassword: secret12", // diff here
+            "description: A new description." // diff here
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+        final ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=newuser,ou=People,dc=example,dc=com");
+        assertThat(cr).isInstanceOf(ModifyRequest.class);
+
+        // @formatter:off
+        /* Expected : 2 add / 1 delete - output :
+         * dn: uid=newuser,ou=People,dc=example,dc=com
+         * changetype: modify
+         * add: userPassword
+         * userPassword: secret12
+         * -
+         * delete: userPassword
+         * userPassword: changeme
+         * -
+         * add: description
+         * description: A new description.
+         */
+         // @formatter:on
+
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(3);
+        for (Modification mod : ((ModifyRequest) cr).getModifications()) {
+            final String attrDescription = mod.getAttribute().getAttributeDescription().toString();
+            final String firstValueAsString = mod.getAttribute().firstValueAsString();
+            if (mod.getModificationType() == ModificationType.ADD) {
+                assertThat(attrDescription).isIn("description", "userPassword");
+                assertThat(firstValueAsString).isIn("A new description.", "secret12");
+            } else if (mod.getModificationType() == ModificationType.DELETE) {
+                assertThat(attrDescription).isEqualTo("userPassword");
+                assertThat(firstValueAsString).isEqualTo("changeme");
+            }
+        }
+        reader.close();
+        target.close();
+    }
+
+    /**
+     * Testing the diff : the entry is present in source but not in the target.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesEntryInSourceNotInTarget() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+            "dn: uid=newuser,ou=People,dc=example,dc=com",
+            "cn: New User"
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+
+        // @formatter:off
+        /* output is :
+          dn: uid=newuser,ou=People,dc=example,dc=com
+          changetype: delete
+
+          dn: uid=scarter,ou=People,dc=example,dc=com
+          changetype: add
+          cn: Samantha Carter
+         */
+        // @formatter:on
+
+        ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=newuser,ou=People,dc=example,dc=com");
+        assertThat(cr).isInstanceOf(DeleteRequest.class);
+
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(cr).isInstanceOf(AddRequest.class);
+        assertThat(((AddRequest) cr).getAttributeCount()).isEqualTo(1);
+        assertThat(((AddRequest) cr).getAttribute("cn").firstValueAsString()).isEqualTo(
+                "Samantha Carter");
+        reader.close();
+        target.close();
+    }
+
+    /**
+     * Testing the diff : entry is in the target not in the source. The rdn of the
+     * following example is completed by ou=People.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesEntryInTargetNotInSource() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+            "dn: uid=scarter,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+
+        // @formatter:off
+        /* output is :
+        dn: uid=scarter,ou=People,dc=example,dc=com
+        changetype: add
+        cn: Samantha Carter
+
+        dn: uid=scarter,dc=example,dc=com
+        changetype: delete
+         */
+        // @formatter:on
+
+        ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(cr).isInstanceOf(AddRequest.class);
+        assertThat(((AddRequest) cr).getAttributeCount()).isEqualTo(1);
+        assertThat(((AddRequest) cr).getAttribute("cn").firstValueAsString()).isEqualTo(
+                "Samantha Carter");
+
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,dc=example,dc=com");
+        assertThat(cr).isInstanceOf(DeleteRequest.class);
+
+        reader.close();
+        source.close();
+        target.close();
+    }
+
+    /**
+     * Differences between two same entries : no modifications expected.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesNoDiff() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+        assertThat(reader.hasNext()).isTrue();
+        final ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications()).isEmpty();
+        reader.close();
+        target.close();
+    }
+
+    /**
+     * Differences between two short LDIF examples.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesShortExamples() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaccf",
+                "sn: Amar",
+                "cn: Aaccf Amar",
+                "initials: APA",
+                "employeeNumber: 0",
+                "uid: user.0",
+                "mail: user.0@example.com",
+                "description: This is the description for Aaccf Amar.",
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaren",
+                "sn: Atp",
+                "cn: Aaren Atp",
+                "initials: AFA",
+                "employeeNumber: 1",
+                "uid: user.1",
+                "mail: user.1@example.com",
+                "description: This is the description for Aaren Atp.",
+                "",
+                "dn: uid=user.2,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aarika",
+                "sn: Atpco",
+                "cn: Aarika Atpco",
+                "initials: AVA",
+                "employeeNumber: 2",
+                "uid: user.2",
+                "mail: user.2@example.com",
+                "description: This is the description for Aarika Atpco.",
+                "",
+                "dn: uid=user.3,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaron",
+                "sn: Atrc",
+                "cn: Aaron Atrc",
+                "initials: ATA",
+                "employeeNumber: 3",
+                "uid: user.3",
+                "mail: user.3@example.com",
+                "description: This is the description for Aaron Atrc.",
+                "",
+                "dn: uid=user.4,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aartjan",
+                "sn: Aalders",
+                "cn: Aartjan Aalders",
+                "initials: AAA",
+                "employeeNumber: 4",
+                "uid: user.4",
+                "mail: user.4@example.com",
+                "description: This is the description for Aartjan Aalders."
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Amar", // diff
+                "sn: Amar",
+                "cn: Aaccf Amar",
+                "initials: APA",
+                "employeeNumber: 55", // diff
+                "uid: user.0",
+                "mail: user.0@example.com",
+                "description: This is the description for Aaccf Amar.",
+                "work-phone: 650/506-0666", // diff
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaren",
+                "sn: Atp",
+                "cn: Aaren Atp",
+                "initials: AFA",
+                "employeeNumber: 1",
+                "uid: user.1",
+                "mail: aaren@example.com", // diff
+                "description: This is the description for Aaren Atp.",
+                "",
+                "dn: uid=user.2,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aarika",
+                "sn: Atpco",
+                "cn: Aarika Atpco",
+                "initials: AVA",
+                "employeeNumber: 2",
+                "uid: user.2",
+                "mail: user.2@example.com", // diff (delete description)
+                "",
+                "dn: uid=user.3,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aaron",
+                "sn: Atrc",
+                "cn: Aaron Atrc",
+                "initials: ATA",
+                "employeeNumber: 3",
+                "uid: user.999", // diff
+                "mail: user.999@example.com", // diff
+                "description: This is the description for Aaron Atrc.",
+                "",
+                "dn: uid=user.4,ou=People,dc=example,dc=com",
+                "objectClass: top",
+                "objectClass: person",
+                "objectClass: organizationalperson",
+                "objectClass: inetorgperson",
+                "givenName: Aartjan",
+                "sn: Aalders",
+                "cn: Aartjan Aalders",
+                "initials: AAA",
+                "employeeNumber: 4",
+                "uid: user.4",
+                "mail: user.4@example.com",
+                "description: This is the description for Aartjan Aalders."
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+        assertThat(reader.hasNext()).isTrue();
+        ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications()).isNotEmpty();
+        // 1st entry : 2 add/delete + 1 add(work-phone)
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(5);
+        // 2nd entry : 1 add/delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(2);
+        // 3rd entry : 1 delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(1);
+        assertThat(((ModifyRequest) cr).getModifications().get(0).getModificationType().toString())
+                .isEqualTo("delete");
+        assertThat(
+                ((ModifyRequest) cr).getModifications().get(0).getAttribute()
+                        .getAttributeDescriptionAsString()).isEqualTo("description");
+        // 4th entry : 2 add/delete
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(4);
+        // 5th entry : 0 modifications
+        cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications().size()).isEqualTo(0);
+        assertThat(reader.hasNext()).isFalse();
+
+        reader.close();
+        target.close();
+    }
+
+    /**
+     * Differences between two same entries : no modifications expected.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifDiffEntriesNoDiffBase64() throws Exception {
+
+        // @formatter:off
+        final LDIFEntryReader source = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "cn: Samantha Carter"
+        );
+
+        final LDIFEntryReader target = new LDIFEntryReader(
+            "dn:: dWlkPXNjYXJ0ZXIsb3U9UGVvcGxlLGRjPWV4YW1wbGUsZGM9Y29t",
+            "cn: Samantha Carter"
+        );
+        // @formatter:on
+
+        final ChangeRecordReader reader = LDIF.diff(source, target);
+        assertThat(reader.hasNext()).isTrue();
+        final ChangeRecord cr = reader.readChangeRecord();
+        assertThat(cr.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(cr instanceof ModifyRequest);
+        assertThat(((ModifyRequest) cr).getModifications()).isEmpty();
+        reader.close();
+        target.close();
+    }
+
+    /**
+     * The diff function doesn't allow malformed ldif. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testLdifDiffEntriesNoDiffMalformedTarget() throws Exception {
+
+        final LDIFEntryReader source =
+                new LDIFEntryReader("dn: uid=scarter,ou=People,dc=example,dc=com");
+
+        final LDIFEntryReader target = new LDIFEntryReader("dn: wrongRDN");
+
+        LDIF.diff(source, target);
+    }
+
+    /**
+     * LDIF diff - EntryReader/Writer doesn't allow null. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifDiffEntriesDoesntAllowNull() throws Exception {
+        LDIF.diff(null, null);
+    }
+
+    /**
+     * Create a patch without any differences with the original.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddNoDiff() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3); // objectclass - sn - mail
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch adds successfully an attribute 'manager' to the entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddDiff() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5); // objectclass - sn - mail
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch adds two new entries to the original.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddDiffNewEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=joneill,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=hamond,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "changetype: add",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        reader.close();
+    }
+
+    /**
+     * Tries to modify a nonexistent entry. The patch throws an error via the
+     * listener which is in RejectedChangeRecordListener.OVERWRITE.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddModifyNonExistantEntryDoNothing() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: cn=Lisa Jangles,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: telephonenumber",
+            "telephonenumber: (408) 555-2468"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete an entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchDeleteEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete in entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchDeleteEntryAmongSeveral() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Testing to delete attributes in a selected entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchDeleteAttributesEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Carter",
+            "sn: Sam",
+            "cn: Sam Carter",
+            "uid: scarter",
+            "mail: user.1@mail.com",
+            "postalAdress: 42 Shepherd Street",
+            "work-phone: 650/506-7000",
+            "work-phone: 650/506-0666",
+            "home-fax: 650-7001"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "delete: work-phone",
+            "work-phone: 650/506-0666",
+            "-",
+            "delete: home-fax"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(8);
+        assertThat(entry.getAttribute("work-phone").firstValueAsString()).isEqualTo("650/506-7000");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Modifying an entry : add
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch attempts to modify the dn adding uppercase.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDNEntryUppercaseUid() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=Scarter",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=Scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Attempts to modify the entry adding upper case in cn.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDNEntryUpperCaseDnNameSurname() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: cn=sam carter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: cn=sam carter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: cn=Sam Carter",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "cn=Sam Carter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * The patch attempts to modify a rdn of a specific entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDNEntry() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: scarter",
+            "",
+            "dn: uid=djackson,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "uid: djackson"
+
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=Susan Jacobs",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        // does not work with a single entry && ...
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString())
+                .isEqualTo("uid=djackson,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=Susan Jacobs,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); //The patch create uid attribute on the selected entry.
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    @Test
+    public final void testLdifPatchModifyDnEntry2() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=user.22",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.22,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.3,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Renames a branch : from ou=People,dc=example,dc=com to ou=Human
+     * Resources,dc=example,dc=com.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDnEntryBranch() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organizationalunit",
+            "ou: People",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3",
+            "",
+            "dn: uid=user.4,ou=People,dc=example,dc=org",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Allan",
+            "sn: Zorg",
+            "cn: Allan Zorg",
+            "uid: user.4"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAllAttributes("ou").iterator().next().firstValueAsString()).isEqualTo(
+                "Human Resources");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.2,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.3,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=org");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Renames a branch : from ou=People,dc=example,dc=com to ou=Human
+     * Resources,dc=example,dc=com. In this example deleteoldrdn is set to 0.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDnEntryBranchKeepsOldRdn() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: organizationalunit",
+            "ou: People",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaron",
+            "sn: Atrc",
+            "cn: Aaron Atrc",
+            "uid: user.3",
+            "",
+            "dn: uid=user.4,ou=People,dc=example,dc=org",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Allan",
+            "sn: Zorg",
+            "cn: Allan Zorg",
+            "uid: user.4"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources",
+            "deleteoldrdn: 0"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAllAttributes("ou").iterator().next().firstValueAsString()).isEqualTo(
+                "People");
+        assertThat(entry.getAttributeCount()).isEqualTo(2);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.2,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.3,ou=Human Resources,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.4,ou=People,dc=example,dc=org");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Moves an entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchModifyDnEntryNewSuperior() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "uid: user.1",
+            "mail: user.1@mail.com",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "# moves the entry from ou=People, dc=example,dc=com to Marketing",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: uid=user.1",
+            "deleteoldrdn: 1",
+            "newsuperior: ou=Marketing,dc=example,dc=com"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo(
+                "uid=user.1,ou=Marketing,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(6);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Applies simple patch to replace/add data to the input.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddReplace() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:sn",
+            "sn: scarter",
+            "-",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(4); // objectclass - sn - mail - manager
+        assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
+                "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Replaces / adds postalAdress.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchAddReplaceLanguageTagExample() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "postalAdress;lang-en: Shepherd Street"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace: postalAdress;lang-fr",
+            "postalAdress;lang-fr: 355 avenue Leon Blum",
+            "-",
+            "replace: postalAdress;lang-en",
+            "postalAdress;lang-en: 42 Shepherd Street"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttributeCount()).isEqualTo(5); // objectclass - sn - mail - manager - postalAdress
+        assertThat(entry.getAttribute("postalAdress;lang-fr").firstValueAsString()).isEqualTo(
+                "355 avenue Leon Blum");
+        assertThat(entry.getAttribute("postalAdress;lang-en").firstValueAsString()).isEqualTo(
+                "42 Shepherd Street");
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Tests some changes : add/replace/delete...
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchVariousChanges() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaccf",
+            "sn: Amar",
+            "cn: Aaccf Amar",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaren",
+            "sn: Atp",
+            "cn: Aaren Atp",
+            "mail: AarenAtp@mail.org",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco",
+            "description:: ZnVubnkgZGVzY3JpcHRpb24gISA6RA==",
+            "mail:: QWFyaWthQXRwY29AbWFpbC5vcmc=",
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Kadja",
+            "sn: Atpcol",
+            "cn: Kadja Atpcol"
+        );
+        // @formatter:on
+
+        final File file = File.createTempFile("sdk", ".png");
+        final String url = file.toURI().toURL().toString();
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:sn",
+            "sn: scarter",
+            "-",
+            "add: manager",
+            "manager: uid=joneill,ou=People,dc=example,dc=com",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "replace:description",
+            "description:: QWFyaWthIEF0cGNvIGRlc2NyaXB0aW9uIDogbG9yZW0gaXBzdW0uLi4=",
+            "-",
+            "add: jpegphoto",
+            "jpegphoto:< " + url,
+            "",
+            "dn: uid=user.3,ou=People,dc=example,dc=com",
+            "changetype: delete"
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
+                "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.0,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(5);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getAttributeCount()).isEqualTo(7);
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("mail").firstValueAsString()).isEqualTo(
+                "AarikaAtpco@mail.org");
+        assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                "Aarika Atpco description : lorem ipsum...");
+        assertThat(entry.getAttribute("jpegphoto")).isNotEmpty();
+        assertThat(reader.hasNext()).isFalse();
+
+        file.delete();
+        reader.close();
+    }
+
+    /**
+     * An example to illustrate an LDIFChangeRecordReader containing changes on
+     * previous ldif.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testLdifPatchContainingChanges() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.0,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaccf",
+            "sn: Amar",
+            "cn: Aaccf Amar",
+            "",
+            "dn: uid=user.1,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aaren",
+            "sn: Atp",
+            "cn: Aaren Atp",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Aarika",
+            "sn: Atpco",
+            "cn: Aarika Atpco"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:sn",
+                "sn: scarter",
+                "-",
+                "add: manager",
+                "manager: uid=joneill,ou=People,dc=example,dc=com",
+                "",
+                "dn: uid=user.0,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:sn",
+                "sn: Amarr",
+                "-",
+                "delete: givenName",
+                "",
+                "dn: uid=user.1,ou=People,dc=example,dc=com",
+                "changetype: modify",
+                "replace:givenName",
+                "givenName: Aarwen",
+                "-",
+                "add: manager",
+                "manager: uid=joneill,ou=People,dc=example,dc=com",
+                "-",
+                "add: mail",
+                "mail: Aarwen@mail.com",
+                "-",
+                "add: fax",
+                "fax: 555 555-5555",
+                "-",
+                "add: description",
+                "description:: QWFyd2VuIGRlc2NyaXB0aW9uLg=="
+        );
+        // @formatter:on
+
+        final EntryReader reader = LDIF.patch(input, patch);
+
+        Entry entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+        // Attr. list : objectclass - sn - mail - manager
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(entry.getAttribute("manager").firstValueAsString()).isEqualTo(
+                "uid=joneill,ou=People,dc=example,dc=com");
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        // Attr. list : objectclass - sn - cn
+        assertThat(entry.getAttributeCount()).isEqualTo(3);
+        assertThat(reader.hasNext()).isTrue();
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        // Attr. list : objectclass - sn - cn - givenName - manager - mail - fax - description
+        assertThat(entry.getAttributeCount()).isEqualTo(8);
+        assertThat(entry.getAttribute("description").firstValueAsString()).isEqualTo(
+                "Aarwen description.");
+        assertThat(reader.hasNext()).isTrue();
+        // Last entry, no modification on it.
+        entry = reader.readEntry();
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        // Attr. list : objectClass - givenname - sn - cn
+        assertThat(entry.getAttributeCount()).isEqualTo(4);
+        assertThat(reader.hasNext()).isFalse();
+        reader.close();
+    }
+
+    /**
+     * Tries to apply a patch which data are not valid. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testLdifPatchInvalidChangeRecord() throws Exception {
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+                "dn: uid=scarter,ou=People,dc=example,dc=com",
+                "changetype: modif\u0000",
+                "replace:sn",
+                "sn: scarter",
+                "-",
+                "add: manager\u0000",
+                "manager: uid=joneill,ou=People,dc=example,dc=com"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch);
+            reader.readEntry();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in FAIL_FAST Mode. On this example,
+     * attaching the listener to the patch should throw a decode exception as a
+     * wrong DN is used in the modify request. (The entry does not exist).
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testFailFastPatchOnModifyRequestFailsDueToWrongDN() throws Exception {
+
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.FAIL_FAST;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=WRONGUID,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in OVERWRITE Mode. On this example,
+     * attaching the listener to the patch should throw a decode exception even
+     * if a wrong DN is used in the modify request. (The entry does not exist).
+     * No data impacted by the patch, no warning sent.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testOverwritePatchOnModifyRequestSucceedsEvenIfWrongDN() throws Exception {
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.OVERWRITE;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=WRONGUID,ou=People,dc=example,dc=com",
+            "changetype: modify",
+            "add: work-phone",
+            "work-phone: 650/506-7000"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+            Entry entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttributeCount()).isEqualTo(3);
+            assertThat(entry.getAttribute("objectClass").firstValueAsString()).isEqualTo(
+                    "person");
+            assertThat(entry.getAttribute("sn").firstValueAsString()).isEqualTo(
+                    "new user");
+            assertThat(entry.getAttribute("mail").firstValueAsString()).isEqualTo(
+                    "mail@mailme.org");
+            assertThat(reader.hasNext()).isFalse();
+            assertThat(entry.getAttribute("work-phone")).isNull();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in FAIL_FAST Mode. On this example,
+     * attaching the listener to the patch should throw a decode exception as a
+     * wrong DN is used in the modify DN request. (The entry does not exist).
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testFailFastPatchOnModifyDNRequestFailsDueToWrongDN() throws Exception {
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.FAIL_FAST;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=WRONGscarter, dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources,dc=example,dc=com",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in OVERWRITE Mode. On this example,
+     * attaching the listener to the patch should not throw a decode exception
+     * even if a wrong DN is used in the modify DN request. (Even if the entry
+     * does not exist).
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testOverwritePatchOnModifyDNRequestSucceedsEvenWithWrongDN() throws Exception {
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.OVERWRITE;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: ou=WRONGscarter, dc=example,dc=com",
+            "changetype: modrdn",
+            "newrdn: ou=Human Resources",
+            "deleteoldrdn: 1"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+            Entry entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
+            assertThat(entry.getName().toString()).isNotEqualTo("uid=scarter,ou=Human Resources,dc=example,dc=com");
+            assertThat(entry.getAttributeCount()).isEqualTo(3);
+            assertThat(entry.getAttribute("objectClass").firstValueAsString()).isEqualTo(
+                    "person");
+            assertThat(entry.getAttribute("sn").firstValueAsString()).isEqualTo(
+                    "new user");
+            assertThat(entry.getAttribute("mail").firstValueAsString()).isEqualTo(
+                    "mail@mailme.org");
+            assertThat(reader.hasNext()).isFalse();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in FAIL_FAST Mode. On this example,
+     * attaching the listener to the patch should throw a decode exeption as
+     * another entry containing the requested change DN already exists.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public final void testFailFastPatchOnModifyDNRequestFailsDueToDuplicateEntry() throws Exception {
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.FAIL_FAST;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: uid=user.2,ou=People,dc=example,dc=com",
+            "deleteoldrdn: 0"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * The listener is in OVERWRITE Mode. On this example,
+     * attaching the listener to the patch should not throw a decode exeption
+     * even if another entry containing the requested change DN already exists.
+     * The DN is overwrited.
+     *
+     * @throws Exception
+     */
+    @Test
+    public final void testOverwritePatchOnModifyDNRequestSucceedsEvenWithDuplicateEntry() throws Exception {
+        final RejectedChangeRecordListener listener = RejectedChangeRecordListener.OVERWRITE;
+        // @formatter:off
+        final LDIFEntryReader input = new LDIFEntryReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "objectClass: person",
+            "sn: new user",
+            "mail: mail@mailme.org",
+            "",
+            "dn: uid=user.2,ou=People,dc=example,dc=com",
+            "objectClass: top",
+            "objectClass: person",
+            "objectClass: organizationalperson",
+            "objectClass: inetorgperson",
+            "givenName: Eniko",
+            "sn: Eniko",
+            "cn: Eniko Atpco",
+            "uid: user.2"
+        );
+        // @formatter:on
+
+        // @formatter:off
+        final  LDIFChangeRecordReader patch = new LDIFChangeRecordReader(
+            "dn: uid=scarter,ou=People,dc=example,dc=com",
+            "changetype: moddn",
+            "newrdn: uid=user.2",
+            "deleteoldrdn: 0"
+        );
+        // @formatter:on
+        EntryReader reader = new LDIFEntryReader();
+        try {
+            reader = LDIF.patch(input, patch, listener);
+            Entry entry = reader.readEntry();
+            assertThat(entry.getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+            assertThat(entry.getAttributeCount()).isEqualTo(4);
+            assertThat(entry.getAttribute("objectClass").firstValueAsString()).isEqualTo(
+                    "person");
+            assertThat(entry.getAttribute("sn").firstValueAsString()).isEqualTo(
+                    "new user");
+            assertThat(entry.getAttribute("mail").firstValueAsString()).isEqualTo(
+                    "mail@mailme.org");
+            assertThat(reader.hasNext()).isFalse();
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * LDIF patch doesn't allow null. Exception expected.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = NullPointerException.class)
+    public final void testLdifPatchDoesntAllowNull() throws Exception {
+        LDIF.patch(null, null);
+    }
+
+    // @formatter:off
+    private static final List<String> LDIF_ONE_ENTRY = Arrays.asList(
+        "dn: uid=user.1,ou=People,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalperson",
+        "objectClass: inetorgperson",
+        "givenName: Eniko",
+        "sn: Atpco",
+        "cn: Eniko Atpco",
+        "uid: user.1");
+    // @formatter:on
+
+    // @formatter:off
+    private static final List<String> LDIF_TWO_ENTRIES = Arrays.asList(
+        "dn: uid=user.1,ou=People,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalperson",
+        "objectClass: inetorgperson",
+        "givenName: Eniko",
+        "sn: Atpco",
+        "uid: user.1",
+        "",
+        "dn: uid=user.2,ou=People,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalperson",
+        "objectClass: inetorgperson",
+        "givenName: Aaaron",
+        "sn: Atp",
+        "uid: user.2");
+
+    @Test
+    public void testMakeEntry() throws Exception {
+        final Entry entry = LDIF.makeEntry(LDIF_ONE_ENTRY);
+        final Entry entry2 = LDIF.makeEntry(LDIF_ONE_ENTRY.toArray(new String[0]));
+
+        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entry.getAttribute("objectClass").firstValueAsString()).isEqualTo("top");
+        assertThat(entry.getAttribute("uid").firstValueAsString()).isEqualTo("user.1");
+        assertThat(entry.getAttribute("givenName").firstValueAsString()).isEqualTo("Eniko");
+        assertThat(entry.getAttribute("sn").firstValueAsString()).isEqualTo("Atpco");
+        assertThat(entry2).isEqualTo(entry);
+    }
+
+    @Test
+    public void testMakeEntries() throws Exception {
+        // @formatter:off
+        final List<Entry> entries = LDIF.makeEntries(LDIF_TWO_ENTRIES);
+        final List<Entry> entries2 = LDIF.makeEntries(LDIF_TWO_ENTRIES.toArray(new String[0]));
+
+        // @formatter:on
+        assertThat(entries).hasSize(2);
+        assertThat(entries.get(0).getName().toString()).isEqualTo("uid=user.1,ou=People,dc=example,dc=com");
+        assertThat(entries.get(1).getName().toString()).isEqualTo("uid=user.2,ou=People,dc=example,dc=com");
+        assertThat(entries2).isEqualTo(entries);
+    }
+
+    @Test
+    public void testMakeEntryEmpty() throws Exception {
+        try {
+            LDIF.makeEntry();
+            TestCaseUtils.failWasExpected(LocalizedIllegalArgumentException.class);
+        } catch (LocalizedIllegalArgumentException e) {
+            assertThat(e.getMessageObject()).isEqualTo(CoreMessages.WARN_READ_LDIF_ENTRY_NO_ENTRY_FOUND.get());
+        }
+    }
+
+    @Test
+    public void testMakeEntryWithMultipleEntries() throws Exception {
+        try {
+            LDIF.makeEntry(LDIF_TWO_ENTRIES);
+            TestCaseUtils.failWasExpected(LocalizedIllegalArgumentException.class);
+        } catch (LocalizedIllegalArgumentException e) {
+            assertThat(e.getMessageObject()).isEqualTo(
+                CoreMessages.WARN_READ_LDIF_ENTRY_MULTIPLE_ENTRIES_FOUND.get(2));
+        }
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testMakeEntryBadLDif() throws Exception {
+        LDIF.makeEntry("dummy: uid=user.1,ou=People,dc=example,dc=com");
+    }
+
+    @Test
+    public void testMakeEntriesEmpty() throws Exception {
+        try {
+            LDIF.makeEntries();
+            TestCaseUtils.failWasExpected(LocalizedIllegalArgumentException.class);
+        } catch (LocalizedIllegalArgumentException e) {
+            assertThat(e.getMessageObject()).isEqualTo(CoreMessages.WARN_READ_LDIF_ENTRY_NO_ENTRY_FOUND.get());
+        }
+    }
+
+    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testMakeEntriesBadLDif() throws Exception {
+        LDIF.makeEntries("dummy: uid=user.1,ou=People,dc=example,dc=com");
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testMakeEntriesNull() throws Exception {
+        LDIF.makeEntries((String[]) null);
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/TemplateTagTestcase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/TemplateTagTestcase.java
new file mode 100644
index 0000000..a475dfc
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldif/TemplateTagTestcase.java
@@ -0,0 +1,283 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldif;
+
+import static org.fest.assertions.Assertions.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.TemplateFile.Template;
+import org.forgerock.opendj.ldif.TemplateFile.TemplateEntry;
+import org.forgerock.opendj.ldif.TemplateFile.TemplateLine;
+import org.forgerock.opendj.ldif.TemplateFile.TemplateValue;
+import org.forgerock.opendj.ldif.TemplateTag.AttributeValueTag;
+import org.forgerock.opendj.ldif.TemplateTag.IfAbsentTag;
+import org.forgerock.opendj.ldif.TemplateTag.IfPresentTag;
+import org.forgerock.opendj.ldif.TemplateTag.TagResult;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class TemplateTagTestcase extends SdkTestCase {
+
+    private static final int LINE_NUMBER = 10;
+    private static final TemplateFile NULL_TEMPLATE_FILE = null;
+    private static final List<LocalizableMessage> NULL_WARNINGS = null;
+    private static final TemplateValue NULL_VALUE = null;
+    private static final TemplateLine NULL_LINE = null;
+
+    @Test
+    public void testIfAbsentTag() throws Exception {
+        TemplateTag tag = new IfAbsentTag();
+        String org = "org";
+        tagWithArguments(tag, "dc", org); // dc=org should be absent
+
+        // org value is absent
+        assertThat(tag.generateValue(templateEntry("v1", "v2"), NULL_VALUE)).isEqualTo(TagResult.SUCCESS);
+
+        // org value is present
+        assertThat(tag.generateValue(templateEntry(org, "v"), NULL_VALUE)).isEqualTo(TagResult.FAILURE);
+        assertThat(tag.generateValue(templateEntry("v", org), NULL_VALUE)).isEqualTo(TagResult.FAILURE);
+        assertThat(tag.generateValue(templateEntry(org), NULL_VALUE)).isEqualTo(TagResult.FAILURE);
+    }
+
+    @Test
+    public void testIfAbsentTagWithNoValue() throws Exception {
+        TemplateTag tag = new IfAbsentTag();
+        tagWithArguments(tag, "dc"); // dc should be absent
+
+        assertThat(tag.generateValue(templateEntry("v"), NULL_VALUE)).isEqualTo(TagResult.FAILURE);
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Invalid number of arguments.*")
+    public void testIfAbsentTagTooManyArguments() throws Exception {
+        tagWithArguments(new IfAbsentTag(), "dc", "org1", "org2"); // too many args
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Invalid number of arguments.*")
+    public void testIfAbsentTagNotEnoughArguments() throws Exception {
+        tagWithArguments(new IfAbsentTag()); // zero args
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Undefined attribute.*")
+    public void testIfAbsentTagNoAttribute() throws Exception {
+        tagWithArguments(new IfAbsentTag(), templateWithNoAttribute(), "dc");
+    }
+
+    @Test
+    public void testDNTagRootDN() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag);
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.rootDN()), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("");
+    }
+
+    @Test
+    public void testDNTagZeroComponent() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag);
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("ou=users,dc=example,dc=test");
+    }
+
+    @Test
+    public void testUnderscoreDNTagZeroComponent() throws Exception {
+        TemplateTag tag = new TemplateTag.UnderscoreDNTag();
+        tagWithArguments(tag);
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("ou=users_dc=example_dc=test");
+    }
+
+    @Test
+    public void testDNTagOneComponent() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag, "1");
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("ou=users");
+    }
+
+    @Test
+    public void testDNTagTwoComponent() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag, "2");
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("ou=users,dc=example");
+    }
+
+    @Test
+    public void testDNTagMinusOneComponent() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag, "-1");
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("dc=test");
+    }
+
+    @Test
+    public void testDNTagMinusTwoComponents() throws Exception {
+        TemplateTag tag = new TemplateTag.DNTag();
+        tagWithArguments(tag, "-2");
+
+        TemplateValue value = new TemplateValue(NULL_LINE);
+        tag.generateValue(templateEntry(DN.valueOf("ou=users,dc=example,dc=test")), value);
+
+        assertThat(value.getValueAsString()).isEqualTo("dc=example,dc=test");
+    }
+
+    @Test
+    public void testIfPresentTag() throws Exception {
+        TemplateTag tag = new TemplateTag.IfPresentTag();
+        String org = "org";
+        tagWithArguments(tag, "dc", org); // dc=org should be present
+
+        // org value is absent
+        assertThat(tag.generateValue(templateEntry("v1", "v2"), NULL_VALUE)).isEqualTo(TagResult.FAILURE);
+
+        // org value is present
+        assertThat(tag.generateValue(templateEntry(org, "v"), NULL_VALUE)).isEqualTo(TagResult.SUCCESS);
+        assertThat(tag.generateValue(templateEntry("v", org), NULL_VALUE)).isEqualTo(TagResult.SUCCESS);
+        assertThat(tag.generateValue(templateEntry(org), NULL_VALUE)).isEqualTo(TagResult.SUCCESS);
+    }
+
+    @Test
+    public void testIfPresentTagWithNoValue() throws Exception {
+        TemplateTag tag = new TemplateTag.IfPresentTag();
+        tagWithArguments(tag, "dc"); // dc=org should be present
+
+        assertThat(tag.generateValue(templateEntry("org"), NULL_VALUE)).isEqualTo(TagResult.SUCCESS);
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Invalid number of arguments.*")
+    public void testIfPresentTagTooManyArguments() throws Exception {
+        tagWithArguments(new IfPresentTag(), "1", "2", "3"); // too many args
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Invalid number of arguments.*")
+    public void testIfPresentTagNotEnoughArguments() throws Exception {
+        tagWithArguments(new IfPresentTag()); // zero args
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Undefined attribute.*")
+    public void testIfPresentTagNoAttribute() throws Exception {
+        tagWithArguments(new IfPresentTag(), templateWithNoAttribute(), "dc");
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Invalid number of arguments.*")
+    public void testAttributeValueTagTooManyArguments() throws Exception {
+        tagWithArguments(new AttributeValueTag(), "dc", "2", "3"); // too many args
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*below the lowest allowed value.*")
+    public void testAttributeValueTagBelowLowerBound() throws Exception {
+        tagWithArguments(new AttributeValueTag(), "dc", "-1");
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Cannot parse value.*")
+    public void testAttributeValueTagNotANumber() throws Exception {
+        tagWithArguments(new AttributeValueTag(), "dc", "notanumber");
+    }
+
+    @Test(expectedExceptions = DecodeException.class,
+            expectedExceptionsMessageRegExp = ".*Undefined attribute.*")
+    public void testAttributeValueTagNoAttribute() throws Exception {
+        tagWithArguments(new AttributeValueTag(), templateWithNoAttribute(), "dc");
+    }
+
+    /** Helper method to initialize tags with template having any attribute and some arguments. */
+    private void tagWithArguments(TemplateTag tag, String... arguments) throws DecodeException {
+        tagWithArguments(tag, templateWithAnyAttribute(), arguments);
+    }
+
+    /** Helper method to initialize tags with template and some arguments. */
+    private void tagWithArguments(TemplateTag tag, Template template, String... arguments)
+            throws DecodeException {
+        tag.initializeForTemplate(Schema.getDefaultSchema(), NULL_TEMPLATE_FILE, template,
+                arguments, LINE_NUMBER, NULL_WARNINGS);
+    }
+
+    /** Helper method to build a template entry containing the provided values. */
+    private TemplateEntry templateEntry(String... values) {
+        TemplateEntry templateEntry = mock(TemplateEntry.class);
+        List<TemplateValue> templateValues = new ArrayList<>();
+        for (String value : values) {
+            templateValues.add(templateValue(value));
+        }
+        when(templateEntry.getValues(any(AttributeType.class))).thenReturn(templateValues);
+        return templateEntry;
+    }
+
+    /** Helper method to build a template entry with the provided DN. */
+    private TemplateEntry templateEntry(DN dn) {
+        TemplateEntry templateEntry = mock(TemplateEntry.class);
+        when(templateEntry.getDN()).thenReturn(dn);
+        return templateEntry;
+    }
+
+    /** Helper method to build a template value from provided string. */
+    private TemplateValue templateValue(String value) {
+        TemplateValue templateVal = new TemplateFile.TemplateValue(null);
+        templateVal.append(value);
+        return templateVal;
+    }
+
+    /** Helper method to build a template that always return true on attribute type check. */
+    private Template templateWithAnyAttribute() {
+        Template template = mock(Template.class);
+        when(template.hasAttribute(any(AttributeType.class))).thenReturn(true);
+        return template;
+    }
+
+    /** Helper method to build a template that always return false on attribute type check. */
+    private Template templateWithNoAttribute() {
+        Template template = mock(Template.class);
+        when(template.hasAttribute(any(AttributeType.class))).thenReturn(false);
+        return template;
+    }
+}
diff --git a/opendj-sdk/opendj-core/src/test/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider b/opendj-sdk/opendj-core/src/test/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider
new file mode 100644
index 0000000..052a548
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider
@@ -0,0 +1,15 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2013 ForgeRock AS.
+org.forgerock.opendj.ldap.spi.BasicTransportProvider
diff --git a/opendj-sdk/opendj-doc-maven-plugin/pom.xml b/opendj-sdk/opendj-doc-maven-plugin/pom.xml
new file mode 100644
index 0000000..b60c44f
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/pom.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2015-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-doc-maven-plugin</artifactId>
+    <packaging>maven-plugin</packaging>
+
+    <name>OpenDJ Doc Helper Maven Plugin</name>
+    <description>Helps to build generated documentation sources.</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>2.3.21</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.twdata.maven</groupId>
+            <artifactId>mojo-executor</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.thoughtworks.qdox</groupId>
+            <artifactId>qdox</artifactId>
+            <version>2.0-M3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.maven.plugin-tools</groupId>
+            <artifactId>maven-plugin-annotations</artifactId>
+            <version>3.2</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate-messages</goal>
+                        </goals>
+                        <configuration>
+                            <messageFiles>
+                                <messageFile>org/forgerock/opendj/maven/doc/docs.properties</messageFile>
+                            </messageFiles>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/CommandLineTool.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/CommandLineTool.java
new file mode 100644
index 0000000..dc76e60
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/CommandLineTool.java
@@ -0,0 +1,167 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import java.util.List;
+
+/**
+ * Represents a command-line tool as used in the configuration for {@see GenerateRefEntriesMojo}.
+ * <br>
+ * Command-line tools are associated with a script name, the Java class of the tool,
+ * and a list of relative paths to hand-written files for trailing sections.
+ * <br>
+ * Trailing section paths are relative to the RefEntry file to write.
+ */
+public class CommandLineTool {
+    /** The script name. */
+    private String name;
+
+    /** The tool class. */
+    private String application;
+
+    /**
+     * Additional paths to DocBook XML {@code RefSect1} documents
+     * to be appended after generated content in reference documentation.
+     *
+     * <br>
+     *
+     * DocBook represents a reference manual page with the {@code RefEntry}.
+     * See <a href="http://www.docbook.org/tdg51/en/html/refentry.html">refentry</a>.
+     *
+     * <br>
+     *
+     * A {@code RefEntry} describing an OpenDJ tool contains
+     * block elements in the following order:
+     *
+     * <pre>
+     *     RefMeta
+     *     RefNameDiv
+     *     RefSynopsisDiv
+     *     RefSect1 - Description (generated, potentially with a hand-written supplement)
+     *     RefSect1 - Options (generated)
+     *     RefSect1 - Subcommands (optional, hand-written intro + generated RefSect2s)
+     *     RefSect1 - Filter (optional, hand-written)
+     *     RefSect1 - Attribute (optional, hand-written)
+     *     RefSect1 - Exit Codes (hand-written)
+     *     RefSect1 - Files (optional, hand-written)
+     *     RefSect1 - Examples (hand-written)
+     *     RefSect1 - See Also (hand-written)
+     * </pre>
+     *
+     * As the trailing RefSect1s following Subcommands are hand-written,
+     * they are included in the generated content as XIncludes elements.
+     * The paths in this case are therefore relative to the current RefEntry.
+     */
+    private List<String> trailingSectionPaths;
+
+    /**
+     * Returns the script name.
+     * @return The script name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Set the script name.
+     * @param name The script name.
+     */
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the tool class.
+     * @return The tool class.
+     */
+    public String getApplication() {
+        return application;
+    }
+
+    /**
+     * Set the tool class.
+     * @param application The tool class.
+     */
+    public void setApplication(final String application) {
+        this.application = application;
+    }
+
+    /**
+     * Returns additional paths to DocBook XML {@code RefSect1} documents
+     * to be appended after generated content in reference documentation.
+     *
+     * <br>
+     *
+     * DocBook represents a reference manual page with the {@code RefEntry}.
+     * See <a href="http://www.docbook.org/tdg51/en/html/refentry.html">refentry</a>.
+     *
+     * <br>
+     *
+     * A {@code RefEntry} describing an OpenDJ tool contains
+     * block elements in the following order:
+     *
+     * <pre>
+     *     RefMeta
+     *     RefNameDiv
+     *     RefSynopsisDiv
+     *     RefSect1 - Description (generated, potentially with a hand-written supplement)
+     *     RefSect1 - Options (generated)
+     *     RefSect1 - Subcommands (optional, hand-written intro + generated RefSect2s)
+     *     RefSect1 - Filter (optional, hand-written)
+     *     RefSect1 - Attribute (optional, hand-written)
+     *     RefSect1 - Exit Codes (hand-written)
+     *     RefSect1 - Files (optional, hand-written)
+     *     RefSect1 - Examples (hand-written)
+     *     RefSect1 - See Also (hand-written)
+     * </pre>
+     *
+     * As the trailing RefSect1s following Subcommands are hand-written,
+     * they are included in the generated content as XIncludes elements.
+     * The paths in this case are therefore relative to the current RefEntry.
+     *
+     * @return The relative paths to trailing section files.
+     */
+    public List<String> getTrailingSectionPaths() {
+        return trailingSectionPaths;
+    }
+
+    /**
+     * Set additional paths to DocBook XML {@code RefSect1} documents.
+     * @param paths The paths relative to the current RefEntry.
+     */
+    public void setTrailingSectionPaths(final List<String> paths) {
+        this.trailingSectionPaths = paths;
+    }
+
+    /** Whether the tool is enabled. Default: true. */
+    private boolean enabled = true;
+
+    /**
+     * Returns true if the tool is enabled.
+     * @return true if the tool is enabled.
+     */
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Set to true if the tool is enabled, false otherwise.
+     * @param enabled true if the tool is enabled, false otherwise.
+     */
+    public void setEnabled(final boolean enabled) {
+        this.enabled = enabled;
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateConfigurationReferenceMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateConfigurationReferenceMojo.java
new file mode 100644
index 0000000..a035a20
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateConfigurationReferenceMojo.java
@@ -0,0 +1,154 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.forgerock.opendj.maven.doc.Utils.*;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Generates the configuration reference, a set of HTML documents describing the server configuration.
+ */
+@Mojo(name = "generate-config-ref", defaultPhase = LifecyclePhase.PRE_SITE)
+public class GenerateConfigurationReferenceMojo extends AbstractMojo {
+    /**
+     * The Maven Project.
+     */
+    @Parameter(property = "project", readonly = true, required = true)
+    private MavenProject project;
+
+    /**
+     * The path to the directory where the configuration reference should be written.
+     * This path must be under <code>${project.build.directory}</code>.
+     */
+    @Parameter(defaultValue = "${project.build.directory}/site/configref")
+    private String outputDirectory;
+
+    /**
+     * Generates the configuration reference under the output directory.
+     * @throws MojoExecutionException   Generation failed
+     * @throws MojoFailureException     Not used
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        createOutputDirectory();
+        generateConfigRef();
+        try {
+            copyResources();
+        } catch (IOException e) {
+            throw new MojoExecutionException("Failed to copy resource files.", e);
+        }
+    }
+
+    /**
+     * Creates the output directory where the configuration reference is written.
+     * @throws MojoExecutionException   The output directory is not under <code>${project.build.directory}</code>
+     *                                  or could not be created.
+     */
+    private void createOutputDirectory() throws MojoExecutionException {
+        String projectBuildDir = project.getBuild().getDirectory();
+
+        if (!outputDirectory.contains(projectBuildDir)) {
+            String errorMsg = String.format(
+                    "The outputDirectory (%s) must be under the ${project.build.directory} (%s).",
+                    outputDirectory,
+                    projectBuildDir);
+            getLog().error(errorMsg);
+            throw new MojoExecutionException(errorMsg);
+        }
+
+        try {
+            createDirectory(outputDirectory);
+        } catch (IOException e) {
+            getLog().error(e.getMessage());
+            throw new MojoExecutionException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Runs the configuration reference generator class.
+     * @throws MojoExecutionException   Failed to run the generator
+     */
+    private void generateConfigRef() throws MojoExecutionException {
+        String generatorClass = "org.opends.server.admin.doc.ConfigGuideGeneration";
+        List<String> commands = new LinkedList<>();
+        try {
+            commands.add(getJavaCommand());
+            commands.add("-classpath");
+            commands.add(getClassPath(getRuntimeClassLoader(project, getLog())));
+            commands.add("-DGenerationDir=" + outputDirectory);
+            commands.add(generatorClass);
+        } catch (Exception e) {
+            throw new MojoExecutionException("Failed to set the classpath.", e);
+        }
+
+        try {
+            ProcessBuilder builder = new ProcessBuilder(commands);
+            Process process = builder.start();
+            process.waitFor();
+            final int result = process.exitValue();
+            if (result != 0) {
+                final StringBuilder message = new StringBuilder();
+                message.append("Failed to generate the config ref. Exit code: ").append(result).append(EOL)
+                        .append("To debug the problem, run the following command and connect your IDE:").append(EOL);
+                commands.add(1, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000");
+                for (String arg: commands) {
+                    message.append(arg).append(' ');
+                }
+                message.append(EOL);
+                throw new MojoExecutionException(message.toString());
+            }
+        }  catch (InterruptedException e) {
+            throw new MojoExecutionException(generatorClass + " interrupted", e);
+        } catch (IOException e) {
+            throw new MojoExecutionException(generatorClass + " not found", e);
+        }
+    }
+
+    /** List of static file resources needed by the configuration reference. */
+    private static String[] resourceFiles = {
+        "duration-syntax.html",
+        "opendj-config.css",
+        "opendj_logo_sm.png",
+        "pageaction.gif",
+        "tab_deselected.jpg",
+        "tab_selected.gif"
+    };
+
+    /**
+     * Copies static files needed by the configuration reference.
+     * @throws IOException  Failed to read a resource or to write a file
+     */
+    private void copyResources() throws IOException {
+        for (String file : resourceFiles) {
+            InputStream original = this.getClass().getResourceAsStream("/config-ref/" + file);
+            File copy = new File(outputDirectory, file);
+            copyInputStreamToFile(original, copy);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateGlobalAcisTableMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateGlobalAcisTableMojo.java
new file mode 100644
index 0000000..346a6f0
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateGlobalAcisTableMojo.java
@@ -0,0 +1,188 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.forgerock.opendj.maven.doc.DocsMessages.*;
+import static org.forgerock.opendj.maven.doc.Utils.applyTemplate;
+import static org.forgerock.opendj.maven.doc.Utils.writeStringToFile;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Generates documentation source table listing global ACIs.
+ */
+@Mojo(name = "generate-global-acis-table")
+public class GenerateGlobalAcisTableMojo extends AbstractMojo {
+    /** The locale for which to generate the documentation. */
+    @Parameter(defaultValue = "en")
+    private String locale;
+
+    /** The config.ldif file containing default global ACIs. **/
+    @Parameter(defaultValue = "${basedir}/resource/config/config.ldif")
+    private File configDotLdif;
+
+    /** Output directory for source files. */
+    @Parameter(defaultValue = "${project.build.directory}/docbkx-sources/shared")
+    private File outputDirectory;
+
+    /** Holds documentation for an ACI. */
+    private class Aci {
+        String name;
+        String description;
+        String definition;
+    }
+
+    /** Holds the list of global ACIs. */
+    private static List<Aci> allGlobalAcis = new LinkedList<>();
+
+    /**
+     * Writes documentation source table listing global ACIs.
+     * @throws MojoExecutionException   Not used.
+     * @throws MojoFailureException     Failed to read ACIs or to write the table file.
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        try {
+            readAcis(getAciDescriptions());
+        } catch (IOException e) {
+            throw new MojoFailureException(e.getMessage(), e);
+        }
+
+        File table = new File(outputDirectory, "table-global-acis.xml");
+        try {
+            writeStringToFile(getGlobalAcisTable(), table);
+        } catch (IOException e) {
+            throw new MojoFailureException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Populates map of {@code ds-cfg-global-aci} descriptions from comments in {@code config.ldif}.
+     * Keys are ACI names. Values are descriptions.
+     * <br>
+     * The format expected for ACI description comments is the following:
+     * {@code # @aci name: description},
+     * where {@code name} matches the name embedded in the ACI,
+     * and {@code description} is a longer description.
+     * @throws IOException  Failed to read the LDIF.
+     */
+    private Map<String, String> getAciDescriptions() throws IOException {
+        final Map<String, String> descriptions = new HashMap<>();
+        BufferedReader reader = new BufferedReader(new FileReader(configDotLdif));
+        String line;
+        while ((line = reader.readLine()) != null) {
+            if (line.startsWith("# @aci ")) {
+                String[] split = line.replace("# @aci ", "").split(":", 2);
+                descriptions.put(split[0], split[1]);
+            }
+        }
+        return descriptions;
+    }
+
+    /**
+     * Reads {@code ds-cfg-global-aci} values from {@code config.ldif} into the list of Acis.
+     * @param descriptions Long descriptions from comments in {@code config.ldif}.
+     *                     Keys are ACI names. Values are descriptions.
+     * @throws IOException  Failed to read the LDIF.
+     */
+    private void readAcis(Map<String, String> descriptions) throws IOException {
+        LDIFEntryReader reader = new LDIFEntryReader(new FileInputStream(configDotLdif));
+        reader.setIncludeBranch(DN.valueOf("cn=Access Control Handler,cn=config"));
+
+        while (reader.hasNext()) {
+            Entry entry = reader.readEntry();
+            for (String attribute : entry.parseAttribute("ds-cfg-global-aci").asSetOfString()) {
+                Aci aci = new Aci();
+                aci.name = getName(attribute);
+                if (descriptions != null) {
+                    aci.description = descriptions.get(aci.name);
+                }
+                aci.definition = attribute;
+                allGlobalAcis.add(aci);
+            }
+        }
+    }
+
+    /**
+     * Returns the user-friendly name embedded in the ACI.
+     * @param aci   The string representation of the ACI value.
+     * @return  The user-friendly name embedded in the ACI,
+     *          or an empty string if no name is found.
+     */
+    private String getName(String aci) {
+        // Extract the user-friendly string in
+        // {@code ...version 3.0; acl "user-friendly name"...}.
+        Pattern pattern = Pattern.compile(".+version 3.0; ?acl \"([^\"]+)\".+");
+        Matcher matcher = pattern.matcher(aci);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        return "";
+    }
+
+    /**
+     * Returns a DocBook XML table listing global ACIs.
+     * @return A DocBook XML table listing global ACIs.
+     */
+    private String getGlobalAcisTable() {
+        final Map<String, Object> map = new HashMap<>();
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+        map.put("lang", locale);
+        map.put("title", DOC_GLOBAL_ACIS_TABLE_TITLE.get());
+        map.put("summary", DOC_GLOBAL_ACIS_TABLE_SUMMARY.get());
+        map.put("nameTitle", DOC_GLOBAL_ACIS_NAME_COLUMN_TITLE.get());
+        map.put("descTitle", DOC_GLOBAL_ACIS_DESCRIPTION_COLUMN_TITLE.get());
+        map.put("defTitle", DOC_GLOBAL_ACIS_DEFINITION_COLUMN_TITLE.get());
+        map.put("acis", getDefaultGlobalAciList());
+        return applyTemplate("table-global-acis.ftl", map);
+    }
+
+    /**
+     * Returns a list of information about default global ACIs.
+     * @return A list of information about default global ACIs.
+     */
+    private List<Map<String, Object>> getDefaultGlobalAciList() {
+        final List<Map<String, Object>> globalAciList = new LinkedList<>();
+        for (final Aci aci : allGlobalAcis) {
+            final Map<String, Object> map = new HashMap<>();
+            map.put("name", aci.name);
+            map.put("description", aci.description);
+            map.put("definition", aci.definition);
+            globalAciList.add(map);
+        }
+        return globalAciList;
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java
new file mode 100644
index 0000000..363e719
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java
@@ -0,0 +1,378 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2008-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.apache.maven.plugins.annotations.LifecyclePhase.*;
+import static org.forgerock.opendj.maven.doc.DocsMessages.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateExceptionHandler;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.forgerock.i18n.LocalizableMessage;
+
+/** Generates an XML file of log messages found in properties files. */
+@Mojo(name = "generate-xml-messages-doc", defaultPhase = PRE_SITE)
+public class GenerateMessageFileMojo extends AbstractMojo {
+    /** The Maven Project. */
+    @Parameter(property = "project", readonly = true, required = true)
+    private MavenProject project;
+    /** The tag of the locale for which to generate the documentation. */
+    @Parameter(defaultValue = "en")
+    private String locale;
+    /** The path to the directory containing the message properties files. */
+    @Parameter(required = true)
+    private String messagesDirectory;
+
+    /**
+     * The path to the directory where the XML file should be written.
+     * This path must be relative to ${project.build.directory}.
+     */
+    @Parameter(required = true)
+    private String outputDirectory;
+
+    /** A list which contains all file names, the extension is not needed. */
+    @Parameter(required = true)
+    private List<String> messageFileNames;
+    /** One-line descriptions for log reference categories. */
+    private static final Map<String, LocalizableMessage> CATEGORY_DESCRIPTIONS = new HashMap<>();
+    static {
+        CATEGORY_DESCRIPTIONS.put("ACCESS_CONTROL", CATEGORY_ACCESS_CONTROL.get());
+        CATEGORY_DESCRIPTIONS.put("ADMIN", CATEGORY_ADMIN.get());
+        CATEGORY_DESCRIPTIONS.put("ADMIN_TOOL", CATEGORY_ADMIN_TOOL.get());
+        CATEGORY_DESCRIPTIONS.put("BACKEND", CATEGORY_BACKEND.get());
+        CATEGORY_DESCRIPTIONS.put("CONFIG", CATEGORY_CONFIG.get());
+        CATEGORY_DESCRIPTIONS.put("CORE", CATEGORY_CORE.get());
+        CATEGORY_DESCRIPTIONS.put("DSCONFIG", CATEGORY_DSCONFIG.get());
+        CATEGORY_DESCRIPTIONS.put("EXTENSIONS", CATEGORY_EXTENSIONS.get());
+        CATEGORY_DESCRIPTIONS.put("JEB", CATEGORY_JEB.get());
+        CATEGORY_DESCRIPTIONS.put("LOG", CATEGORY_LOG.get());
+        CATEGORY_DESCRIPTIONS.put("PLUGIN", CATEGORY_PLUGIN.get());
+        CATEGORY_DESCRIPTIONS.put("PROTOCOL", CATEGORY_PROTOCOL.get());
+        CATEGORY_DESCRIPTIONS.put("QUICKSETUP", CATEGORY_QUICKSETUP.get());
+        CATEGORY_DESCRIPTIONS.put("RUNTIME_INFORMATION", CATEGORY_RUNTIME_INFORMATION.get());
+        CATEGORY_DESCRIPTIONS.put("SCHEMA", CATEGORY_SCHEMA.get());
+        CATEGORY_DESCRIPTIONS.put("SYNC", CATEGORY_SYNC.get());
+        CATEGORY_DESCRIPTIONS.put("TASK", CATEGORY_TASK.get());
+        CATEGORY_DESCRIPTIONS.put("THIRD_PARTY", CATEGORY_THIRD_PARTY.get());
+        CATEGORY_DESCRIPTIONS.put("TOOLS", CATEGORY_TOOLS.get());
+        CATEGORY_DESCRIPTIONS.put("USER_DEFINED", CATEGORY_USER_DEFINED.get());
+        CATEGORY_DESCRIPTIONS.put("UTIL", CATEGORY_UTIL.get());
+        CATEGORY_DESCRIPTIONS.put("VERSION", CATEGORY_VERSION.get());
+    }
+
+    /** Message giving formatting rules for string keys. */
+    public static final String KEY_FORM_MSG = ".\n\nOpenDJ message property keys must be of the form\n\n"
+            + "\t\'[CATEGORY]_[SEVERITY]_[DESCRIPTION]_[ORDINAL]\'\n\n";
+    private static final String ERROR_SEVERITY_IDENTIFIER_STRING = "ERR_";
+
+    /** FreeMarker template configuration. */
+    private Configuration configuration;
+
+    private Configuration getConfiguration() {
+        if (configuration == null) {
+            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+            configuration.setClassForTemplateLoading(GenerateSchemaDocMojo.class, "/templates");
+            configuration.setDefaultEncoding("UTF-8");
+            configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
+        }
+        return configuration;
+    }
+
+    /**
+     * Writes the result of applying the FreeMarker template to the data.
+     * @param file                  The file to write to.
+     * @param template              The name of a file in {@code resources/templates/}.
+     * @param map                   The data to use in the template.
+     * @throws IOException          Failed to write to the file.
+     * @throws TemplateException    Failed to load the template.
+     */
+    private void writeLogRef(final File file, final String template, final Map<String, Object> map)
+            throws IOException, TemplateException {
+        // FreeMarker requires a configuration to find the template.
+        configuration = getConfiguration();
+
+        // FreeMarker takes the data and a Writer to process the template.
+        try (Writer writer = new PrintWriter(file)) {
+            configuration.getTemplate(template).process(map, writer);
+        }
+    }
+
+    /** Represents a log reference entry for an individual message. */
+    private static class MessageRefEntry implements Comparable<MessageRefEntry> {
+        private Integer ordinal;
+        private String xmlId;
+        private String formatString;
+
+        /** Build log reference entry for an log message. */
+        public MessageRefEntry(final String msgPropKey, final Integer ordinal, final String formatString) {
+            this.formatString = formatString;
+            this.ordinal = ordinal;
+            xmlId = getXmlId(msgPropKey);
+        }
+
+        private String getXmlId(final String messagePropertyKey) {
+            // XML IDs must be unique, must begin with a letter ([A-Za-z]),
+            // and may be followed by any number of letters, digits ([0-9]),
+            // hyphens ("-"), underscores ("_"), colons (":"), and periods (".").
+
+            final String invalidChars = "[^A-Za-z0-9\\-_:\\.]";
+            return messagePropertyKey.replaceAll(invalidChars, "-");
+        }
+
+        /**
+         * Returns a map of this log reference entry, suitable for use with FreeMarker.
+         * This implementation copies the message string verbatim.
+         * @return A map of this log reference entry, suitable for use with FreeMarker.
+         */
+        public Map<String, Object> toMap() {
+            Map<String, Object> map = new HashMap<>();
+            String id = (ordinal != null) ? ordinal.toString() : MESSAGE_NO_ORDINAL.get().toString();
+            map.put("xmlId", "log-ref-" + xmlId);
+            map.put("id", MESSAGE_ORDINAL_ID.get(id));
+            map.put("severity", MESSAGE_SEVERITY.get(ERROR_SEVERITY_PRINTABLE.get()));
+            map.put("message", MESSAGE_MESSAGE.get(formatString));
+            return map;
+        }
+
+        /**
+         * Compare message entries by unique identifier.
+         *
+         * @return See {@link java.lang.Comparable#compareTo(Object)}.
+         */
+        @Override
+        public int compareTo(MessageRefEntry mre) {
+            if (this.ordinal != null && mre.ordinal != null) {
+                return this.ordinal.compareTo(mre.ordinal);
+            }
+            return 0;
+        }
+    }
+
+    /** Represents a log reference list of messages for a category. */
+    private static class MessageRefCategory {
+        private String category;
+        private TreeSet<MessageRefEntry> messages;
+
+        MessageRefCategory(final String category, final TreeSet<MessageRefEntry> messages) {
+            this.category = category;
+            this.messages = messages;
+        }
+
+        /**
+         * Returns a map of this log reference category, suitable for use with FreeMarker.
+         * @return A map of this log reference category, suitable for use with FreeMarker.
+         */
+        public Map<String, Object> toMap() {
+            Map<String, Object> map = new HashMap<>();
+            map.put("id", category);
+            map.put("category", MESSAGE_CATEGORY.get(category));
+            List<Map<String, Object>> messageEntries = new LinkedList<>();
+            for (MessageRefEntry entry : messages) {
+                messageEntries.add(entry.toMap());
+            }
+            map.put("entries", messageEntries);
+            return map;
+        }
+    }
+
+    private static class MessagePropertyKey implements Comparable<MessagePropertyKey> {
+        private String description;
+        private Integer ordinal;
+
+        /**
+         * Creates a message property key from a string value.
+         *
+         * @param key
+         *            from properties file
+         * @return MessagePropertyKey created from string
+         */
+        public static MessagePropertyKey parseString(String key) {
+            int li = key.lastIndexOf("_");
+            if (li == -1) {
+                throw new IllegalArgumentException("Incorrectly formatted key " + key);
+            }
+
+            final String description = key.substring(0, li).toUpperCase();
+            Integer ordinal = null;
+            try {
+                String ordString = key.substring(li + 1);
+                ordinal = Integer.parseInt(ordString);
+            } catch (Exception nfe) {
+                // Ignore exception, the message has no ordinal.
+            }
+            return new MessagePropertyKey(description, ordinal);
+        }
+
+        /**
+         * Creates a parameterized instance.
+         *
+         * @param description
+         *            of this key
+         * @param ordinal
+         *            of this key
+         */
+        public MessagePropertyKey(String description, Integer ordinal) {
+            this.description = description;
+            this.ordinal = ordinal;
+        }
+
+        /**
+         * Gets the ordinal of this key.
+         *
+         * @return ordinal of this key
+         */
+        public Integer getOrdinal() {
+            return this.ordinal;
+        }
+
+        @Override
+        public String toString() {
+            if (ordinal != null) {
+                return description + "_" + ordinal;
+            }
+            return description;
+        }
+
+        @Override
+        public int compareTo(MessagePropertyKey k) {
+            if (ordinal == k.ordinal) {
+                return description.compareTo(k.description);
+            } else if (ordinal != null && k.ordinal != null) {
+                return ordinal.compareTo(k.ordinal);
+            } else {
+                return 0;
+            }
+        }
+    }
+
+    /**
+     * For maven exec plugin execution. Generates for all included message files
+     * (sample.properties), a xml log ref file (log-ref-sample.xml)
+     *
+     * @throws MojoExecutionException
+     *          if a problem occurs
+     * @throws MojoFailureException
+     *          if a problem occurs
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        String projectBuildDir = project.getBuild().getDirectory();
+
+        if (!outputDirectory.contains(projectBuildDir)) {
+            String errorMsg = String.format("outputDirectory parameter (%s) must be included "
+                    + "in ${project.build.directory} (%s)", outputDirectory, projectBuildDir);
+            getLog().error(errorMsg);
+            throw new MojoExecutionException(errorMsg);
+        }
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+        map.put("lang", locale);
+        map.put("title", LOG_REF_TITLE.get());
+        map.put("indexterm", LOG_REF_INDEXTERM.get());
+        map.put("intro", LOG_REF_INTRO.get());
+        List<Map<String, Object>> categories = new LinkedList<>();
+        for (String category : messageFileNames) {
+            File source = new File(messagesDirectory, category + ".properties");
+            categories.add(getCategoryMap(source, category.toUpperCase()));
+        }
+        map.put("categories", categories);
+        File file = new File(outputDirectory, "log-message-reference.xml");
+        try {
+            createOutputDirectory();
+            writeLogRef(file, "log-message-reference.ftl", map);
+        } catch (Exception e) {
+            throw new MojoFailureException(e.getMessage(), e);
+        }
+    }
+
+    private void createOutputDirectory() throws IOException {
+        File outputDir = new File(outputDirectory);
+        if (!outputDir.exists() && !outputDir.mkdirs()) {
+            throw new IOException("Failed to create output directory.");
+        }
+    }
+
+    private Map<String, Object> getCategoryMap(File source, String globalCategory) throws MojoExecutionException {
+        Properties properties = new Properties();
+        try {
+            properties.load(new FileInputStream(source));
+            Map<MessagePropertyKey, String> errorMessages = loadErrorProperties(properties);
+            TreeSet<MessageRefEntry> messageRefEntries = new TreeSet<>();
+            Set<Integer> usedOrdinals = new HashSet<>();
+
+            for (MessagePropertyKey msgKey : errorMessages.keySet()) {
+                String formatString = errorMessages.get(msgKey).replaceAll("<", "&lt;");
+                Integer ordinal = msgKey.getOrdinal();
+                if (ordinal != null && usedOrdinals.contains(ordinal)) {
+                    throw new Exception("The ordinal value \'" + ordinal + "\' in key " + msgKey
+                            + " has been previously defined in " + source + KEY_FORM_MSG);
+                }
+                usedOrdinals.add(ordinal);
+                messageRefEntries.add(new MessageRefEntry(msgKey.toString(), ordinal, formatString));
+            }
+
+            return messageRefEntries.isEmpty()
+                    ? new HashMap<String, Object>()
+                    : new MessageRefCategory(globalCategory, messageRefEntries).toMap();
+        } catch (Exception e) {
+            throw new MojoExecutionException(e.getMessage(), e);
+        }
+    }
+
+    private Map<MessagePropertyKey, String> loadErrorProperties(Properties properties) throws Exception {
+        Map<MessagePropertyKey, String> errorMessage = new TreeMap<>();
+        for (Object propO : properties.keySet()) {
+            String propKey = propO.toString();
+            try {
+                // Document only ERROR messages.
+                if (propKey.startsWith(ERROR_SEVERITY_IDENTIFIER_STRING)) {
+                    MessagePropertyKey key = MessagePropertyKey.parseString(propKey);
+                    String formatString = properties.getProperty(propKey);
+                    errorMessage.put(key, formatString);
+                }
+            } catch (IllegalArgumentException iae) {
+                throw new Exception("invalid property key " + propKey + ": " + iae.getMessage() + KEY_FORM_MSG, iae);
+            }
+        }
+
+        return errorMessage;
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateRefEntriesMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateRefEntriesMojo.java
new file mode 100644
index 0000000..9747a9b
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateRefEntriesMojo.java
@@ -0,0 +1,275 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.forgerock.opendj.maven.doc.Utils.*;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+import java.net.URLClassLoader;
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Generate DocBook RefEntry source documents for command-line tools man pages.
+ */
+@Mojo(name = "generate-refentry", defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
+        requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
+public final class GenerateRefEntriesMojo extends AbstractMojo {
+
+    /** The Maven Project. */
+    @Parameter(property = "project", required = true, readonly = true)
+    private MavenProject project;
+
+    /** Tools for which to generate RefEntry files. */
+    @Parameter
+    private List<CommandLineTool> tools;
+
+    /** Where to write the RefEntry files. */
+    @Parameter(required = true)
+    private File outputDir;
+
+    private static final String EOL = System.getProperty("line.separator");
+
+    /**
+     * Writes a RefEntry file to the output directory for each tool.
+     * Files names correspond to script names: {@code man-&lt;name>.xml}.
+     *
+     * @throws MojoExecutionException   Encountered a problem writing a file.
+     * @throws MojoFailureException     Failed to initialize effectively,
+     *                                  or to write one or more RefEntry files.
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        if (!isOutputDirAvailable()) {
+            throw new MojoFailureException("Output directory " + outputDir.getPath() + " not available");
+        }
+
+        // A Maven plugin classpath does not include project files.
+        // Prepare a ClassLoader capable of loading the command-line tools.
+        final URLClassLoader toolsClassLoader;
+        try {
+            toolsClassLoader = getRuntimeClassLoader(project, getLog());
+        } catch (Exception e) {
+            throw new MojoExecutionException("Failed to get class loader.", e);
+        }
+        for (CommandLineTool tool : tools) {
+            if (tool.isEnabled()) {
+                generateManPageForTool(toolsClassLoader, tool);
+            }
+        }
+    }
+
+    /**
+     * Generate a RefEntry file to the output directory for a tool.
+     * The files name corresponds to the tool name: {@code man-&lt;name>.xml}.
+     * @param toolsClassLoader          The ClassLoader to run the tool.
+     * @param tool                      The tool to run in order to generate the page.
+     * @throws MojoExecutionException   Failed to run the tool.
+     * @throws MojoFailureException     Tool did not exit successfully.
+     */
+    private void generateManPageForTool(final URLClassLoader toolsClassLoader, final CommandLineTool tool)
+            throws MojoExecutionException, MojoFailureException {
+        final File   manPage    = new File(outputDir, "man-" + tool.getName() + ".xml");
+        final String toolScript = tool.getName();
+        final String toolSects  = pathsToXIncludes(tool.getTrailingSectionPaths());
+        final String toolClass  = tool.getApplication();
+        List<String> commands   = new LinkedList<>();
+        commands.add(getJavaCommand());
+        commands.addAll(getJavaArgs(toolScript, toolSects));
+        commands.add("-classpath");
+        try {
+            commands.add(getClassPath(toolsClassLoader));
+        } catch (URISyntaxException e) {
+            throw new MojoExecutionException("Failed to set the classpath.", e);
+        }
+        commands.add(toolClass);
+        commands.add(getUsageArgument(toolScript));
+
+        getLog().info("Writing man page: " + manPage.getPath());
+        try {
+            // Tools tend to use System.exit() so run them as separate processes.
+            ProcessBuilder builder = new ProcessBuilder(commands);
+            Process process = builder.start();
+            writeToFile(process.getInputStream(), manPage);
+            process.waitFor();
+            final int result = process.exitValue();
+            if (result != 0) {
+                final StringBuilder message = new StringBuilder();
+                message.append("Failed to write page. Tool exit code: ").append(result).append(EOL)
+                        .append("To debug the problem, run the following command and connect your IDE:").append(EOL);
+                commands.add(1, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000");
+                for (String arg: commands) {
+                    // Surround with quotes to handle trailing sections.
+                    message.append("\"").append(arg).append("\"").append(' ');
+                }
+                message.append(EOL);
+                throw new MojoFailureException(message.toString());
+            }
+        }  catch (InterruptedException e) {
+            throw new MojoExecutionException(toolClass + " interrupted", e);
+        } catch (IOException e) {
+            throw new MojoExecutionException(toolClass + " not found", e);
+        }
+
+        if (tool.getName().equals("dsconfig")) {
+            try {
+                splitPage(manPage);
+            } catch (IOException e) {
+                throw new MojoExecutionException("Failed to split "  + manPage.getName(), e);
+            }
+        }
+    }
+
+    /**
+     * Returns true if the output directory is available.
+     * Attempts to create the directory if it does not exist.
+     * @return True if the output directory is available.
+     */
+    private boolean isOutputDirAvailable() {
+        return outputDir != null && (outputDir.exists() && outputDir.isDirectory() || outputDir.mkdirs());
+    }
+
+    /**
+     * Returns the Java args for running a tool.
+     * @param scriptName        The name of the tool.
+     * @param trailingSections  The man page sections to Xinclude.
+     * @return The Java args for running a tool.
+     */
+    private List<String> getJavaArgs(final String scriptName, final String trailingSections) {
+        List<String> args = new LinkedList<>();
+        args.add("-Dorg.forgerock.opendj.gendoc=true");
+        args.add("-Dorg.opends.server.ServerRoot=" + System.getProperty("java.io.tmpdir"));
+        args.add("-Dcom.forgerock.opendj.ldap.tools.scriptName=" + scriptName);
+        args.add("-Dorg.forgerock.opendj.gendoc.trailing=" + trailingSections + "");
+        return args;
+    }
+
+    /**
+     * Translates relative paths to XML files into XInclude elements.
+     *
+     * @param paths Paths to XInclude'd files, relative to the RefEntry.
+     * @return      String of XInclude elements corresponding to the paths.
+     */
+    private String pathsToXIncludes(final List<String> paths) {
+        if (paths == null) {
+            return "";
+        }
+
+        // Assume xmlns:xinclude="http://www.w3.org/2001/XInclude",
+        // as in the declaration of resources/templates/refEntry.ftl.
+        final String nameSpace = "xinclude";
+        final StringBuilder result = new StringBuilder();
+        for (String path : paths) {
+            result.append("<").append(nameSpace).append(":include href='").append(path).append("' />");
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns the usage argument.
+     * @param scriptName The name of the tool.
+     * @return The usage argument.
+     */
+    private String getUsageArgument(final String scriptName) {
+        return scriptName.equals("dsjavaproperties") ? "-H" : "-?";
+    }
+
+    /**
+     * Write the content of the input stream to the output file.
+     * @param input     The input stream to write.
+     * @param output    The file to write it to.
+     * @throws IOException  Failed to write the content of the input stream.
+     */
+    private void writeToFile(final InputStream input, final File output) throws IOException {
+        try (FileWriter writer = new FileWriter(output)) {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                writer.write(line);
+                writer.write(EOL);
+            }
+        }
+    }
+
+    /**
+     * Splits the content of a single man page into multiple pages.
+     * <br>
+     * RefEntry elements must be separated with a marker:
+     * {@code @@@scriptName + "-" + subCommand.getName() + @@@}.
+     *
+     * @param page          The page to split.
+     * @throws IOException  Failed to split the page.
+     */
+    private void splitPage(final File page) throws IOException {
+        // Read from a copy of the page.
+        final File pageCopy = new File(page.getPath() + ".tmp");
+        copyFile(page, pageCopy);
+        try (final BufferedReader reader = new BufferedReader(new FileReader(pageCopy))) {
+            // Write first to the page, then to pages named according to marker values.
+            File output = page;
+            getLog().info("Rewriting man page: " + page.getPath());
+            final Pattern marker = Pattern.compile("@@@(.+?)@@@");
+            final StringBuilder builder = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                final Matcher matcher = marker.matcher(line);
+                if (matcher.find()) {
+                    writeToFile(builder.toString(), output);
+                    builder.setLength(0);
+                    output = new File(page.getParentFile(), "man-" + matcher.group(1) + ".xml");
+                    getLog().info("Writing man page: " + output.getPath());
+                } else {
+                    builder.append(line).append(System.getProperty("line.separator"));
+                }
+            }
+            writeToFile(builder.toString(), output);
+            if (!pageCopy.delete()) {
+                throw new IOException("Failed to delete " +  pageCopy.getName());
+            }
+        }
+    }
+
+    /**
+     * Writes the content of the input to the output file.
+     * @param input         The UTF-8 input to write.
+     * @param output        The file to write it to.
+     * @throws IOException  Failed to write the content of the input.
+     */
+    private void writeToFile(final String input, final File output) throws IOException {
+        InputStream is = new ByteArrayInputStream(input.getBytes(Charset.forName("UTF-8")));
+        writeToFile(is, output);
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateResultCodeDocMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateResultCodeDocMojo.java
new file mode 100644
index 0000000..dfe0be0
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateResultCodeDocMojo.java
@@ -0,0 +1,174 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.forgerock.opendj.maven.doc.Utils.*;
+
+import com.thoughtworks.qdox.JavaProjectBuilder;
+import com.thoughtworks.qdox.model.JavaClass;
+import com.thoughtworks.qdox.model.JavaField;
+import com.thoughtworks.qdox.model.JavaType;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.forgerock.opendj.ldap.ResultCode;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generates documentation source for LDAP result codes based on
+ * {@code org.forgerock.opendj.ldap.ResultCode}.
+ * <br>
+ * This implementation parses the source to match Javadoc comments with result codes.
+ * It is assumed that the class's ResultCode fields are named with result code enum values,
+ * and that those fields have Javadoc comments describing each result code.
+ */
+@Mojo(name = "generate-result-code-doc", defaultPhase = LifecyclePhase.COMPILE)
+public class GenerateResultCodeDocMojo extends AbstractMojo {
+    /**
+     * The Java file containing the source of the ResultCode class,
+     * {@code org.forgerock.opendj.ldap.ResultCode}.
+     * <br>
+     * For example, {@code opendj-core/src/main/java/org/forgerock/opendj/ldap/ResultCode.java}.
+     */
+    @Parameter(required = true)
+    private File resultCodeSource;
+
+    /** The XML file to generate. */
+    @Parameter(required = true)
+    private File xmlFile;
+
+    /**
+     * Generates documentation source for LDAP result codes.
+     *
+     * @throws MojoExecutionException   Generation failed
+     * @throws MojoFailureException     Not used
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        final Map<String, Object> map = new HashMap<>();
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+
+        // The overall explanation in the generated doc is the class comment.
+        final JavaClass resultCodeClass;
+        try {
+            resultCodeClass = getJavaClass();
+        } catch (IOException e) {
+            throw new MojoExecutionException("Could not read " + resultCodeSource.getPath(), e);
+        }
+        map.put("classComment", cleanComment(resultCodeClass.getComment()));
+
+        // Documentation for each result code comes from the Javadoc for the code,
+        // and from the value and friendly name of the code.
+        final Map<String, Object> comments = new HashMap<>();
+        for (final JavaField field : resultCodeClass.getFields()) {
+            final JavaType type = field.getType();
+            if (type.getValue().equals("ResultCode")) {
+                comments.put(field.getName(), cleanComment(field.getComment()));
+            }
+        }
+        map.put("resultCodes", getResultCodesDoc(comments));
+
+        final String template = "appendix-ldap-result-codes.ftl";
+        try {
+            writeStringToFile(applyTemplate(template, map), xmlFile);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Could not write to " + xmlFile.getPath(), e);
+        }
+        getLog().info("Wrote " + xmlFile.getPath());
+    }
+
+    /**
+     * Returns an object to access to the result code Java source.
+     * @return An object to access to the result code Java source.
+     * @throws IOException  Could not read the source
+     */
+    private JavaClass getJavaClass() throws IOException {
+        final JavaProjectBuilder builder = new JavaProjectBuilder();
+        builder.addSource(resultCodeSource);
+        return builder.getClassByName("org.forgerock.opendj.ldap.ResultCode");
+    }
+
+    /**
+     * Returns a clean string for use in generated documentation.
+     * @param comment   The comment to clean.
+     * @return A clean string for use in generated documentation.
+     */
+    private String cleanComment(String comment) {
+        return stripCodeValueSentences(stripTags(convertLineSeparators(comment))).trim();
+    }
+
+    /**
+     * Returns a string with line separators converted to spaces.
+     * @param string    The string to convert.
+     * @return A string with line separators converted to spaces.
+     */
+    private String convertLineSeparators(String string) {
+        return string.replaceAll(System.lineSeparator(), " ");
+    }
+
+    /**
+     * Returns a string with the HTML tags removed.
+     * @param string    The string to strip.
+     * @return A string with the HTML tags removed.
+     */
+    private String stripTags(String string) {
+        return string.replaceAll("<[^>]*>", "");
+    }
+
+    /**
+     * Returns a string with lines sentences of the following form removed:
+     * This result code corresponds to the LDAP result code value of &#x7b;&#x40;code 0&#x7d;.
+     * @param string    The string to strip.
+     * @return A string with lines sentences of the matching form removed.
+     */
+    private String stripCodeValueSentences(String string) {
+        return string
+                .replaceAll("This result code corresponds to the LDAP result code value of \\{@code \\d+\\}.", "");
+    }
+
+    /**
+     * Returns a list of documentation objects for all result codes.
+     * @param comments  A map of field names to the clean comments.
+     * @return A list of documentation objects for all result codes.
+     */
+    private List<Map<String, Object>> getResultCodesDoc(Map<String, Object> comments) {
+        final List<Map<String, Object>> list = new LinkedList<>();
+        if (comments == null || comments.isEmpty()) {
+            return list;
+        }
+
+        for (ResultCode resultCode : ResultCode.values()) {
+            final Map<String, Object> doc = new HashMap<>();
+            doc.put("intValue", resultCode.intValue());
+            doc.put("name", resultCode.getName());
+            final Object comment = comments.get(resultCode.asEnum().toString());
+            doc.put("comment", comment);
+            list.add(doc);
+        }
+        return list;
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateSchemaDocMojo.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateSchemaDocMojo.java
new file mode 100644
index 0000000..1d65b3f
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateSchemaDocMojo.java
@@ -0,0 +1,252 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+import static org.forgerock.opendj.maven.doc.Utils.*;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.forgerock.opendj.ldap.schema.CoreSchemaSupportedLocales;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Generate schema-related reference documentation sources.
+ */
+@Mojo(name = "generate-schema-ref")
+public class GenerateSchemaDocMojo extends AbstractMojo {
+    /** The locale for which to generate the documentation. */
+    @Parameter(defaultValue = "en")
+    private String locale;
+
+    /** Output directory for source files. */
+    @Parameter(defaultValue = "${project.build.directory}/docbkx-sources/shared")
+    private File outputDirectory;
+
+    /**
+     * Writes schema reference documentation source files.
+     * @throws MojoExecutionException   Not used.
+     * @throws MojoFailureException     Failed to write a file.
+     */
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        final Locale currentLocale = getLocaleFromTag(locale);
+        final String localeReference = getLocalesAndSubTypesDocumentation(currentLocale);
+        final File localeReferenceFile = new File(outputDirectory, "sec-locales-subtypes.xml");
+        try {
+            writeStringToFile(localeReference, localeReferenceFile);
+        } catch (IOException e) {
+            throw new MojoExecutionException("Failed to write " + localeReferenceFile.getPath());
+        }
+    }
+
+    /**
+     * Returns a DocBook XML Section element documenting supported locales and language subtypes.
+     * @param currentLocale The locale for which to generate the documentation.
+     * @return A DocBook XML Section element documenting supported locales and language subtypes.
+     */
+    private String getLocalesAndSubTypesDocumentation(final Locale currentLocale) {
+        final Map<String, Object> map = new HashMap<>();
+        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
+        map.put("lang", getTagFromLocale(currentLocale));
+        map.put("title", DOC_LOCALE_SECTION_TITLE.get());
+        map.put("info", DOC_LOCALE_SECTION_INFO.get());
+        map.put("locales", getLocalesDocMap(currentLocale));
+        map.put("subtypes", getSubTypesDocMap(currentLocale));
+        return applyTemplate("sec-locales-subtypes.ftl", map);
+    }
+
+    private final Map<String, String> localeTagsToOids =
+            CoreSchemaSupportedLocales.getJvmSupportedLocaleNamesToOids();
+
+    /** Container for documentation regarding a locale. */
+    private class LocaleDoc {
+        String tag;
+        String language;
+        String oid;
+    }
+
+    /**
+     * Returns a map of languages to Locale documentation containers.
+     * @param currentLocale     The Locale of the resulting documentation.
+     * @return A map of languages to Locale documentation containers.
+     */
+    private Map<String, LocaleDoc> getLanguagesToLocalesMap(final Locale currentLocale) {
+        Map<String, LocaleDoc> locales = new TreeMap<>();
+        for (String tag : localeTagsToOids.keySet()) {
+            final Locale locale = getLocaleFromTag(tag);
+            if (locale == null) {
+                continue;
+            }
+            final LocaleDoc localeDoc = new LocaleDoc();
+            localeDoc.tag = tag;
+            localeDoc.language = locale.getDisplayName(currentLocale);
+            localeDoc.oid = localeTagsToOids.get(tag);
+            if (!localeDoc.language.equals(localeDoc.tag)) {
+                // No display language so must not be supported in current JVM
+                locales.put(localeDoc.language, localeDoc);
+            } else if (localeDoc.tag.equals("sh")) {
+                localeDoc.language = DOC_LANGUAGE_SH.get().toString(currentLocale);
+                locales.put(localeDoc.language, localeDoc);
+            }
+        }
+        return locales;
+    }
+
+    /**
+     * Returns a map of information for documenting supported locales.
+     * @param currentLocale The locale for which to generate the information.
+     * @return A map of information for documenting supported locales.
+     */
+    private Map<String, Object> getLocalesDocMap(final Locale currentLocale) {
+        final Map<String, Object> result = new HashMap<>();
+        result.put("title", DOC_SUPPORTED_LOCALES_TITLE.get());
+        result.put("indexTerm", DOC_SUPPORTED_LOCALES_INDEXTERM.get());
+        final Map<String, LocaleDoc> localesMap = getLanguagesToLocalesMap(currentLocale);
+        final Set<String> sortedLanguages = localesMap.keySet();
+        final List<Map<String, Object>> locales = new LinkedList<>();
+        for (final String language : sortedLanguages) {
+            final LocaleDoc locale = localesMap.get(language);
+            final Map<String, Object> map = new HashMap<>();
+            map.put("language", locale.language);
+            map.put("tag", DOC_LOCALE_TAG.get(locale.tag));
+            map.put("oid", DOC_LOCALE_OID.get(locale.oid));
+            locales.add(map);
+        }
+        result.put("locales", locales);
+        return result;
+    }
+
+    /**
+     * Returns a map of information for documenting supported language subtypes.
+     * @param currentLocale The locale for which to generate the information.
+     * @return A map of information for documenting supported language subtypes.
+     */
+    private Map<String, Object> getSubTypesDocMap(final Locale currentLocale) {
+        final Map<String, Object> result = new HashMap<>();
+        result.put("title", DOC_SUPPORTED_SUBTYPES_TITLE.get());
+        result.put("indexTerm", DOC_SUPPORTED_SUBTYPES_INDEXTERM.get());
+        final List<Map<String, Object>> locales = new LinkedList<>();
+        for (final String tag : localeTagsToOids.keySet()) {
+            final Map<String, Object> map = new HashMap<>();
+            int idx = tag.indexOf('-');
+            if (idx == -1) {
+                final Locale locale = getLocaleFromTag(tag);
+                if (locale == null) {
+                    continue;
+                }
+                final String language = locale.getDisplayName(currentLocale);
+                if (!language.equals(tag)) {
+                    map.put("language", language);
+                    map.put("tag", tag);
+                } else if (tag.equals("sh")) {
+                    map.put("language", DOC_LANGUAGE_SH.get().toString(currentLocale));
+                    map.put("tag", tag);
+                }
+                if (!map.isEmpty()) {
+                    locales.add(map);
+                }
+            }
+        }
+        result.put("locales", locales);
+        return result;
+    }
+
+    /**
+     * Returns the Locale based on the tag, or null if the tag is null.
+     * <br>
+     * Java 6 is missing {@code Locale.forLanguageTag()}.
+     * @param tag   The tag for the locale, such as {@code en_US}.
+     * @return The Locale based on the tag, or null if the tag is null.
+     */
+    private Locale getLocaleFromTag(final String tag) {
+        if (tag == null) {
+            return null;
+        }
+
+        // Apparently Locale tags can include not only languages and countries,
+        // but also variants, e.g. es_ES_Traditional_WIN.
+        // Pull these apart to be able to construct the locale.
+        //
+        // OpenDJ does not seem to have any locales with variants, but just in case...
+        // The separator in OpenDJ seems to be '-'.
+        // @see CoreSchemaSupportedLocales#LOCALE_NAMES_TO_OIDS
+        final char sep = '-';
+        int langIdx = tag.indexOf(sep);
+        final String lang;
+        if (langIdx == -1) {
+            // No country or variant
+            return new Locale(tag);
+        } else {
+            lang = tag.substring(0, langIdx);
+        }
+
+        int countryIdx = tag.indexOf(sep, langIdx + 1);
+        final String country;
+        if (countryIdx == -1) {
+            // No variant
+            country = tag.substring(langIdx + 1);
+            return new Locale(lang, country);
+        } else {
+            country = tag.substring(langIdx + 1, countryIdx);
+            final String variant = tag.substring(countryIdx + 1);
+            return new Locale(lang, country, variant);
+        }
+    }
+
+    /**
+     * Returns the tag based on the Locale, or null if the Locale is null.
+     * <br>
+     * Java 6 is missing {@code Locale.toLanguageTag()}.
+     * @param locale        The Locale for which to return the tag.
+     * @return The tag based on the Locale, or null if the Locale is null.
+     */
+    private String getTagFromLocale(final Locale locale) {
+        if (locale == null) {
+            return null;
+        }
+
+        final String  lang    = locale.getLanguage();
+        final String  country = locale.getCountry();
+        final String  variant = locale.getVariant();
+        final char    sep     = '-';
+        StringBuilder tag     = new StringBuilder();
+        if (lang != null) {
+            tag.append(lang);
+        }
+        if (country != null && !country.isEmpty()) {
+            tag.append(sep).append(country);
+        }
+        if (variant != null && !variant.isEmpty()) {
+            tag.append(sep).append(variant);
+        }
+        return tag.toString();
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java
new file mode 100644
index 0000000..be8a961
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/Utils.java
@@ -0,0 +1,239 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.maven.doc;
+
+import static org.forgerock.util.Utils.*;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateExceptionHandler;
+import org.apache.maven.artifact.DependencyResolutionRequiredException;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.project.MavenProject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides utility methods for generating documentation.
+ */
+public final class Utils {
+
+    /** Line separator. */
+    static final String EOL = System.getProperty("line.separator");
+
+    /**
+     * Creates a directory unless it already exists.
+     * @param directory     The directory to create.
+     * @throws IOException  Failed to create directory.
+     */
+    static void createDirectory(final String directory) throws IOException {
+        File dir = new File(directory);
+        if (!dir.exists()) {
+            if (!dir.mkdirs()) {
+                throw new IOException("Failed to create directory: " + directory);
+            }
+        }
+    }
+
+    /**
+     * Returns the path to the current Java executable.
+     * @return The path to the current Java executable.
+     */
+    static String getJavaCommand() {
+        return System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
+    }
+
+    /**
+     * Copies the content of the original file to the copy.
+     * @param original      The original file.
+     * @param copy          The copy.
+     * @throws IOException  Failed to make the copy.
+     */
+    static void copyFile(File original, File copy) throws IOException {
+        copyInputStreamToFile(new FileInputStream(original), copy);
+    }
+
+    /**
+     * Copies the content of the original input stream to the copy.
+     * @param original      The original input stream.
+     * @param copy          The copy.
+     * @throws IOException  Failed to make the copy.
+     */
+    static void copyInputStreamToFile(InputStream original, File copy) throws IOException {
+        if (original == null) {
+            throw new IOException("Could not read input to copy.");
+        }
+        createFile(copy);
+        try (OutputStream outputStream = new FileOutputStream(copy)) {
+            int bytesRead;
+            byte[] buffer = new byte[4096];
+            while ((bytesRead = original.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+        } finally {
+            closeSilently(original);
+        }
+    }
+
+    /**
+     * Writes a string to a file.
+     * @param string    The string to write
+     * @param file      The file to write to
+     * @throws IOException  The file did not exist, or could not be created for writing.
+     */
+    static void writeStringToFile(final String string, final File file) throws IOException {
+        createFile(file);
+        try (PrintWriter printWriter = new PrintWriter(file)) {
+            printWriter.print(string);
+        }
+    }
+
+    /**
+     * Creates a file including parent directories if it does not yet exist.
+     * @param file          The file to create
+     * @throws IOException  Failed to create the file
+     */
+    private static void createFile(File file) throws IOException {
+        if (!file.exists()) {
+            createDirectory(file.getParent());
+            if (!file.createNewFile()) {
+                throw new IOException("Failed to create " + file.getPath());
+            }
+        }
+    }
+
+    /**
+     * Returns the classpath for the class loader and its parent.
+     * @param classLoader   Contains the URLs of the class path to return.
+     * @return The classpath for the class loader and its parent.
+     */
+    static String getClassPath(URLClassLoader classLoader) throws URISyntaxException {
+        Set<URL> urls = new LinkedHashSet<>();
+        Collections.addAll(urls, classLoader.getURLs());
+        Collections.addAll(urls, ((URLClassLoader) classLoader.getParent()).getURLs());
+        Set<String> paths = new LinkedHashSet<>();
+        for (URL url: urls) {
+            paths.add(new File(url.toURI()).getPath());
+        }
+        return joinAsString(File.pathSeparator, paths);
+    }
+
+    /**
+     * Returns a ClassLoader including the project's runtime classpath elements.
+     * This is useful when running a Java command from inside a Maven plugin.
+     *
+     * @param project   The Maven project holding runtime classpath elements.
+     * @param log       A plugin log to use for debugging.
+     * @return A ClassLoader including the project's runtime classpath elements.
+     * @throws DependencyResolutionRequiredException    Failed to access the runtime classpath
+     * @throws MalformedURLException                    Failed to add an element to the classpath
+     */
+    static URLClassLoader getRuntimeClassLoader(MavenProject project, Log log)
+            throws DependencyResolutionRequiredException, MalformedURLException {
+        List<String> runtimeClasspathElements = project.getRuntimeClasspathElements();
+        Set<URL> runtimeUrls = new LinkedHashSet<>();
+        for (String element : runtimeClasspathElements) {
+            runtimeUrls.add(new File(element).toURI().toURL());
+        }
+
+        final URLClassLoader urlClassLoader = new URLClassLoader(
+                runtimeUrls.toArray(new URL[runtimeClasspathElements.size()]),
+                Thread.currentThread().getContextClassLoader());
+        debugClassPathElements(urlClassLoader, log);
+        return urlClassLoader;
+    }
+
+    /**
+     * Logs what is on the classpath for debugging.
+     * @param classLoader   The ClassLoader with the classpath.
+     * @param log           The Maven plugin log in which to write debug messages.
+     */
+    static void debugClassPathElements(ClassLoader classLoader, Log log) {
+        if (null == classLoader) {
+            return;
+        }
+        log.debug("--------------------");
+        log.debug(classLoader.toString());
+        if (classLoader instanceof URLClassLoader) {
+            final URLClassLoader ucl = (URLClassLoader) classLoader;
+            int i = 0;
+            for (URL url : ucl.getURLs()) {
+                log.debug("url[" + (i++) + "]=" + url);
+            }
+        }
+        debugClassPathElements(classLoader.getParent(), log);
+    }
+
+    /** FreeMarker template configuration. */
+    static Configuration configuration;
+
+    /**
+     * Returns a FreeMarker configuration for applying templates.
+     * @return A FreeMarker configuration for applying templates.
+     */
+    static Configuration getConfiguration() {
+        if (configuration == null) {
+            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+            configuration.setClassForTemplateLoading(Utils.class, "/templates");
+            configuration.setDefaultEncoding("UTF-8");
+            configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
+        }
+        return configuration;
+    }
+
+    /**
+     * Returns the String result from applying a FreeMarker template.
+     * @param template The name of a template file found in {@code resources/templates/}.
+     * @param map      The map holding the data to use in the template.
+     * @return The String result from applying a FreeMarker template.
+     */
+    static String applyTemplate(final String template, final Map<String, Object> map) {
+        // FreeMarker requires a configuration to find the template.
+        configuration = getConfiguration();
+
+        // FreeMarker takes the data and a Writer to process the template.
+        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                Writer writer = new OutputStreamWriter(outputStream)) {
+            Template configurationTemplate = configuration.getTemplate(template);
+            configurationTemplate.process(map, writer);
+            return outputStream.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    private Utils() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/package-info.java b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/package-info.java
new file mode 100644
index 0000000..0daf7e2
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+/**
+ * Helps to build generated documentation sources.
+ */
+package org.forgerock.opendj.maven.doc;
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/duration-syntax.html b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/duration-syntax.html
new file mode 100644
index 0000000..fbdaa0b
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/duration-syntax.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<title>OpenDJ - Duration Syntax</title>
+<link rel="stylesheet" type="text/css" href="opends-config.css">
+</head>
+<body>
+<div style="font-size:11px;margin-top:-10px;margin-bottom:-10px; text-align:right"><a href="index.html" target="_top">Configuration Reference Home</a></div>
+<h2>Duration Syntax</h2>
+<p>Durations are specified with positive integers and unit specifiers. Unit specifiers include the following.</p>
+<ul>
+<li><tt>ms</tt> - milliseconds</li>
+<li><tt>s</tt> - seconds</li>
+<li><tt>m</tt> - minutes</li>
+<li><tt>h</tt> - hours</li>
+<li><tt>d</tt> - days</li>
+<li><tt>w</tt> - weeks</li>
+</ul>
+<p>An duration of 1 week is specified as <tt>1w</tt>. A duration of 1 week, 1 day, 1 hour, 1 minute, and 1 second is specified as <tt>1w1d1h1m1s</tt>.</p>
+<p>The value -1 is reserved for unlimited durations. An unlimited duration is generally specified with the string <tt>unlimited</tt>.</p>
+<p>Not all properties taking a duration allow all unit specifiers. For example, milliseconds are not allowed if durations smaller than one second are not permitted.</p>
+<p>Some properties taking a duration also require minimum or maximum durations.</p>
+</body>
+</html>
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj-config.css b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj-config.css
new file mode 100644
index 0000000..3f02bba
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj-config.css
@@ -0,0 +1,101 @@
+body                     { font-family: Arial, Helvetica, sans-serif; 
+                           font-size: 12px;
+                           color: #333; background-color: #fff; margin:20px 15px }
+
+/* +++ 210 Page titles, headings, and paragraphs +++ */
+.pagename                { font-size: 1.7em; font-weight: normal;color:#626D75; 
+                           margin: 0.5em 0.5em 0.5em 0; }
+h1                       { font-size: 2.0em; font-weight: normal;
+	                         margin-top: 0em; margin-bottom: 0em; color:#626D75}
+h2                       { font-size: 1.6em; font-weight: normal; 
+                           margin: 0.5em 0em 1em 0em;
+						   
+	                        border-bottom: 1px #D9D9D9 solid; padding-bottom:5px; color:#626D75}
+h3                       { font-size: 1.4em; font-weight: bold;
+                           margin: 1.5em 0em .8em 0em;
+						   border-top: 1px #D9D9D9 solid; padding-top:10px;color:#626D75 }
+h4                       { font-size: 1.2em; font-weight: bold;color:#626D75;
+                           margin: 1.2em 0em .8em 0em; }
+h5                       { font-size: 1.0em; font-weight: bold;
+	                         margin: 1.2em 0em .8em 0em; }
+h6                       { font-size: 0.8em; font-weight: bold;
+	                         margin: 1.2em 0em .8em 0em; }
+p                        { margin: .75em 0 1em 0; }
+strong, b                { font-weight: bold;	}
+
+a:link                        { color: #47a; }
+a:visited                { color: #68a; } 
+
+
+/*ol                       { margin: 0.8em 0 0.8em 0.8em }
+ul                       { margin: 0.8em 0.2em } 
+li                       { margin: 0.6em 0.1em}
+li > p                   { margin-top: 0.2em }*/
+ol                       { list-style-type:decimal }
+ol ol                    { list-style-type:lower-alpha }
+ol ol ol                 { list-style-type:lower-roman }
+ol ol ol ol              { list-style-type:decimal }
+ol ol ol ol ol           { list-style-type:lower-alpha }
+ol ol ol ol ol ol        { list-style-type:lower-roman }
+ul ol                    { list-style-type:decimal }
+ul ol ol                 { list-style-type:lower-alpha }
+ul ol ol ol              { list-style-type:lower-roman }
+ul ol ol ol ol           { list-style-type:decimal }
+ul ol ol ol ol ol        { list-style-type:lower-alpha }
+ul ol ol ol ol ol ol     { list-style-type:lower-roman }
+ul                       { list-style:square }
+
+ol                       { margin-top:0.8em;margin-bottom:0.8em}
+ul                       { margin-top:0.8em;margin-bottom:0.8em } 
+li                       { margin-top:0.6em;margin-bottom:0.6em}
+li > p                   { margin-top: 0.2em }
+
+
+dl {margin-top:10px}
+dl dt                    { font-weight: bold;margin-bottom:3px }
+dl dd                    { margin-left: 10px; }
+
+
+.tabmenu                 {  font-size: 11px;margin: 0; margin-left: -15px;margin-top:29px;padding: 0.25em 0 0.25em 1em;
+                           border-bottom: 1px solid #8f989f;margin-bottom:10px; width:105% ; white-space:nowrap}
+.tabmenu span            { margin: 0; padding: 0; }
+.tabmenu span a          { color: #1A1A1A; background: #eee;
+                           margin: 0 0 0 -4px; padding: 4px 9px 2px;
+                           text-decoration: none; cursor: pointer;
+                           border: 1px solid gray; border-bottom:none;background-image:url(tab_deselected.jpg); }
+.tabmenu span a:hover      {text-decoration:underline; }
+.tabmenu span a:visited    {color: #1A1A1A; }
+
+
+.tabmenu .activetab      { color: #1A1A1A; 
+                           font-weight: normal;
+                           background-image:url(tab_selected.gif);background-color: #fff; cursor: default; border-bottom: 1px solid #fff; }
+
+.view-help {color:#333;; font-size:11px; margin-bottom:-10px}
+
+
+table               { border:none;border-bottom:dotted 1px #ccc;margin: 0px 0px 20px;font-size: 12px;}
+table tr th       { border:none;border-top: dotted 1px #ccc;padding:5px 10px;background-color:#f5f5f5;font-weight:bold;text-align:left;vertical-align:bottom; 
+	                       empty-cells: show;font-size: 12px; }
+table tr td       { border:none;border-top: dotted 1px #ccc;padding:4px 10px;vertical-align:top;
+	                       empty-cells: show;font-size: 12px; }
+
+.breadcrumb { background-color:#f5f5f5; color:#f5994f; position:absolute;top:0;left:0;right:0; padding:6px 15px;font-weight:normal;border-bottom:dotted 1px #ccc;width:100% }
+/*.breadcrumb a {color: #f5994f} */
+.pageactions a           { background-color: #f5f5f5;font-size:10px; border: 1px solid  #999;background-image:url(pageaction.gif);text-decoration:none; padding: 0.2em 0.5em;color:#333; }
+
+
+/*.titletable {border-top:none;border-bottom: 1px #D9D9D9 solid;margin-bottom:20px } */
+.titletable {border:none;margin-bottom:5px;margin-top:5px }
+.titletable h2 { font-size:2.1em;border:none; margin-bottom: .2em; margin-top:.4em }
+.titletable td { vertical-align:middle; border-top:none;padding-left:0px } 
+.propertyname {font-size: 12px; font-weight: bold;padding:3px 0px 3px 10px;margin: 0px;border-top: dotted 1px #ccc;background-color:#f5f5f5; }
+.alpha-index a {font-size:11px;margin-right:1px; font-weight:bold}
+.alpha-index span {font-size:11px;margin-right:1px; }
+.alpha-index {margin:20px 0px -10px; padding-bottom:0px}
+.category-index {margin:20px 0px -10px; padding-bottom:0px; font-size:11px}
+hr {border: 0; color: #eee; background-color: #eee; height: 1px; width: 100%; text-align: left;}
+.jump-table {border:none; }
+.jump-table tr td {border:none;padding:1px 20px 1px 20px;}
+.jump-table tr th { border:none;padding:1px 20px 3px 10px;font-weight: bold;text-align:left;vertical-align:bottom; 
+	                       empty-cells: show; background-color:#fff; color:#333}
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj_logo_sm.png b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj_logo_sm.png
new file mode 100644
index 0000000..cd82801
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/opendj_logo_sm.png
Binary files differ
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/pageaction.gif b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/pageaction.gif
new file mode 100644
index 0000000..db39ca7
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/pageaction.gif
Binary files differ
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_deselected.jpg b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_deselected.jpg
new file mode 100644
index 0000000..b7e9ed5
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_deselected.jpg
Binary files differ
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_selected.gif b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_selected.gif
new file mode 100644
index 0000000..9460e3f
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/config-ref/tab_selected.gif
Binary files differ
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/org/forgerock/opendj/maven/doc/docs.properties b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/org/forgerock/opendj/maven/doc/docs.properties
new file mode 100644
index 0000000..c7fd1d0
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/org/forgerock/opendj/maven/doc/docs.properties
@@ -0,0 +1,68 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2015 ForgeRock AS.
+
+#
+# Documentation messages
+#
+CATEGORY_ACCESS_CONTROL=Access Control.
+CATEGORY_ADMIN=the administration framework.
+CATEGORY_ADMIN_TOOL=the tool like the offline installer and uninstaller.
+CATEGORY_BACKEND=generic backends.
+CATEGORY_CONFIG=configuration handling.
+CATEGORY_CORE=the core server.
+CATEGORY_DSCONFIG=the dsconfig administration tool.
+CATEGORY_EXTENSIONS=server extensions (for example, extended operations, \
+  SASL mechanisms, password storage schemes, password validators, and so on).
+CATEGORY_JEB=the JE backend.
+CATEGORY_LOG=the server loggers.
+CATEGORY_PLUGIN=plugin processing.
+CATEGORY_PROTOCOL=connection and protocol handling (for example, ASN.1 and LDAP).
+CATEGORY_QUICKSETUP=quicksetup tools.
+CATEGORY_RUNTIME_INFORMATION=the runtime information.
+CATEGORY_SCHEMA=the server schema elements.
+CATEGORY_SYNC=replication.
+CATEGORY_TASK=tasks.
+CATEGORY_THIRD_PARTY=third-party (including user-defined) modules.
+CATEGORY_TOOLS=tools.
+CATEGORY_USER_DEFINED=user-defined modules.
+CATEGORY_UTIL=the general server utilities.
+CATEGORY_VERSION=version information.
+
+MESSAGE_CATEGORY=Log Message Category: %s
+MESSAGE_ORDINAL_ID=ID: %s
+MESSAGE_NO_ORDINAL=N/A
+MESSAGE_SEVERITY=Severity: %s
+MESSAGE_MESSAGE=Message: %s
+
+ERROR_SEVERITY_PRINTABLE=ERROR
+
+LOG_REF_TITLE=Log Message Reference
+LOG_REF_INDEXTERM=Logs
+LOG_REF_INTRO=<olink targetdoc="admin-guide" targetptr="logging" /> describes logs. \
+  Access and audit logs concern client operations               \
+  rather than OpenDJ directory server and tools,                \
+  and so are not listed here.                                   \
+  Instead, this appendix covers severe and fatal error messages \
+  for the directory server and its tools,                       \
+  such as those logged in                                       \
+  <filename>/path/to/opendj/logs/errors</filename>, and         \
+  <filename>/path/to/opendj/logs/replication</filename>.
+
+DOC_GLOBAL_ACIS_TABLE_TITLE=Default Global ACIs
+DOC_GLOBAL_ACIS_TABLE_SUMMARY=OpenDJ directory server defines \
+  the following global ACIs by default:
+DOC_GLOBAL_ACIS_NAME_COLUMN_TITLE=Name
+DOC_GLOBAL_ACIS_DESCRIPTION_COLUMN_TITLE=Description
+DOC_GLOBAL_ACIS_DEFINITION_COLUMN_TITLE=ACI Definition
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/appendix-ldap-result-codes.ftl b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/appendix-ldap-result-codes.ftl
new file mode 100644
index 0000000..158c532
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/appendix-ldap-result-codes.ftl
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright ${year} ForgeRock AS.
+-->
+<#-- Comment text comes from the Javadoc, so the language is English. -->
+<appendix xml:id="appendix-ldap-result-codes"
+          xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="en"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://docbook.org/ns/docbook
+                              http://docbook.org/xml/5.0/xsd/docbook.xsd"
+          xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>LDAP Result Codes</title>
+
+ <para>
+  ${classComment}
+ </para>
+
+ <indexterm>
+  <primary>LDAP</primary>
+  <secondary>Result codes</secondary>
+ </indexterm>
+
+ <table pgwide="1">
+  <title>OpenDJ LDAP Result Codes</title>
+  <tgroup cols="3">
+   <colspec colnum="1" colwidth="1*" />
+   <colspec colnum="2" colwidth="2*" />
+   <colspec colnum="3" colwidth="3*" />
+
+   <thead>
+    <row>
+     <entry>Result Code</entry>
+     <entry>Name</entry>
+     <entry>Description</entry>
+    </row>
+   </thead>
+
+   <tbody>
+    <#list resultCodes as resultCode>
+    <row valign="top">
+     <entry>
+      <para>
+       ${resultCode.intValue}
+      </para>
+     </entry>
+     <entry>
+      <para>
+       ${resultCode.name}
+      </para>
+     </entry>
+     <entry>
+      <para>
+       ${resultCode.comment}
+      </para>
+     </entry>
+    </row>
+    </#list>
+   </tbody>
+
+  </tgroup>
+ </table>
+</appendix>
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/log-message-reference.ftl b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/log-message-reference.ftl
new file mode 100644
index 0000000..8bdf55d
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/log-message-reference.ftl
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2012-${year} ForgeRock AS.
+-->
+<appendix xml:id="appendix-log-messages"
+          xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${lang}"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://docbook.org/ns/docbook
+                              http://docbook.org/xml/5.0/xsd/docbook.xsd"
+          xmlns:xlink="http://www.w3.org/1999/xlink"
+          xmlns:xinclude="http://www.w3.org/2001/XInclude">
+
+ <title>${title}</title>
+
+ <indexterm>
+  <primary>${indexterm}</primary>
+ </indexterm>
+
+ <para>
+  ${intro}
+ </para>
+
+ <#list categories as section>
+ <section xml:id="${section.id}">
+  <title>${section.category}</title>
+
+  <variablelist>
+  <#list section.entries as entry>
+   <varlistentry xml:id="log-ref-${entry.xmlId}">
+    <term>${entry.id}</term>
+    <listitem>
+     <para>
+      ${entry.severity}
+     </para>
+
+     <para>
+      ${entry.message?ensure_ends_with(".")}
+     </para>
+    </listitem>
+   </varlistentry>
+  </#list>
+  </variablelist>
+ </section>
+ </#list>
+</appendix>
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/sec-locales-subtypes.ftl b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/sec-locales-subtypes.ftl
new file mode 100644
index 0000000..1e3bf25
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/sec-locales-subtypes.ftl
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-${year} ForgeRock AS.
+-->
+<section xml:id="sec-locales-subtypes"
+         xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${lang}"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://docbook.org/ns/docbook
+                             http://docbook.org/xml/5.0/xsd/docbook.xsd">
+
+ <title>${title}</title>
+
+ <para>
+  ${info}
+ </para>
+
+ <variablelist xml:id="supported-locales">
+  <title>${locales.title}</title>
+  <indexterm><primary>${locales.indexTerm}</primary></indexterm>
+
+  <#list locales.locales as locale>
+  <varlistentry>
+   <term>${locale.language}</term>
+   <listitem>
+    <para>
+     ${locale.tag}
+    </para>
+
+    <para>
+     ${locale.oid}
+    </para>
+   </listitem>
+  </varlistentry>
+  </#list>
+
+ </variablelist>
+
+ <itemizedlist xml:id="supported-language-subtypes">
+  <title>${subtypes.title}</title>
+  <indexterm><primary>${subtypes.indexTerm}</primary></indexterm>
+
+  <#list subtypes.locales?sort_by("language") as subtype>
+  <listitem>
+   <para>${subtype.language}, ${subtype.tag}</para>
+  </listitem>
+  </#list>
+
+ </itemizedlist>
+
+</section>
diff --git a/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/table-global-acis.ftl b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/table-global-acis.ftl
new file mode 100644
index 0000000..95c540d
--- /dev/null
+++ b/opendj-sdk/opendj-doc-maven-plugin/src/main/resources/templates/table-global-acis.ftl
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright ${year} ForgeRock AS.
+-->
+<table xml:id="table-global-acis"
+       xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="${lang}"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://docbook.org/ns/docbook
+                           http://docbook.org/xml/5.0/xsd/docbook.xsd"
+       pgwide="1">
+ <title>${title}</title>
+
+ <textobject>
+  <para>
+   ${summary}
+  </para>
+ </textobject>
+
+ <tgroup cols="3">
+  <colspec colnum="1" colwidth="1*"/>
+  <colspec colnum="2" colwidth="2*" />
+  <colspec colnum="3" colwidth="2*" />
+
+  <thead>
+   <row>
+    <entry>${nameTitle}</entry>
+    <entry>${descTitle}</entry>
+    <entry>${defTitle}</entry>
+   </row>
+  </thead>
+
+  <tbody>
+   <#list acis?sort_by("name") as aci>
+   <row valign="top">
+    <entry>
+     <para>${aci.name}</para>        <!-- In English in config.ldif by default -->
+    </entry>
+    <entry>
+     <para>${aci.description}</para> <!-- In English in config.ldif by default -->
+    </entry>
+    <entry>
+     <para><literal>${aci.definition}</literal></para>
+    </entry>
+   </row>
+   </#list>
+  </tbody>
+ </tgroup>
+</table>
diff --git a/opendj-sdk/opendj-grizzly/pom.xml b/opendj-sdk/opendj-grizzly/pom.xml
new file mode 100644
index 0000000..d684b16
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/pom.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2013-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-grizzly</artifactId>
+    <name>OpenDJ Grizzly Transport Provider</name>
+    <description>This module includes a Grizzly based network transport provider for OpenDJ.</description>
+
+    <packaging>bundle</packaging>
+
+    <properties>
+        <opendj.osgi.import.additional>
+            org.forgerock.opendj.*;provide:=true
+        </opendj.osgi.import.additional>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.grizzly</groupId>
+            <artifactId>grizzly-framework</artifactId>
+            <version>${grizzly-framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock</groupId>
+            <artifactId>forgerock-build-tools</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate-messages</goal>
+                        </goals>
+                        <configuration>
+                            <messageFiles>
+                                <messageFile>com/forgerock/opendj/grizzly/grizzly.properties</messageFile>
+                            </messageFiles>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <!-- Export only public APIs of this module-->
+                        <Export-Package>
+                            org.forgerock.opendj.grizzly*
+                        </Export-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <reportSets>
+                    <reportSet>
+                        <reports>
+                            <report>dependencies</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <links>
+                        <link>http://commons.forgerock.org/i18n-framework/i18n-core/apidocs</link>
+                    </links>
+                </configuration>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/GrizzlyTransportProvider.java b/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/GrizzlyTransportProvider.java
new file mode 100644
index 0000000..bde6ddc
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/GrizzlyTransportProvider.java
@@ -0,0 +1,52 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.grizzly.GrizzlyLDAPConnectionFactory;
+import org.forgerock.opendj.grizzly.GrizzlyLDAPListener;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPListenerImpl;
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.Options;
+
+/**
+ * Grizzly transport provider implementation.
+ */
+public class GrizzlyTransportProvider implements TransportProvider {
+
+    @Override
+    public LDAPConnectionFactoryImpl getLDAPConnectionFactory(String host, int port, Options options) {
+        return new GrizzlyLDAPConnectionFactory(host, port, options);
+    }
+
+    @Override
+    public LDAPListenerImpl getLDAPListener(InetSocketAddress address,
+            ServerConnectionFactory<LDAPClientContext, Integer> factory, Options options)
+            throws IOException {
+        return new GrizzlyLDAPListener(address, factory, options);
+    }
+
+    @Override
+    public String getName() {
+        return "Grizzly";
+    }
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/package-info.java b/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/package-info.java
new file mode 100644
index 0000000..a788349
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/com/forgerock/opendj/grizzly/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+/**
+ * Classes implementing Grizzly transport provider.
+ */
+package com.forgerock.opendj.grizzly;
+
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferReader.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferReader.java
new file mode 100644
index 0000000..1226bde
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferReader.java
@@ -0,0 +1,572 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.AbstractASN1Reader;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.memory.BuffersBuffer;
+import org.glassfish.grizzly.memory.CompositeBuffer;
+import org.glassfish.grizzly.memory.MemoryManager;
+
+/** Grizzly ASN1 reader implementation. */
+final class ASN1BufferReader extends AbstractASN1Reader {
+    private final class ChildSequenceLimiter implements SequenceLimiter {
+        private SequenceLimiter parent;
+        private ChildSequenceLimiter child;
+        private int readLimit;
+        private int bytesRead;
+
+        @Override
+        public void checkLimit(final int readSize) throws IOException {
+            if (0 < readLimit && readLimit < bytesRead + readSize) {
+                final LocalizableMessage message = ERR_ASN1_TRUNCATED_LENGTH_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+            parent.checkLimit(readSize);
+            bytesRead += readSize;
+        }
+
+        @Override
+        public SequenceLimiter endSequence() throws IOException {
+            parent.checkLimit(remaining());
+            if (remaining() > 0) {
+                logger.debug(LocalizableMessage.raw(
+                    "Ignoring %d unused trailing bytes in ASN.1 SEQUENCE", remaining()));
+            }
+            for (int i = 0; i < remaining(); i++) {
+                buffer.get();
+            }
+            return parent;
+        }
+
+        @Override
+        public int remaining() {
+            return readLimit - bytesRead;
+        }
+
+        @Override
+        public ChildSequenceLimiter startSequence(final int readLimit) {
+            if (child == null) {
+                child = new ChildSequenceLimiter();
+                child.parent = this;
+            }
+            child.readLimit = readLimit;
+            child.bytesRead = 0;
+            return child;
+        }
+    }
+
+    private final class RootSequenceLimiter implements SequenceLimiter {
+        private ChildSequenceLimiter child;
+
+        @Override
+        public void checkLimit(final int readSize) throws IOException {
+            if (buffer.remaining() < readSize) {
+                final LocalizableMessage message = ERR_ASN1_TRUNCATED_LENGTH_BYTE.get();
+                throw DecodeException.fatalError(message);
+            }
+        }
+
+        @Override
+        public ChildSequenceLimiter endSequence() throws DecodeException {
+            final LocalizableMessage message = ERR_ASN1_SEQUENCE_READ_NOT_STARTED.get();
+            throw new IllegalStateException(message.toString());
+        }
+
+        @Override
+        public int remaining() {
+            return buffer.remaining();
+        }
+
+        @Override
+        public ChildSequenceLimiter startSequence(final int readLimit) {
+            if (child == null) {
+                child = new ChildSequenceLimiter();
+                child.parent = this;
+            }
+            child.readLimit = readLimit;
+            child.bytesRead = 0;
+            return child;
+        }
+    }
+
+    private interface SequenceLimiter {
+        void checkLimit(int readSize) throws IOException;
+
+        SequenceLimiter endSequence() throws IOException;
+
+        int remaining();
+
+        SequenceLimiter startSequence(int readLimit);
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    private static final int MAX_STRING_BUFFER_SIZE = 1024;
+    private int state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    private byte peekType;
+    private int peekLength = -1;
+    private int lengthBytesNeeded;
+    private final int maxElementSize;
+    private final CompositeBuffer buffer;
+    private SequenceLimiter readLimiter;
+    private final byte[] stringBuffer;
+
+    /**
+     * Creates a new ASN1 reader whose source is the provided input stream and
+     * having a user defined maximum BER element size.
+     *
+     * @param maxElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     * @param memoryManager
+     *            The memory manager to use for buffering.
+     */
+    ASN1BufferReader(final int maxElementSize, final MemoryManager<?> memoryManager) {
+        this.readLimiter = new RootSequenceLimiter();
+        this.stringBuffer = new byte[MAX_STRING_BUFFER_SIZE];
+        this.maxElementSize = maxElementSize;
+        this.buffer = BuffersBuffer.create(memoryManager);
+    }
+
+    /**
+     * Closes this ASN.1 reader and the underlying stream.
+     *
+     * @throws IOException
+     *             if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        buffer.dispose();
+    }
+
+    /**
+     * Determines if a complete ASN.1 element is ready to be read from the
+     * stream reader.
+     *
+     * @return <code>true</code> if another complete element is available or
+     *         <code>false</code> otherwise.
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    @Override
+    public boolean elementAvailable() throws IOException {
+        return (state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(true))
+                && (state != ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE || needFirstLengthByteState(true))
+                && (state != ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES
+                    || needAdditionalLengthBytesState(true))
+                && peekLength <= readLimiter.remaining();
+    }
+
+    /**
+     * Determines if the input stream contains at least one ASN.1 element to be
+     * read.
+     *
+     * @return <code>true</code> if another element is available or
+     *         <code>false</code> otherwise.
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    @Override
+    public boolean hasNextElement() throws IOException {
+        return state != ASN1.ELEMENT_READ_STATE_NEED_TYPE || needTypeState(true);
+    }
+
+    @Override
+    public int peekLength() throws IOException {
+        peekType();
+
+        switch (state) {
+        case ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE:
+            needFirstLengthByteState(false);
+            break;
+
+        case ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES:
+            needAdditionalLengthBytesState(false);
+        }
+
+        return peekLength;
+    }
+
+    @Override
+    public byte peekType() throws IOException {
+        if (state == ASN1.ELEMENT_READ_STATE_NEED_TYPE) {
+            needTypeState(false);
+        }
+
+        return peekType;
+    }
+
+    @Override
+    public boolean readBoolean() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength != 1) {
+            final LocalizableMessage message = ERR_ASN1_BOOLEAN_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        readLimiter.checkLimit(peekLength);
+        final byte readByte = buffer.get();
+
+        logger.trace("READ ASN.1 BOOLEAN(type=0x%x, length=%d, value=%s)", peekType, peekLength, readByte != 0x00);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return readByte != 0x00;
+    }
+
+    @Override
+    public void readEndSequence() throws IOException {
+        readLimiter = readLimiter.endSequence();
+
+        logger.trace("READ ASN.1 END SEQUENCE");
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readEndExplicitTag() throws DecodeException, IOException {
+        readEndSequence();
+    }
+
+    @Override
+    public void readEndSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readEndSequence();
+    }
+
+    @Override
+    public int readEnumerated() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 4) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        // From an implementation point of view, an enumerated value is
+        // equivalent to an integer.
+        return (int) readInteger();
+    }
+
+    @Override
+    public long readInteger() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength < 1 || peekLength > 8) {
+            final LocalizableMessage message = ERR_ASN1_INTEGER_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        readLimiter.checkLimit(peekLength);
+        if (peekLength > 4) {
+            long longValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = buffer.get();
+                if (i == 0 && ((byte) readByte) < 0) {
+                    longValue = 0xFFFFFFFFFFFFFFFFL;
+                }
+                longValue = (longValue << 8) | (readByte & 0xFF);
+            }
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return longValue;
+        } else {
+            int intValue = 0;
+            for (int i = 0; i < peekLength; i++) {
+                final int readByte = buffer.get();
+                if (i == 0 && ((byte) readByte) < 0) {
+                    intValue = 0xFFFFFFFF;
+                }
+                intValue = (intValue << 8) | (readByte & 0xFF);
+            }
+
+            logger.trace("READ ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", peekType, peekLength, intValue);
+
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return intValue;
+        }
+    }
+
+    @Override
+    public void readNull() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        // Make sure that the decoded length is exactly zero byte.
+        if (peekLength != 0) {
+            final LocalizableMessage message = ERR_ASN1_NULL_INVALID_LENGTH.get(peekLength);
+            throw DecodeException.fatalError(message);
+        }
+
+        logger.trace("READ ASN.1 NULL(type=0x%x, length=%d)", peekType, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public ByteString readOctetString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return ByteString.empty();
+        }
+
+        readLimiter.checkLimit(peekLength);
+        // Copy the value and construct the element to return.
+        final byte[] value = new byte[peekLength];
+        buffer.get(value);
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d)", peekType, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return ByteString.wrap(value);
+    }
+
+    @Override
+    public ByteStringBuilder readOctetString(final ByteStringBuilder builder) throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return builder;
+        }
+
+        readLimiter.checkLimit(peekLength);
+        // Copy the value and construct the element to return.
+        // TODO: Is there a more efficient way to do this?
+        for (int i = 0; i < peekLength; i++) {
+            builder.appendByte(buffer.get());
+        }
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d)", peekType, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return builder;
+    }
+
+    @Override
+    public String readOctetStringAsString() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        if (peekLength == 0) {
+            state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+            return "";
+        }
+
+        byte[] readBuffer;
+        if (peekLength <= stringBuffer.length) {
+            readBuffer = stringBuffer;
+        } else {
+            readBuffer = new byte[peekLength];
+        }
+
+        readLimiter.checkLimit(peekLength);
+        buffer.get(readBuffer, 0, peekLength);
+
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+
+        String str;
+        try {
+            str = new String(readBuffer, 0, peekLength, "UTF-8");
+        } catch (final Exception e) {
+            // TODO: I18N
+            logger.warn(LocalizableMessage.raw("Unable to decode ASN.1 OCTETSTRING bytes as UTF-8 string: %s", e));
+
+            str = new String(stringBuffer, 0, peekLength);
+        }
+
+        logger.trace("READ ASN.1 OCTETSTRING(type=0x%x, length=%d, value=%s)", peekType, peekLength, str);
+
+        return str;
+    }
+
+    @Override
+    public void readStartSequence() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        readLimiter = readLimiter.startSequence(peekLength);
+
+        logger.trace("READ ASN.1 START SEQUENCE(type=0x%x, length=%d)", peekType, peekLength);
+
+        // Reset the state
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+    }
+
+    @Override
+    public void readStartExplicitTag() throws DecodeException, IOException {
+        readStartSequence();
+    }
+
+    @Override
+    public void readStartSet() throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        readStartSequence();
+    }
+
+    @Override
+    public ASN1Reader skipElement() throws IOException {
+        // Read the header if haven't done so already
+        peekLength();
+
+        readLimiter.checkLimit(peekLength);
+        for (int i = 0; i < peekLength; i++) {
+            buffer.get();
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_TYPE;
+        return this;
+    }
+
+    void appendBytesRead(final Buffer buffer) {
+        this.buffer.append(buffer);
+    }
+
+    void disposeBytesRead() {
+        this.buffer.shrink();
+    }
+
+    /**
+     * Internal helper method reading the additional ASN.1 length bytes and
+     * transition to the next state if successful.
+     *
+     * @param ensureRead
+     *            <code>true</code> to check for availability first.
+     * @return <code>true</code> if the length bytes was successfully read.
+     * @throws IOException
+     *             If an error occurs while reading from the stream.
+     */
+    private boolean needAdditionalLengthBytesState(final boolean ensureRead) throws IOException {
+        if (ensureRead && (readLimiter.remaining() < lengthBytesNeeded)) {
+            return false;
+        }
+
+        byte readByte;
+        readLimiter.checkLimit(lengthBytesNeeded);
+        while (lengthBytesNeeded > 0) {
+            readByte = buffer.get();
+            peekLength = (peekLength << 8) | (readByte & 0xFF);
+            lengthBytesNeeded--;
+        }
+
+        // Make sure that the element is not larger than the maximum allowed
+        // message size.
+        if (maxElementSize > 0 && peekLength > maxElementSize) {
+            final LocalizableMessage m =
+                    ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED
+                            .get(peekLength, maxElementSize);
+            throw DecodeException.fatalError(m);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES;
+        return true;
+    }
+
+    /**
+     * Internal helper method reading the first length bytes and transition to
+     * the next state if successful.
+     *
+     * @param ensureRead
+     *            <code>true</code> to check for availability first.
+     * @return <code>true</code> if the length bytes was successfully read
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    private boolean needFirstLengthByteState(final boolean ensureRead) throws IOException {
+        if (ensureRead && (readLimiter.remaining() <= 0)) {
+            return false;
+        }
+
+        readLimiter.checkLimit(1);
+        byte readByte = buffer.get();
+        peekLength = (readByte & 0x7F);
+        if (peekLength != readByte) {
+            lengthBytesNeeded = peekLength;
+            if (lengthBytesNeeded > 4) {
+                final LocalizableMessage message =
+                        ERR_ASN1_INVALID_NUM_LENGTH_BYTES.get(lengthBytesNeeded);
+                throw DecodeException.fatalError(message);
+            }
+            peekLength = 0x00;
+
+            if (ensureRead && (readLimiter.remaining() < lengthBytesNeeded)) {
+                state = ASN1.ELEMENT_READ_STATE_NEED_ADDITIONAL_LENGTH_BYTES;
+                return false;
+            }
+
+            readLimiter.checkLimit(lengthBytesNeeded);
+            while (lengthBytesNeeded > 0) {
+                readByte = buffer.get();
+                peekLength = (peekLength << 8) | (readByte & 0xFF);
+                lengthBytesNeeded--;
+            }
+        }
+
+        // Make sure that the element is not larger than the maximum allowed
+        // message size.
+        if (maxElementSize > 0 && peekLength > maxElementSize) {
+            final LocalizableMessage m =
+                    ERR_LDAP_CLIENT_DECODE_MAX_REQUEST_SIZE_EXCEEDED
+                            .get(peekLength, maxElementSize);
+            throw DecodeException.fatalError(m);
+        }
+        state = ASN1.ELEMENT_READ_STATE_NEED_VALUE_BYTES;
+        return true;
+    }
+
+    /**
+     * Internal helper method reading the ASN.1 type byte and transition to the
+     * next state if successful.
+     *
+     * @param ensureRead
+     *            <code>true</code> to check for availability first.
+     * @return <code>true</code> if the type byte was successfully read
+     * @throws IOException
+     *             If an error occurs while trying to decode an ASN1 element.
+     */
+    private boolean needTypeState(final boolean ensureRead) throws IOException {
+        // Read just the type.
+        if (ensureRead && readLimiter.remaining() <= 0) {
+            return false;
+        }
+
+        readLimiter.checkLimit(1);
+        peekType = buffer.get();
+        state = ASN1.ELEMENT_READ_STATE_NEED_FIRST_LENGTH_BYTE;
+        return true;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferWriter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferWriter.java
new file mode 100644
index 0000000..c50e864
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ASN1BufferWriter.java
@@ -0,0 +1,445 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.io.AbstractASN1Writer;
+import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.Cacheable;
+import org.glassfish.grizzly.memory.ByteBufferWrapper;
+
+import com.forgerock.opendj.util.StaticUtils;
+
+/** Grizzly ASN1 writer implementation. */
+final class ASN1BufferWriter extends AbstractASN1Writer implements Cacheable {
+    private class ChildSequenceBuffer implements SequenceBuffer {
+        private SequenceBuffer parent;
+        private ChildSequenceBuffer child;
+        private final ByteStringBuilder buffer = new ByteStringBuilder(BUFFER_INIT_SIZE);
+
+        @Override
+        public SequenceBuffer endSequence() throws IOException {
+            writeLength(parent, buffer.length());
+            parent.writeByteArray(buffer.getBackingArray(), 0, buffer.length());
+            buffer.clearAndTruncate(DEFAULT_MAX_INTERNAL_BUFFER_SIZE, BUFFER_INIT_SIZE);
+            logger.trace("WRITE ASN.1 END SEQUENCE(length=%d)", buffer.length());
+            return parent;
+        }
+
+        @Override
+        public SequenceBuffer startSequence(final byte type) throws IOException {
+            if (child == null) {
+                child = new ChildSequenceBuffer();
+                child.parent = this;
+            }
+            buffer.appendByte(type);
+            child.buffer.clear();
+            return child;
+        }
+
+        @Override
+        public void writeByte(final byte b) throws IOException {
+            buffer.appendByte(b);
+        }
+
+        @Override
+        public void writeByteArray(final byte[] bs, final int offset, final int length) throws IOException {
+            buffer.appendBytes(bs, offset, length);
+        }
+    }
+
+    private static final class RecyclableBuffer extends ByteBufferWrapper {
+        private volatile boolean usable = true;
+
+        private RecyclableBuffer() {
+            visible = ByteBuffer.allocate(BUFFER_INIT_SIZE);
+            allowBufferDispose = true;
+        }
+
+        @Override
+        public void dispose() {
+            usable = true;
+        }
+
+        /**
+         * Ensures that the specified number of additional bytes will fit in the
+         * buffer and resizes it if necessary.
+         *
+         * @param size
+         *            The number of additional bytes.
+         */
+        public void ensureAdditionalCapacity(final int size) {
+            final int newCount = visible.position() + size;
+            if (newCount > visible.capacity()) {
+                final ByteBuffer newByteBuffer =
+                        ByteBuffer.allocate(Math.max(visible.capacity() << 1, newCount));
+                visible.flip();
+                visible = newByteBuffer.put(visible);
+            }
+        }
+    }
+
+    private class RootSequenceBuffer implements SequenceBuffer {
+        private ChildSequenceBuffer child;
+
+        @Override
+        public SequenceBuffer endSequence() throws IOException {
+            final LocalizableMessage message = ERR_ASN1_SEQUENCE_WRITE_NOT_STARTED.get();
+            throw new IllegalStateException(message.toString());
+        }
+
+        @Override
+        public SequenceBuffer startSequence(final byte type) throws IOException {
+            if (child == null) {
+                child = new ChildSequenceBuffer();
+                child.parent = this;
+            }
+            outBuffer.ensureAdditionalCapacity(1);
+            outBuffer.put(type);
+            child.buffer.clear();
+            return child;
+        }
+
+        @Override
+        public void writeByte(final byte b) throws IOException {
+            outBuffer.ensureAdditionalCapacity(1);
+            outBuffer.put(b);
+        }
+
+        @Override
+        public void writeByteArray(final byte[] bs, final int offset, final int length)
+                throws IOException {
+            outBuffer.ensureAdditionalCapacity(length);
+            outBuffer.put(bs, offset, length);
+        }
+    }
+
+    private interface SequenceBuffer {
+        SequenceBuffer endSequence() throws IOException;
+
+        SequenceBuffer startSequence(byte type) throws IOException;
+
+        void writeByte(byte b) throws IOException;
+
+        void writeByteArray(byte[] bs, int offset, int length) throws IOException;
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** Initial size of newly created buffers. */
+    private static final int BUFFER_INIT_SIZE = 1024;
+    /** Default maximum size for cached protocol/entry encoding buffers. */
+    private static final int DEFAULT_MAX_INTERNAL_BUFFER_SIZE = 32 * 1024;
+
+    /** Reset the writer. */
+    void reset() {
+        if (!outBuffer.usable) {
+            // If the output buffer is unusable, create a new one.
+            outBuffer = new RecyclableBuffer();
+        }
+        outBuffer.clear();
+    }
+
+    private SequenceBuffer sequenceBuffer;
+    private RecyclableBuffer outBuffer;
+    private final RootSequenceBuffer rootBuffer;
+
+    /** Creates a new ASN.1 writer that writes to a StreamWriter. */
+    ASN1BufferWriter() {
+        this.sequenceBuffer = this.rootBuffer = new RootSequenceBuffer();
+        this.outBuffer = new RecyclableBuffer();
+    }
+
+    /**
+     * Closes this ASN.1 writer and the underlying outputstream. Any unfinished
+     * sequences will be ended.
+     *
+     * @throws IOException
+     *             if an error occurs while closing the stream.
+     */
+    @Override
+    public void close() throws IOException {
+        outBuffer = null;
+    }
+
+    /**
+     * Flushes the stream.
+     *
+     * @throws IOException
+     *             If an I/O error occurs
+     */
+    @Override
+    public void flush() throws IOException {
+        // Do nothing
+    }
+
+    /** Recycle the writer to allow re-use. */
+    @Override
+    public void recycle() {
+        sequenceBuffer = rootBuffer;
+        outBuffer.clear();
+    }
+
+    @Override
+    public ASN1Writer writeBoolean(final byte type, final boolean booleanValue) throws IOException {
+        sequenceBuffer.writeByte(type);
+        writeLength(sequenceBuffer, 1);
+        sequenceBuffer.writeByte(booleanValue ? ASN1.BOOLEAN_VALUE_TRUE : ASN1.BOOLEAN_VALUE_FALSE);
+
+        logger.trace("WRITE ASN.1 BOOLEAN(type=0x%x, length=%d, value=%s)", type, 1, booleanValue);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeEndSequence() throws IOException {
+        sequenceBuffer = sequenceBuffer.endSequence();
+
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeEndSet() throws IOException {
+        return writeEndSequence();
+    }
+
+    @Override
+    public ASN1Writer writeEnumerated(final byte type, final int intValue) throws IOException {
+        return writeInteger(type, intValue);
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final byte type, final int intValue) throws IOException {
+        sequenceBuffer.writeByte(type);
+        if (((intValue < 0) && ((intValue & 0xFFFFFF80) == 0xFFFFFF80))
+                || ((intValue & 0x0000007F) == intValue)) {
+            writeLength(sequenceBuffer, 1);
+            sequenceBuffer.writeByte((byte) intValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 1, intValue);
+        } else if (((intValue < 0) && ((intValue & 0xFFFF8000) == 0xFFFF8000))
+                || ((intValue & 0x00007FFF) == intValue)) {
+            writeLength(sequenceBuffer, 2);
+            sequenceBuffer.writeByte((byte) (intValue >> 8));
+            sequenceBuffer.writeByte((byte) intValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 2, intValue);
+        } else if (((intValue < 0) && ((intValue & 0xFF800000) == 0xFF800000))
+                || ((intValue & 0x007FFFFF) == intValue)) {
+            writeLength(sequenceBuffer, 3);
+            sequenceBuffer.writeByte((byte) (intValue >> 16));
+            sequenceBuffer.writeByte((byte) (intValue >> 8));
+            sequenceBuffer.writeByte((byte) intValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 3, intValue);
+        } else {
+            writeLength(sequenceBuffer, 4);
+            sequenceBuffer.writeByte((byte) (intValue >> 24));
+            sequenceBuffer.writeByte((byte) (intValue >> 16));
+            sequenceBuffer.writeByte((byte) (intValue >> 8));
+            sequenceBuffer.writeByte((byte) intValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 4, intValue);
+        }
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeInteger(final byte type, final long longValue) throws IOException {
+        sequenceBuffer.writeByte(type);
+        if (((longValue < 0) && ((longValue & 0xFFFFFFFFFFFFFF80L) == 0xFFFFFFFFFFFFFF80L))
+                || ((longValue & 0x000000000000007FL) == longValue)) {
+            writeLength(sequenceBuffer, 1);
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 1, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFFFFFF8000L) == 0xFFFFFFFFFFFF8000L))
+                || ((longValue & 0x0000000000007FFFL) == longValue)) {
+            writeLength(sequenceBuffer, 2);
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 2, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFFFF800000L) == 0xFFFFFFFFFF800000L))
+                || ((longValue & 0x00000000007FFFFFL) == longValue)) {
+            writeLength(sequenceBuffer, 3);
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 3, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFFFF80000000L) == 0xFFFFFFFF80000000L))
+                || ((longValue & 0x000000007FFFFFFFL) == longValue)) {
+            writeLength(sequenceBuffer, 4);
+            sequenceBuffer.writeByte((byte) (longValue >> 24));
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 4, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFFFF8000000000L) == 0xFFFFFF8000000000L))
+                || ((longValue & 0x0000007FFFFFFFFFL) == longValue)) {
+            writeLength(sequenceBuffer, 5);
+            sequenceBuffer.writeByte((byte) (longValue >> 32));
+            sequenceBuffer.writeByte((byte) (longValue >> 24));
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 5, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFFFF800000000000L) == 0xFFFF800000000000L))
+                || ((longValue & 0x00007FFFFFFFFFFFL) == longValue)) {
+            writeLength(sequenceBuffer, 6);
+            sequenceBuffer.writeByte((byte) (longValue >> 40));
+            sequenceBuffer.writeByte((byte) (longValue >> 32));
+            sequenceBuffer.writeByte((byte) (longValue >> 24));
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 6, longValue);
+        } else if (((longValue < 0) && ((longValue & 0xFF80000000000000L) == 0xFF80000000000000L))
+                || ((longValue & 0x007FFFFFFFFFFFFFL) == longValue)) {
+            writeLength(sequenceBuffer, 7);
+            sequenceBuffer.writeByte((byte) (longValue >> 48));
+            sequenceBuffer.writeByte((byte) (longValue >> 40));
+            sequenceBuffer.writeByte((byte) (longValue >> 32));
+            sequenceBuffer.writeByte((byte) (longValue >> 24));
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 7, longValue);
+        } else {
+            writeLength(sequenceBuffer, 8);
+            sequenceBuffer.writeByte((byte) (longValue >> 56));
+            sequenceBuffer.writeByte((byte) (longValue >> 48));
+            sequenceBuffer.writeByte((byte) (longValue >> 40));
+            sequenceBuffer.writeByte((byte) (longValue >> 32));
+            sequenceBuffer.writeByte((byte) (longValue >> 24));
+            sequenceBuffer.writeByte((byte) (longValue >> 16));
+            sequenceBuffer.writeByte((byte) (longValue >> 8));
+            sequenceBuffer.writeByte((byte) longValue);
+            logger.trace("WRITE ASN.1 INTEGER(type=0x%x, length=%d, value=%d)", type, 8, longValue);
+        }
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeNull(final byte type) throws IOException {
+        sequenceBuffer.writeByte(type);
+        writeLength(sequenceBuffer, 0);
+
+        logger.trace("WRITE ASN.1 NULL(type=0x%x, length=%d)", type, 0);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final byte[] value, final int offset,
+            final int length) throws IOException {
+        sequenceBuffer.writeByte(type);
+        writeLength(sequenceBuffer, length);
+        sequenceBuffer.writeByteArray(value, offset, length);
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d)", type, length);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final ByteSequence value)
+            throws IOException {
+        sequenceBuffer.writeByte(type);
+        writeLength(sequenceBuffer, value.length());
+        // TODO: Is there a more efficient way to do this?
+        for (int i = 0; i < value.length(); i++) {
+            sequenceBuffer.writeByte(value.byteAt(i));
+        }
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d)", type, value.length());
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeOctetString(final byte type, final String value) throws IOException {
+        sequenceBuffer.writeByte(type);
+
+        if (value == null) {
+            writeLength(sequenceBuffer, 0);
+            return this;
+        }
+
+        final byte[] bytes = StaticUtils.getBytes(value);
+        writeLength(sequenceBuffer, bytes.length);
+        sequenceBuffer.writeByteArray(bytes, 0, bytes.length);
+
+        logger.trace("WRITE ASN.1 OCTETSTRING(type=0x%x, length=%d, value=%s)", type, bytes.length, value);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeStartSequence(final byte type) throws IOException {
+        // Get a child sequence buffer
+        sequenceBuffer = sequenceBuffer.startSequence(type);
+
+        logger.trace("WRITE ASN.1 START SEQUENCE(type=0x%x)", type);
+        return this;
+    }
+
+    @Override
+    public ASN1Writer writeStartSet(final byte type) throws IOException {
+        // From an implementation point of view, a set is equivalent to a
+        // sequence.
+        return writeStartSequence(type);
+    }
+
+    Buffer getBuffer() {
+        outBuffer.usable = false;
+        return outBuffer.flip();
+    }
+
+    /**
+     * Writes the provided value for use as the length of an ASN.1 element.
+     *
+     * @param buffer
+     *            The sequence buffer to write to.
+     * @param length
+     *            The length to encode for use in an ASN.1 element.
+     * @throws IOException
+     *             if an error occurs while writing.
+     */
+    private void writeLength(final SequenceBuffer buffer, final int length) throws IOException {
+        if (length < 128) {
+            buffer.writeByte((byte) length);
+        } else if ((length & 0x000000FF) == length) {
+            buffer.writeByte((byte) 0x81);
+            buffer.writeByte((byte) length);
+        } else if ((length & 0x0000FFFF) == length) {
+            buffer.writeByte((byte) 0x82);
+            buffer.writeByte((byte) (length >> 8));
+            buffer.writeByte((byte) length);
+        } else if ((length & 0x00FFFFFF) == length) {
+            buffer.writeByte((byte) 0x83);
+            buffer.writeByte((byte) (length >> 16));
+            buffer.writeByte((byte) (length >> 8));
+            buffer.writeByte((byte) length);
+        } else {
+            buffer.writeByte((byte) 0x84);
+            buffer.writeByte((byte) (length >> 24));
+            buffer.writeByte((byte) (length >> 16));
+            buffer.writeByte((byte) (length >> 8));
+            buffer.writeByte((byte) length);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ConnectionSecurityLayerFilter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ConnectionSecurityLayerFilter.java
new file mode 100644
index 0000000..2f273c7
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/ConnectionSecurityLayerFilter.java
@@ -0,0 +1,120 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.glassfish.grizzly.AbstractTransformer;
+import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.TransformationResult;
+import org.glassfish.grizzly.attributes.AttributeStorage;
+import org.glassfish.grizzly.filterchain.AbstractCodecFilter;
+import org.glassfish.grizzly.memory.Buffers;
+import org.glassfish.grizzly.memory.MemoryManager;
+
+/**
+ * Connection security layer filter adapter.
+ */
+final class ConnectionSecurityLayerFilter extends AbstractCodecFilter<Buffer, Buffer> {
+    /**
+     * <tt>Transformer</tt>, which decodes SASL encrypted data, contained in the
+     * input Buffer, to the output Buffer.
+     */
+    private static final class Decoder extends AbstractTransformer<Buffer, Buffer> {
+        private static final int BUFFER_SIZE = 4096;
+        private final byte[] buffer = new byte[BUFFER_SIZE];
+        private final ConnectionSecurityLayer layer;
+
+        public Decoder(final ConnectionSecurityLayer layer, final MemoryManager<?> memoryManager) {
+            this.layer = layer;
+            setMemoryManager(memoryManager);
+        }
+
+        @Override
+        public String getName() {
+            return getClass().getName();
+        }
+
+        @Override
+        public boolean hasInputRemaining(final AttributeStorage storage, final Buffer input) {
+            return input != null && input.hasRemaining();
+        }
+
+        @Override
+        public TransformationResult<Buffer, Buffer> transformImpl(final AttributeStorage storage,
+                final Buffer input) {
+            final MemoryManager<?> memoryManager = obtainMemoryManager(storage);
+            final int len = Math.min(buffer.length, input.remaining());
+            input.get(buffer, 0, len);
+
+            try {
+                final Buffer output = Buffers.wrap(memoryManager, layer.unwrap(buffer, 0, len));
+                return TransformationResult.createCompletedResult(output, input);
+            } catch (final LdapException e) {
+                return TransformationResult.createErrorResult(e.getResult().getResultCode()
+                        .intValue(), e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * <tt>Transformer</tt>, which encodes SASL encrypted data, contained in the
+     * input Buffer, to the output Buffer.
+     */
+    private static final class Encoder extends AbstractTransformer<Buffer, Buffer> {
+        private static final int BUFFER_SIZE = 4096;
+        private final byte[] buffer = new byte[BUFFER_SIZE];
+        private final ConnectionSecurityLayer layer;
+
+        private Encoder(final ConnectionSecurityLayer layer, final MemoryManager<?> memoryManager) {
+            this.layer = layer;
+            setMemoryManager(memoryManager);
+        }
+
+        @Override
+        public String getName() {
+            return getClass().getName();
+        }
+
+        @Override
+        public boolean hasInputRemaining(final AttributeStorage storage, final Buffer input) {
+            return input != null && input.hasRemaining();
+        }
+
+        @Override
+        public TransformationResult<Buffer, Buffer> transformImpl(final AttributeStorage storage,
+                final Buffer input) {
+            final MemoryManager<?> memoryManager = obtainMemoryManager(storage);
+            final int len = Math.min(buffer.length, input.remaining());
+            input.get(buffer, 0, len);
+
+            try {
+                final Buffer output = Buffers.wrap(memoryManager, layer.wrap(buffer, 0, len));
+                return TransformationResult.createCompletedResult(output, input);
+            } catch (final LdapException e) {
+                return TransformationResult.createErrorResult(e.getResult().getResultCode()
+                        .intValue(), e.getMessage());
+            }
+        }
+    }
+
+    ConnectionSecurityLayerFilter(final ConnectionSecurityLayer layer,
+            final MemoryManager<?> memoryManager) {
+        super(new Decoder(layer, memoryManager), new Encoder(layer, memoryManager));
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java
new file mode 100644
index 0000000..beb4102
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransport.java
@@ -0,0 +1,153 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
+import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder;
+import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
+import org.glassfish.grizzly.strategies.WorkerThreadIOStrategy;
+import org.glassfish.grizzly.threadpool.ThreadPoolConfig;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * The default {@link TCPNIOTransport} which all {@code LDAPConnectionFactory}s
+ * and {@code LDAPListener}s will use unless otherwise specified in their
+ * options.
+ */
+final class DefaultTCPNIOTransport extends ReferenceCountedObject<TCPNIOTransport> {
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    static final DefaultTCPNIOTransport DEFAULT_TRANSPORT = new DefaultTCPNIOTransport();
+
+    private DefaultTCPNIOTransport() {
+        // Prevent instantiation.
+    }
+
+    @Override
+    protected void destroyInstance(final TCPNIOTransport instance) {
+        try {
+            instance.shutdownNow();
+        } catch (final IOException e) {
+            // TODO: I18N
+            logger.warn(LocalizableMessage.raw("An error occurred while shutting down the Grizzly transport", e));
+        }
+    }
+
+    @Override
+    protected TCPNIOTransport newInstance() {
+        final TCPNIOTransportBuilder builder = TCPNIOTransportBuilder.newInstance();
+
+        /*
+         * Determine which threading strategy to use, and total number of
+         * threads.
+         */
+        final String useWorkerThreadsStr =
+                System.getProperty("org.forgerock.opendj.transport.useWorkerThreads");
+        final boolean useWorkerThreadStrategy;
+        if (useWorkerThreadsStr != null) {
+            useWorkerThreadStrategy = Boolean.parseBoolean(useWorkerThreadsStr);
+        } else {
+            /*
+             * The most best performing strategy to use is the
+             * SameThreadIOStrategy, however it can only be used in cases where
+             * result listeners will not block.
+             */
+            useWorkerThreadStrategy = true;
+        }
+
+        if (useWorkerThreadStrategy) {
+            builder.setIOStrategy(WorkerThreadIOStrategy.getInstance());
+        } else {
+            builder.setIOStrategy(SameThreadIOStrategy.getInstance());
+        }
+
+        // Calculate thread counts.
+        final int cpus = Runtime.getRuntime().availableProcessors();
+
+        // Calculate the number of selector threads.
+        final String selectorsStr = System.getProperty("org.forgerock.opendj.transport.selectors");
+        final int selectorThreadCount;
+
+        if (selectorsStr != null) {
+            selectorThreadCount = Integer.parseInt(selectorsStr);
+        } else {
+            selectorThreadCount =
+                    useWorkerThreadStrategy ? Math.max(2, cpus / 4) : Math.max(5, (cpus / 2) - 1);
+        }
+
+        builder.setSelectorThreadPoolConfig(ThreadPoolConfig.defaultConfig().setCorePoolSize(
+                selectorThreadCount).setMaxPoolSize(selectorThreadCount).setPoolName(
+                "OpenDJ LDAP SDK Grizzly selector thread"));
+
+        // Calculate the number of worker threads.
+        if (builder.getWorkerThreadPoolConfig() != null) {
+            final String workersStr = System.getProperty("org.forgerock.opendj.transport.workers");
+            final int workerThreadCount;
+
+            if (workersStr != null) {
+                workerThreadCount = Integer.parseInt(workersStr);
+            } else {
+                workerThreadCount = useWorkerThreadStrategy ? Math.max(5, (cpus * 2)) : 0;
+            }
+
+            builder.setWorkerThreadPoolConfig(ThreadPoolConfig.defaultConfig().setCorePoolSize(
+                    workerThreadCount).setMaxPoolSize(workerThreadCount).setPoolName(
+                    "OpenDJ LDAP SDK Grizzly worker thread"));
+        }
+
+        // Parse IO related options.
+        final String lingerStr = System.getProperty("org.forgerock.opendj.transport.linger");
+        if (lingerStr != null) {
+            // Disabled by default.
+            builder.setLinger(Integer.parseInt(lingerStr));
+        }
+
+        final String tcpNoDelayStr =
+                System.getProperty("org.forgerock.opendj.transport.tcpNoDelay");
+        if (tcpNoDelayStr != null) {
+            // Enabled by default.
+            builder.setTcpNoDelay(Boolean.parseBoolean(tcpNoDelayStr));
+        }
+
+        final String reuseAddressStr =
+                System.getProperty("org.forgerock.opendj.transport.reuseAddress");
+        if (reuseAddressStr != null) {
+            // Enabled by default.
+            builder.setReuseAddress(Boolean.parseBoolean(reuseAddressStr));
+        }
+
+        final TCPNIOTransport transport = builder.build();
+
+        // FIXME: raise bug in Grizzly. We should not need to do this, but
+        // failure to do so causes many deadlocks.
+        transport.setSelectorRunnersCount(selectorThreadCount);
+
+        try {
+            transport.start();
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        return transport;
+    }
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java
new file mode 100644
index 0000000..a584232
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java
@@ -0,0 +1,859 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT;
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT;
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_REQUEST_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.REQUEST_TIMEOUT;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.ResultCode.CLIENT_SIDE_LOCAL_ERROR;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.LDAPWriter;
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.TimeoutEventListener;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindClient;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.ExtendedResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.opendj.ldap.spi.ResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.time.Duration;
+import org.glassfish.grizzly.CompletionHandler;
+import org.glassfish.grizzly.EmptyCompletionHandler;
+import org.glassfish.grizzly.filterchain.Filter;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
+import org.glassfish.grizzly.ssl.SSLFilter;
+
+/** LDAP connection implementation. */
+final class GrizzlyLDAPConnection implements LDAPConnectionImpl, TimeoutEventListener {
+    /**
+     * A dummy SSL client engine configurator as SSLFilter only needs client
+     * config. This prevents Grizzly from needlessly using JVM defaults which
+     * may be incorrectly configured.
+     */
+    private static final SSLEngineConfigurator DUMMY_SSL_ENGINE_CONFIGURATOR;
+    static {
+        try {
+            DUMMY_SSL_ENGINE_CONFIGURATOR =
+                    new SSLEngineConfigurator(new SSLContextBuilder().setTrustManager(
+                            TrustManagers.distrustAll()).getSSLContext());
+        } catch (GeneralSecurityException e) {
+            // This should never happen.
+            throw new IllegalStateException("Unable to create Dummy SSL Engine Configurator", e);
+        }
+    }
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    private final AtomicBoolean bindOrStartTLSInProgress = new AtomicBoolean(false);
+    private final org.glassfish.grizzly.Connection<?> connection;
+    private final AtomicInteger nextMsgID = new AtomicInteger(1);
+    private final GrizzlyLDAPConnectionFactory factory;
+    private final ConcurrentHashMap<Integer, ResultLdapPromiseImpl<?, ?>> pendingRequests = new ConcurrentHashMap<>();
+    private final long requestTimeoutMS;
+    private final Object stateLock = new Object();
+    /** Guarded by stateLock. */
+    private Result connectionInvalidReason;
+    private boolean failedDueToDisconnect;
+    private boolean isClosed;
+    private boolean isFailed;
+    private List<ConnectionEventListener> listeners;
+
+    /**
+     * Create a LDAP Connection with provided Grizzly connection and LDAP
+     * connection factory.
+     *
+     * @param connection
+     *            actual connection
+     * @param factory
+     *            factory that provides LDAP connections
+     */
+    GrizzlyLDAPConnection(final org.glassfish.grizzly.Connection<?> connection,
+            final GrizzlyLDAPConnectionFactory factory) {
+        this.connection = connection;
+        this.factory = factory;
+        final Duration requestTimeout = factory.getLDAPOptions().get(REQUEST_TIMEOUT);
+        this.requestTimeoutMS = requestTimeout.isUnlimited() ? 0 : requestTimeout.to(TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+        /*
+         * Need to be careful here since both abandonAsync and Promise.cancel can
+         * be called separately by the client application. Therefore
+         * promise.cancel() should abandon the request, and abandonAsync should
+         * cancel the promise. In addition, bind or StartTLS requests cannot be
+         * abandoned.
+         */
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                /*
+                 * If there is a bind or startTLS in progress then it must be
+                 * this request which is being abandoned. The following check
+                 * will prevent it from happening.
+                 */
+                checkBindOrStartTLSInProgress();
+            }
+        } catch (final LdapException e) {
+            return newFailedLdapPromise(e);
+        }
+
+        // Remove the promise associated with the request to be abandoned.
+        final ResultLdapPromiseImpl<?, ?> pendingRequest = pendingRequests.remove(request.getRequestID());
+        if (pendingRequest == null) {
+            /*
+             * There has never been a request with the specified message ID or
+             * the response has already been received and handled. We can ignore
+             * this abandon request.
+             */
+            return newSuccessfulLdapPromise((Void) null);
+        }
+
+        /*
+         * This will cancel the promise, but will also recursively invoke this
+         * method. Since the pending request has been removed, there is no risk
+         * of an infinite loop.
+         */
+        pendingRequest.cancel(false);
+
+        /*
+         * FIXME: there's a potential race condition here if a bind or startTLS
+         * is initiated just after we removed the pending request.
+         */
+        return sendAbandonRequest(request);
+    }
+
+    private LdapPromise<Void> sendAbandonRequest(final AbandonRequest request) {
+        final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+        try {
+            final int messageID = nextMsgID.getAndIncrement();
+            writer.writeAbandonRequest(messageID, request);
+            connection.write(writer.getASN1Writer().getBuffer(), null);
+            return newSuccessfulLdapPromise((Void) null, messageID);
+        } catch (final IOException e) {
+            return newFailedLdapPromise(adaptRequestIOException(e));
+        } finally {
+            GrizzlyUtils.recycleWriter(writer);
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> addAsync(final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ResultLdapPromiseImpl<AddRequest, Result> promise =
+                newResultLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeAddRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public void addConnectionEventListener(final ConnectionEventListener listener) {
+        Reject.ifNull(listener);
+        final boolean notifyClose;
+        final boolean notifyErrorOccurred;
+        synchronized (stateLock) {
+            notifyClose = isClosed;
+            notifyErrorOccurred = isFailed;
+            if (!isClosed) {
+                if (listeners == null) {
+                    listeners = new CopyOnWriteArrayList<>();
+                }
+                listeners.add(listener);
+            }
+        }
+        if (notifyErrorOccurred) {
+            // Use the reason provided in the disconnect notification.
+            listener.handleConnectionError(failedDueToDisconnect,
+                    newLdapException(connectionInvalidReason));
+        }
+        if (notifyClose) {
+            listener.handleConnectionClosed();
+        }
+    }
+
+    @Override
+    public LdapPromise<BindResult> bindAsync(final BindRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final BindClient context;
+        try {
+            context = request.createBindClient(Connections.getHostString(factory.getSocketAddress()));
+        } catch (final LdapException e) {
+            return newFailedLdapPromise(e, messageID);
+        }
+
+        final BindResultLdapPromiseImpl promise =
+                newBindLdapPromise(messageID, request, context, intermediateResponseHandler);
+
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                if (!pendingRequests.isEmpty()) {
+                    promise.setResultOrError(Responses.newBindResult(ResultCode.OPERATIONS_ERROR).setDiagnosticMessage(
+                            "There are other operations pending on this connection"));
+                    return promise;
+                }
+                if (!bindOrStartTLSInProgress.compareAndSet(false, true)) {
+                    promise.setResultOrError(Responses.newBindResult(ResultCode.OPERATIONS_ERROR).setDiagnosticMessage(
+                            "Bind or Start TLS operation in progress"));
+                    return promise;
+                }
+                pendingRequests.put(messageID, promise);
+            }
+
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    // Use the bind client to get the initial request instead of
+                    // using the bind request passed to this method.
+                    final GenericBindRequest initialRequest = context.nextBindRequest();
+                    writer.writeBindRequest(messageID, 3, initialRequest);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                bindOrStartTLSInProgress.set(false);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+
+        return promise;
+    }
+
+    @Override
+    public void close() {
+        close(Requests.newUnbindRequest(), null);
+    }
+
+    @Override
+    public void close(final UnbindRequest request, final String reason) {
+        // FIXME: I18N need to internationalize this message.
+        Reject.ifNull(request);
+        close(request, false, Responses.newResult(ResultCode.CLIENT_SIDE_USER_CANCELLED)
+                .setDiagnosticMessage(reason != null ? reason : "Connection closed by client"));
+    }
+
+    @Override
+    public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ResultLdapPromiseImpl<CompareRequest, CompareResult> promise =
+                newCompareLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeCompareRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public LdapPromise<Result> deleteAsync(final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ResultLdapPromiseImpl<DeleteRequest, Result> promise =
+                newResultLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeDeleteRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ExtendedResultLdapPromiseImpl<R> promise =
+                newExtendedLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                if (StartTLSExtendedRequest.OID.equals(request.getOID())) {
+                    if (!pendingRequests.isEmpty()) {
+                        promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult(
+                                ResultCode.OPERATIONS_ERROR, "", "There are pending operations on this connection"));
+                        return promise;
+                    } else if (isTLSEnabled()) {
+                        promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult(
+                                ResultCode.OPERATIONS_ERROR, "", "This connection is already TLS enabled"));
+                        return promise;
+                    } else if (!bindOrStartTLSInProgress.compareAndSet(false, true)) {
+                        promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult(
+                                ResultCode.OPERATIONS_ERROR, "", "Bind or Start TLS operation in progress"));
+                        return promise;
+                    }
+                } else {
+                    checkBindOrStartTLSInProgress();
+                }
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeExtendedRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                bindOrStartTLSInProgress.set(false);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public boolean isClosed() {
+        synchronized (stateLock) {
+            return isClosed;
+        }
+    }
+
+    @Override
+    public boolean isValid() {
+        synchronized (stateLock) {
+            return isValid0();
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> modifyAsync(final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ResultLdapPromiseImpl<ModifyRequest, Result> promise =
+                newResultLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeModifyRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final ResultLdapPromiseImpl<ModifyDNRequest, Result> promise =
+                newResultLdapPromise(messageID, request, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeModifyDNRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public void removeConnectionEventListener(final ConnectionEventListener listener) {
+        Reject.ifNull(listener);
+        synchronized (stateLock) {
+            if (listeners != null) {
+                listeners.remove(listener);
+            }
+        }
+    }
+
+    @Override
+    public LdapPromise<Result> searchAsync(final SearchRequest request,
+        final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
+        final int messageID = nextMsgID.getAndIncrement();
+        final SearchResultLdapPromiseImpl promise =
+                newSearchLdapPromise(messageID, request, entryHandler, intermediateResponseHandler, this);
+        try {
+            synchronized (stateLock) {
+                checkConnectionIsValid();
+                checkBindOrStartTLSInProgress();
+                pendingRequests.put(messageID, promise);
+            }
+            try {
+                final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+                try {
+                    writer.writeSearchRequest(messageID, request);
+                    connection.write(writer.getASN1Writer().getBuffer(), null);
+                } finally {
+                    GrizzlyUtils.recycleWriter(writer);
+                }
+            } catch (final IOException e) {
+                pendingRequests.remove(messageID);
+                throw adaptRequestIOException(e);
+            }
+        } catch (final LdapException e) {
+            promise.adaptErrorResult(e.getResult());
+        }
+        return promise;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + connection.getLocalAddress()
+            + ',' + connection.getPeerAddress() + ')';
+    }
+
+    @Override
+    public long handleTimeout(final long currentTime) {
+        if (requestTimeoutMS <= 0) {
+            return 0;
+        }
+
+        long delay = requestTimeoutMS;
+        for (final ResultLdapPromiseImpl<?, ?> promise : pendingRequests.values()) {
+            if (promise == null || !promise.checkForTimeout()) {
+                continue;
+            }
+            final long diff = (promise.getTimestamp() + requestTimeoutMS) - currentTime;
+            if (diff > 0) {
+                // Will expire in diff milliseconds.
+                delay = Math.min(delay, diff);
+            } else if (pendingRequests.remove(promise.getRequestID()) == null) {
+                // Result arrived at the same time.
+                continue;
+            } else if (promise.isBindOrStartTLS()) {
+                /*
+                 * No other operations can be performed while a bind or StartTLS
+                 * request is active, so we cannot time out the request. We
+                 * therefore have a choice: either ignore timeouts for these
+                 * operations, or enforce them but doing so requires
+                 * invalidating the connection. We'll do the latter, since
+                 * ignoring timeouts could cause the application to hang.
+                 */
+                logger.debug(LocalizableMessage.raw("Failing bind or StartTLS request due to timeout %s"
+                        + "(connection will be invalidated): ", promise));
+                final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
+                        LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT.get(requestTimeoutMS).toString());
+                promise.adaptErrorResult(result);
+
+                // Fail the connection.
+                final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
+                        LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT.get(requestTimeoutMS).toString());
+                connectionErrorOccurred(errorResult);
+            } else {
+                logger.debug(LocalizableMessage.raw("Failing request due to timeout: %s", promise));
+                final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
+                        LDAP_CONNECTION_REQUEST_TIMEOUT.get(requestTimeoutMS).toString());
+                promise.adaptErrorResult(result);
+
+                /*
+                 * FIXME: there's a potential race condition here if a bind or
+                 * startTLS is initiated just after we check the boolean. It
+                 * seems potentially even more dangerous to send the abandon
+                 * request while holding the state lock, since a blocking write
+                 * could hang the application.
+                 */
+                // if (!bindOrStartTLSInProgress.get()) {
+                // sendAbandonRequest(newAbandonRequest(promise.getRequestID()));
+                // }
+            }
+        }
+        return delay;
+    }
+
+    @Override
+    public long getTimeout() {
+        return requestTimeoutMS;
+    }
+
+    /**
+     * Closes this connection, invoking event listeners as needed.
+     *
+     * @param unbindRequest
+     *            The client provided unbind request if this is a client
+     *            initiated close, or {@code null} if the connection has failed.
+     * @param isDisconnectNotification
+     *            {@code true} if this is a connection failure signalled by a
+     *            server disconnect notification.
+     * @param reason
+     *            The result indicating why the connection was closed.
+     */
+    void close(final UnbindRequest unbindRequest, final boolean isDisconnectNotification,
+            final Result reason) {
+        final boolean notifyClose;
+        final boolean notifyErrorOccurred;
+        final List<ConnectionEventListener> tmpListeners;
+        synchronized (stateLock) {
+            if (isClosed) {
+                // Already closed locally.
+                return;
+            } else if (unbindRequest != null) {
+                // Local close.
+                notifyClose = true;
+                notifyErrorOccurred = false;
+                isClosed = true;
+                tmpListeners = listeners;
+                listeners = null; // Prevent future invocations.
+                if (connectionInvalidReason == null) {
+                    connectionInvalidReason = reason;
+                }
+            } else if (isFailed) {
+                // Already failed.
+                return;
+            } else {
+                // Connection has failed and this is the first indication.
+                notifyClose = false;
+                notifyErrorOccurred = true;
+                isFailed = true;
+                failedDueToDisconnect = isDisconnectNotification;
+                connectionInvalidReason = reason;
+                tmpListeners = listeners; // Keep list for client close.
+            }
+        }
+
+        // First abort all outstanding requests.
+        for (final int requestID : pendingRequests.keySet()) {
+            final ResultLdapPromiseImpl<?, ?> promise = pendingRequests.remove(requestID);
+            if (promise != null) {
+                promise.adaptErrorResult(connectionInvalidReason);
+            }
+        }
+
+        /*
+         * If this is the final client initiated close then release close the
+         * connection and release resources.
+         */
+        if (notifyClose) {
+            final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+            try {
+                writer.writeUnbindRequest(nextMsgID.getAndIncrement(), unbindRequest);
+                connection.write(writer.getASN1Writer().getBuffer(), null);
+            } catch (final Exception ignore) {
+                /*
+                 * Underlying channel probably blown up. Ignore all errors,
+                 * including possibly runtime exceptions (see OPENDJ-672).
+                 */
+            } finally {
+                GrizzlyUtils.recycleWriter(writer);
+            }
+            factory.getTimeoutChecker().removeListener(this);
+            connection.closeSilently();
+            factory.releaseTransportAndTimeoutChecker();
+        }
+
+        // Notify listeners.
+        if (tmpListeners != null) {
+            if (notifyErrorOccurred) {
+                for (final ConnectionEventListener listener : tmpListeners) {
+                    // Use the reason provided in the disconnect notification.
+                    listener.handleConnectionError(isDisconnectNotification, newLdapException(reason));
+                }
+            }
+            if (notifyClose) {
+                for (final ConnectionEventListener listener : tmpListeners) {
+                    listener.handleConnectionClosed();
+                }
+            }
+        }
+    }
+
+    int continuePendingBindRequest(final BindResultLdapPromiseImpl promise) throws LdapException {
+        final int newMsgID = nextMsgID.getAndIncrement();
+        synchronized (stateLock) {
+            checkConnectionIsValid();
+            pendingRequests.put(newMsgID, promise);
+        }
+        return newMsgID;
+    }
+
+    Options getLDAPOptions() {
+        return factory.getLDAPOptions();
+    }
+
+    ResultLdapPromiseImpl<?, ?> getPendingRequest(final Integer messageID) {
+        return pendingRequests.get(messageID);
+    }
+
+    void handleUnsolicitedNotification(final ExtendedResult result) {
+        final List<ConnectionEventListener> tmpListeners;
+        synchronized (stateLock) {
+            tmpListeners = listeners;
+        }
+        if (tmpListeners != null) {
+            for (final ConnectionEventListener listener : tmpListeners) {
+                listener.handleUnsolicitedNotification(result);
+            }
+        }
+    }
+
+    /**
+     * Installs a new Grizzly filter (e.g. SSL/SASL) beneath the top-level LDAP
+     * filter.
+     *
+     * @param filter
+     *            The filter to be installed.
+     */
+    void installFilter(final Filter filter) {
+        synchronized (stateLock) {
+            GrizzlyUtils.addFilterToConnection(filter, connection);
+        }
+    }
+
+    /**
+     * Indicates whether or not TLS is enabled on this connection.
+     *
+     * @return {@code true} if TLS is enabled on this connection, otherwise
+     *         {@code false}.
+     */
+    boolean isTLSEnabled() {
+        synchronized (stateLock) {
+            final FilterChain currentFilterChain = (FilterChain) connection.getProcessor();
+            for (final Filter filter : currentFilterChain) {
+                if (filter instanceof SSLFilter) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    ResultLdapPromiseImpl<?, ?> removePendingRequest(final Integer messageID) {
+        return pendingRequests.remove(messageID);
+    }
+
+    void setBindOrStartTLSInProgress(final boolean state) {
+        bindOrStartTLSInProgress.set(state);
+    }
+
+    @Override
+    public Promise<Void, LdapException> enableTLS(
+            final SSLContext sslContext,
+            final List<String> sslEnabledProtocols,
+            final List<String> sslEnabledCipherSuites) {
+        final PromiseImpl<Void, LdapException> promise = PromiseImpl.create();
+        final EmptyCompletionHandler<SSLEngine> completionHandler = new EmptyCompletionHandler<SSLEngine>() {
+            @Override
+            public void completed(final SSLEngine result) {
+                promise.handleResult(null);
+            }
+
+            @Override
+            public void failed(final Throwable throwable) {
+                final Result errorResult = newResult(CLIENT_SIDE_LOCAL_ERROR)
+                        .setCause(throwable).setDiagnosticMessage("SSL handshake failed");
+                connectionErrorOccurred(errorResult);
+                promise.handleException(newLdapException(errorResult));
+            }
+        };
+
+        try {
+            startTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites, completionHandler);
+        } catch (final IOException e) {
+            completionHandler.failed(e);
+        }
+        return promise;
+    }
+
+    void startTLS(final SSLContext sslContext, final List<String> protocols, final List<String> cipherSuites,
+                  final CompletionHandler<SSLEngine> completionHandler) throws IOException {
+        synchronized (stateLock) {
+            if (isTLSEnabled()) {
+                throw new IllegalStateException("TLS already enabled");
+            }
+
+            final SSLEngineConfigurator sslEngineConfigurator = new SSLEngineConfigurator(sslContext, true, false,
+                    false);
+            sslEngineConfigurator.setEnabledProtocols(protocols.isEmpty() ? null : protocols
+                    .toArray(new String[protocols.size()]));
+            sslEngineConfigurator.setEnabledCipherSuites(cipherSuites.isEmpty() ? null : cipherSuites
+                    .toArray(new String[cipherSuites.size()]));
+            final SSLFilter sslFilter = new SSLFilter(DUMMY_SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator);
+            installFilter(sslFilter);
+            sslFilter.handshake(connection, completionHandler);
+        }
+    }
+
+    private LdapException adaptRequestIOException(final IOException e) {
+        // FIXME: what other sort of IOExceptions can be thrown?
+        // FIXME: Is this the best result code?
+        final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_ENCODING_ERROR).setCause(e);
+        connectionErrorOccurred(errorResult);
+        return newLdapException(errorResult);
+    }
+
+    private void checkBindOrStartTLSInProgress() throws LdapException {
+        if (bindOrStartTLSInProgress.get()) {
+            throw newLdapException(ResultCode.OPERATIONS_ERROR, "Bind or Start TLS operation in progress");
+        }
+    }
+
+    private void checkConnectionIsValid() throws LdapException {
+        if (!isValid0()) {
+            if (failedDueToDisconnect) {
+                /*
+                 * Connection termination was triggered remotely. We don't want
+                 * to blindly pass on the result code to requests since it could
+                 * be confused for a genuine response. For example, if the
+                 * disconnect contained the invalidCredentials result code then
+                 * this could be misinterpreted as a genuine authentication
+                 * failure for subsequent bind requests.
+                 */
+                throw newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, "Connection closed by server");
+            } else {
+                throw newLdapException(connectionInvalidReason);
+            }
+        }
+    }
+
+    private void connectionErrorOccurred(final Result reason) {
+        close(null, false, reason);
+    }
+
+    private boolean isValid0() {
+        return !isFailed && !isClosed;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java
new file mode 100644
index 0000000..be856b2
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java
@@ -0,0 +1,254 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
+import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
+import static org.forgerock.opendj.grizzly.GrizzlyUtils.buildFilterChain;
+import static org.forgerock.opendj.grizzly.GrizzlyUtils.configureConnection;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.TimeoutChecker.TIMEOUT_CHECKER;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.TimeoutChecker;
+import org.forgerock.opendj.ldap.TimeoutEventListener;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.util.Option;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.time.Duration;
+import org.glassfish.grizzly.CompletionHandler;
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.SocketConnectorHandler;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.nio.transport.TCPNIOConnectorHandler;
+import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * LDAP connection factory implementation using Grizzly for transport.
+ */
+public final class GrizzlyLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * Adapts a Grizzly connection completion handler to an LDAP connection promise.
+     */
+    @SuppressWarnings("rawtypes")
+    private final class CompletionHandlerAdapter implements CompletionHandler<Connection>, TimeoutEventListener {
+        private final PromiseImpl<LDAPConnectionImpl, LdapException> promise;
+        private final long timeoutEndTime;
+
+        private CompletionHandlerAdapter(final PromiseImpl<LDAPConnectionImpl, LdapException> promise) {
+            this.promise = promise;
+            final long timeoutMS = getTimeout();
+            this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0;
+            timeoutChecker.get().addListener(this);
+        }
+
+        @Override
+        public void cancelled() {
+            // Ignore this.
+        }
+
+        @Override
+        public void completed(final Connection result) {
+            // Adapt the connection.
+            final GrizzlyLDAPConnection connection = adaptConnection(result);
+            timeoutChecker.get().removeListener(this);
+            if (!promise.tryHandleResult(connection)) {
+                // The connection has been either cancelled or it has timed out.
+                connection.close();
+            }
+        }
+
+        @Override
+        public void failed(final Throwable throwable) {
+            // Adapt and forward.
+            timeoutChecker.get().removeListener(this);
+            promise.handleException(adaptConnectionException(throwable));
+            releaseTransportAndTimeoutChecker();
+        }
+
+        @Override
+        public void updated(final Connection result) {
+            // Ignore this.
+        }
+
+        private GrizzlyLDAPConnection adaptConnection(final Connection<?> connection) {
+            configureConnection(connection, logger, options);
+
+            final GrizzlyLDAPConnection ldapConnection =
+                    new GrizzlyLDAPConnection(connection, GrizzlyLDAPConnectionFactory.this);
+            timeoutChecker.get().addListener(ldapConnection);
+            clientFilter.registerConnection(connection, ldapConnection);
+            return ldapConnection;
+        }
+
+        private LdapException adaptConnectionException(Throwable t) {
+            if (!(t instanceof LdapException) && t instanceof ExecutionException) {
+                t = t.getCause() != null ? t.getCause() : t;
+            }
+            if (t instanceof LdapException) {
+                return (LdapException) t;
+            } else {
+                return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, t.getMessage(), t);
+            }
+        }
+
+        @Override
+        public long handleTimeout(final long currentTime) {
+            if (timeoutEndTime == 0) {
+                return 0;
+            } else if (timeoutEndTime > currentTime) {
+                return timeoutEndTime - currentTime;
+            } else {
+                promise.handleException(newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+                        LDAP_CONNECTION_CONNECT_TIMEOUT.get(getSocketAddress(), getTimeout()).toString()));
+                return 0;
+            }
+        }
+
+        @Override
+        public long getTimeout() {
+            final Duration duration = options.get(CONNECT_TIMEOUT);
+            return duration.isUnlimited() ? 0L : duration.to(TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private final LDAPClientFilter clientFilter;
+    private final FilterChain defaultFilterChain;
+    private final Options options;
+    private final String host;
+    private final int port;
+
+    /**
+     * Prevents the transport and timeoutChecker being released when there are
+     * remaining references (this factory or any connections). It is initially
+     * set to 1 because this factory has a reference.
+     */
+    private final AtomicInteger referenceCount = new AtomicInteger(1);
+
+    /**
+     * Indicates whether this factory has been closed or not.
+     */
+    private final AtomicBoolean isClosed = new AtomicBoolean();
+
+    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
+    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER.acquire();
+
+    /**
+     * Grizzly TCP Transport NIO implementation to use for connections. If {@code null}, default transport will be
+     * used.
+     */
+    public static final Option<TCPNIOTransport> GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null);
+
+    /**
+     * Creates a new LDAP connection factory based on Grizzly which can be used to create connections to the Directory
+     * Server at the provided host and port address using provided connection options.
+     *
+     * @param host
+     *         The hostname of the Directory Server to connect to.
+     * @param port
+     *         The port number of the Directory Server to connect to.
+     * @param options
+     *         The LDAP connection options to use when creating connections.
+     */
+    public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options) {
+        this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.get(GRIZZLY_TRANSPORT));
+        this.host = host;
+        this.port = port;
+        this.options = options;
+        this.clientFilter = new LDAPClientFilter(options.get(LDAP_DECODE_OPTIONS), 0);
+        this.defaultFilterChain = buildFilterChain(this.transport.get().getProcessor(), clientFilter);
+    }
+
+    @Override
+    public void close() {
+        if (isClosed.compareAndSet(false, true)) {
+            releaseTransportAndTimeoutChecker();
+        }
+    }
+
+    @Override
+    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
+        acquireTransportAndTimeoutChecker(); // Protect resources.
+        final SocketConnectorHandler connectorHandler = TCPNIOConnectorHandler.builder(transport.get())
+                                                                              .processor(defaultFilterChain)
+                                                                              .build();
+        final PromiseImpl<LDAPConnectionImpl, LdapException> promise = PromiseImpl.create();
+        connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise));
+        return promise;
+    }
+
+    @Override
+    public InetSocketAddress getSocketAddress() {
+        return new InetSocketAddress(host, port);
+    }
+
+    @Override
+    public String getHostName() {
+        return host;
+    }
+
+    @Override
+    public int getPort() {
+        return port;
+    }
+
+    TimeoutChecker getTimeoutChecker() {
+        return timeoutChecker.get();
+    }
+
+    Options getLDAPOptions() {
+        return options;
+    }
+
+    void releaseTransportAndTimeoutChecker() {
+        if (referenceCount.decrementAndGet() == 0) {
+            transport.release();
+            timeoutChecker.release();
+        }
+    }
+
+    private void acquireTransportAndTimeoutChecker() {
+        /*
+         * If the factory is not closed then we need to prevent the resources
+         * (transport, timeout checker) from being released while the connection
+         * attempt is in progress.
+         */
+        referenceCount.incrementAndGet();
+        if (isClosed.get()) {
+            releaseTransportAndTimeoutChecker();
+            throw new IllegalStateException("Attempted to get a connection after factory close");
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
new file mode 100644
index 0000000..caaf571
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
@@ -0,0 +1,152 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
+import static org.forgerock.opendj.ldap.LDAPListener.*;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.spi.LDAPListenerImpl;
+import org.forgerock.util.Options;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.nio.transport.TCPNIOBindingHandler;
+import org.glassfish.grizzly.nio.transport.TCPNIOServerConnection;
+import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * LDAP listener implementation using Grizzly for transport.
+ */
+public final class GrizzlyLDAPListener implements LDAPListenerImpl {
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
+    private final ServerConnectionFactory<LDAPClientContext, Integer> connectionFactory;
+    private final TCPNIOServerConnection serverConnection;
+    private final AtomicBoolean isClosed = new AtomicBoolean();
+    private final InetSocketAddress socketAddress;
+    private final Options options;
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections using the provided address and connection options.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     */
+    public GrizzlyLDAPListener(final InetSocketAddress address,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options) throws IOException {
+        this(address, factory, options, null);
+    }
+
+    /**
+     * Creates a new LDAP listener implementation which will listen for LDAP
+     * client connections using the provided address, connection options and
+     * provided TCP transport.
+     *
+     * @param address
+     *            The address to listen on.
+     * @param factory
+     *            The server connection factory which will be used to create
+     *            server connections.
+     * @param options
+     *            The LDAP listener options.
+     * @param transport
+     *            Grizzly TCP Transport NIO implementation to use for
+     *            connections. If {@code null}, default transport will be used.
+     * @throws IOException
+     *             If an error occurred while trying to listen on the provided
+     *             address.
+     */
+    public GrizzlyLDAPListener(final InetSocketAddress address,
+            final ServerConnectionFactory<LDAPClientContext, Integer> factory,
+            final Options options, TCPNIOTransport transport) throws IOException {
+        this.transport = DEFAULT_TRANSPORT.acquireIfNull(transport);
+        this.connectionFactory = factory;
+        this.options = Options.copyOf(options);
+        final LDAPServerFilter serverFilter =
+                new LDAPServerFilter(this, options.get(LDAP_DECODE_OPTIONS), options.get(REQUEST_MAX_SIZE_IN_BYTES));
+        final FilterChain ldapChain =
+                GrizzlyUtils.buildFilterChain(this.transport.get().getProcessor(), serverFilter);
+        final TCPNIOBindingHandler bindingHandler =
+                TCPNIOBindingHandler.builder(this.transport.get()).processor(ldapChain).build();
+        this.serverConnection = bindingHandler.bind(address, options.get(CONNECT_MAX_BACKLOG));
+
+        /*
+         * Get the socket address now, ensuring that the host is the same as the
+         * one provided in the constructor. The port will have changed if 0 was
+         * passed in.
+         */
+        final int port = ((InetSocketAddress) serverConnection.getLocalAddress()).getPort();
+        socketAddress = new InetSocketAddress(Connections.getHostString(address), port);
+    }
+
+    @Override
+    public void close() {
+        if (isClosed.compareAndSet(false, true)) {
+            try {
+                serverConnection.close().get();
+            } catch (final InterruptedException e) {
+                // Cannot handle here.
+                Thread.currentThread().interrupt();
+            } catch (final Exception e) {
+                // TODO: I18N
+                logger.warn(LocalizableMessage.raw("Exception occurred while closing listener", e));
+            }
+            transport.release();
+        }
+    }
+
+    @Override
+    public InetSocketAddress getSocketAddress() {
+        return socketAddress;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("LDAPListener(");
+        builder.append(getSocketAddress());
+        builder.append(')');
+        return builder.toString();
+    }
+
+    ServerConnectionFactory<LDAPClientContext, Integer> getConnectionFactory() {
+        return connectionFactory;
+    }
+
+    Options getLDAPListenerOptions() {
+        return options;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java
new file mode 100644
index 0000000..2881ab2
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java
@@ -0,0 +1,228 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.channels.SocketChannel;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.io.LDAPReader;
+import org.forgerock.opendj.io.LDAPWriter;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.util.Options;
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.Processor;
+import org.glassfish.grizzly.ThreadCache;
+import org.glassfish.grizzly.filterchain.Filter;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.filterchain.FilterChainBuilder;
+import org.glassfish.grizzly.filterchain.TransportFilter;
+import org.glassfish.grizzly.memory.MemoryManager;
+import org.glassfish.grizzly.nio.transport.TCPNIOConnection;
+import org.glassfish.grizzly.ssl.SSLFilter;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+
+/**
+ * Common utility methods.
+ */
+final class GrizzlyUtils {
+    @SuppressWarnings("rawtypes")
+    private static final ThreadCache.CachedTypeIndex<LDAPWriter> WRITER_INDEX = ThreadCache
+            .obtainIndex(LDAPWriter.class, 1);
+
+    /**
+     * Build a filter chain from the provided processor if possible and the
+     * provided filter.
+     * <p>
+     * If the provided processor can't be used for building the new filter
+     * chain, then a chain with only a {@code TransportFilter} is used as a base
+     * chain.
+     *
+     * @param processor
+     *            processor to build the filter chain from. If the processor is
+     *            not a filter chain (for example, it can be a
+     *            {@code StandaloneProcessor} then it is ignored to build the
+     *            returned filter chain
+     * @param filter
+     *            filter to add at the end of the filter chain
+     * @return a new filter chain, based on the provided processor if processor
+     *         is a {@code FilterChain}, and having the provided filter as the
+     *         last filter
+     */
+    static FilterChain buildFilterChain(Processor<?> processor, Filter filter) {
+        if (processor instanceof FilterChain) {
+            return FilterChainBuilder.stateless().addAll((FilterChain) processor).add(filter).build();
+        } else {
+            return FilterChainBuilder.stateless().add(new TransportFilter()).add(filter).build();
+        }
+    }
+
+    /**
+     * Adds a filter to filter chain registered with the given connection.
+     * <p>
+     * For a non-SSL filter, filter is added at the last position before the
+     * LDAP filter.
+     * <p>
+     * For a SSL filter, filter is added before any
+     * {@code ConnectionSecurityLayerFilter} which is already present in the
+     * filter chain.
+     *
+     * @param filter
+     *            filter to add
+     * @param connection
+     *            connection to update with the new filter chain containing the
+     *            provided filter
+     */
+    static void addFilterToConnection(final Filter filter, Connection<?> connection) {
+        final FilterChain currentChain = (FilterChain) connection.getProcessor();
+        final FilterChain newChain = addFilterToChain(filter, currentChain);
+        connection.setProcessor(newChain);
+    }
+
+    /**
+     * Adds a filter to a filter chain.
+     * <p>
+     * For a non-SSL filter, filter is added at the last position before the
+     * LDAP filter.
+     * <p>
+     * For a SSL filter, filter is added before any
+     * {@code ConnectionSecurityLayerFilter} which is already present in the
+     * filter chain.
+     *
+     * @param filter
+     *            filter to add
+     * @param chain
+     *            initial filter chain
+     * @return a new filter chain which includes the provided filter
+     */
+    static FilterChain addFilterToChain(final Filter filter, final FilterChain chain) {
+        // By default, before LDAP filter which is the last one
+        int indexToAddFilter = chain.size() - 1;
+        if (filter instanceof SSLFilter) {
+            // Before any ConnectionSecurityLayerFilters if present
+            for (int i = chain.size() - 2; i >= 0; i--) {
+                if (!(chain.get(i) instanceof ConnectionSecurityLayerFilter)) {
+                    indexToAddFilter = i + 1;
+                    break;
+                }
+            }
+        }
+        return FilterChainBuilder.stateless().addAll(chain).add(indexToAddFilter, filter).build();
+    }
+
+    /**
+     * Creates a new LDAP Reader with the provided maximum size of ASN1 element,
+     * options and memory manager.
+     *
+     * @param decodeOptions
+     *            allow to control how responses and requests are decoded
+     * @param maxASN1ElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     * @param memoryManager
+     *            The memory manager to use for buffering.
+     * @return a LDAP reader
+     */
+    static LDAPReader<ASN1BufferReader> createReader(DecodeOptions decodeOptions,
+            int maxASN1ElementSize, MemoryManager<?> memoryManager) {
+        ASN1BufferReader asn1Reader = new ASN1BufferReader(maxASN1ElementSize, memoryManager);
+        return LDAP.getReader(asn1Reader, decodeOptions);
+    }
+
+    /**
+     * Returns a LDAP writer, with a clean ASN1Writer, possibly from
+     * the thread local cache.
+     * <p>
+     * The writer is either returned from thread local cache or created.
+     * In the former case, the writer is removed from the cache.
+     *
+     * @return a LDAP writer
+     */
+    @SuppressWarnings("unchecked")
+    static LDAPWriter<ASN1BufferWriter> getWriter() {
+        LDAPWriter<ASN1BufferWriter> writer = ThreadCache.takeFromCache(WRITER_INDEX);
+        if (writer == null) {
+            writer = LDAP.getWriter(new ASN1BufferWriter());
+        }
+        writer.getASN1Writer().reset();
+        return writer;
+    }
+
+    /**
+     * Recycle a LDAP writer to a thread local cache.
+     * <p>
+     * The LDAP writer is then available for the thread using the
+     * {@get()} method.
+     *
+     * @param writer LDAP writer to recycle
+     */
+    static void recycleWriter(LDAPWriter<ASN1BufferWriter> writer) {
+        writer.getASN1Writer().recycle();
+        ThreadCache.putToCache(WRITER_INDEX, writer);
+    }
+
+    static void configureConnection(final Connection<?> connection, final LocalizedLogger logger, Options options) {
+        /*
+         * Test shows that its much faster with non block writes but risk
+         * running out of memory if the server is slow.
+         */
+        connection.configureBlocking(true);
+
+        // Configure socket options.
+        final SocketChannel channel = (SocketChannel) ((TCPNIOConnection) connection).getChannel();
+        final Socket socket = channel.socket();
+        final boolean tcpNoDelay = options.get(TCP_NO_DELAY);
+        final boolean keepAlive = options.get(SO_KEEPALIVE);
+        final boolean reuseAddress = options.get(SO_REUSE_ADDRESS);
+        final int linger = options.get(SO_LINGER_IN_SECONDS);
+        try {
+            socket.setTcpNoDelay(tcpNoDelay);
+        } catch (final SocketException e) {
+            logger.traceException(e, "Unable to set TCP_NODELAY to %d on client connection",
+                    tcpNoDelay);
+        }
+        try {
+            socket.setKeepAlive(keepAlive);
+        } catch (final SocketException e) {
+            logger.traceException(e, "Unable to set SO_KEEPALIVE to %d on client connection",
+                    keepAlive);
+        }
+        try {
+            socket.setReuseAddress(reuseAddress);
+        } catch (final SocketException e) {
+            logger.traceException(e, "Unable to set SO_REUSEADDR to %d on client connection",
+                    reuseAddress);
+        }
+        try {
+            if (linger < 0) {
+                socket.setSoLinger(false, 0);
+            } else {
+                socket.setSoLinger(true, linger);
+            }
+        } catch (final SocketException e) {
+            logger.traceException(e, "Unable to set SO_LINGER to %d on client connection", linger);
+        }
+    }
+
+    /** Prevent instantiation. */
+    private GrizzlyUtils() {
+        // No implementation required.
+    }
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPBaseFilter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPBaseFilter.java
new file mode 100644
index 0000000..3f35af3
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPBaseFilter.java
@@ -0,0 +1,121 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.io.LDAPMessageHandler;
+import org.forgerock.opendj.io.LDAPReader;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.filterchain.BaseFilter;
+import org.glassfish.grizzly.filterchain.FilterChainContext;
+import org.glassfish.grizzly.filterchain.NextAction;
+
+/**
+ * Base class for LDAP-enabled filter.
+ * <p>
+ * Provides a common {@code handleRead()} method for both client and server
+ * filters.
+ */
+abstract class LDAPBaseFilter extends BaseFilter {
+
+    /**
+     * The maximum BER element size, or <code>0</code> to indicate that there is
+     * no limit.
+     */
+    final int maxASN1ElementSize;
+
+    /**
+     * Allow to control how to decode requests and responses.
+     */
+    final DecodeOptions decodeOptions;
+
+    /**
+     * Creates a filter with provided decode options and max size of
+     * ASN1 element.
+     *
+     * @param options
+     *            control how to decode requests and responses
+     * @param maxASN1ElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     */
+    LDAPBaseFilter(final DecodeOptions options, final int maxASN1ElementSize) {
+        this.decodeOptions = options;
+        this.maxASN1ElementSize = maxASN1ElementSize;
+    }
+
+    @Override
+    public final NextAction handleRead(final FilterChainContext ctx) throws IOException {
+        final LDAPBaseHandler handler = getLDAPHandler(ctx);
+        final LDAPReader<ASN1BufferReader> reader = handler.getReader();
+        final ASN1BufferReader asn1Reader = reader.getASN1Reader();
+        final Buffer buffer = (Buffer) ctx.getMessage();
+
+        asn1Reader.appendBytesRead(buffer);
+        try {
+            while (reader.hasMessageAvailable()) {
+                reader.readMessage(handler);
+            }
+        } catch (IOException e) {
+            handleReadException(ctx, e);
+            throw e;
+        } finally {
+            asn1Reader.disposeBytesRead();
+        }
+
+        return ctx.getStopAction();
+    }
+
+    /**
+     * Handle an exception occuring during a read within the
+     * {@code handleRead()} method.
+     *
+     * @param ctx
+     *            context when reading
+     * @param e
+     *            exception occuring while reading
+     */
+    abstract void handleReadException(FilterChainContext ctx, IOException e);
+
+    /**
+     * Interface for the {@code LDAPMessageHandler} used in the filter, that
+     * must be able to retrieve a Grizzly reader.
+     */
+    interface LDAPBaseHandler extends LDAPMessageHandler {
+        /**
+         * Returns the LDAP reader for this handler.
+         * @return the reader
+         */
+        LDAPReader<ASN1BufferReader> getReader();
+    }
+
+    /**
+     * Returns the LDAP message handler associated to the underlying connection
+     * of the provided context.
+     * <p>
+     * If no handler exists yet for the underlying connection, a new one is
+     * created and recorded for the connection.
+     *
+     * @param ctx
+     *            current filter chain context
+     * @return the response handler associated to the connection, which can be a
+     *         new one if no handler have been created yet
+     */
+    abstract LDAPBaseHandler getLDAPHandler(final FilterChainContext ctx);
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java
new file mode 100644
index 0000000..b5b308c
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java
@@ -0,0 +1,496 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import javax.net.ssl.SSLEngine;
+
+import org.forgerock.opendj.io.AbstractLDAPMessageHandler;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.io.LDAPReader;
+import org.forgerock.opendj.io.LDAPWriter;
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindClient;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.ExtendedResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.ResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.EmptyCompletionHandler;
+import org.glassfish.grizzly.Grizzly;
+import org.glassfish.grizzly.attributes.Attribute;
+import org.glassfish.grizzly.filterchain.FilterChainContext;
+import org.glassfish.grizzly.filterchain.NextAction;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.ResultCode.*;
+import static org.forgerock.opendj.ldap.responses.Responses.*;
+
+/**
+ * Grizzly filter implementation for decoding LDAP responses and handling client
+ * side logic for SSL and SASL operations over LDAP.
+ */
+final class LDAPClientFilter extends LDAPBaseFilter {
+    private static final Attribute<GrizzlyLDAPConnection> LDAP_CONNECTION_ATTR =
+            Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("LDAPClientConnection");
+
+    private static final Attribute<ClientResponseHandler> RESPONSE_HANDLER_ATTR =
+            Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("ClientResponseHandler");
+
+    static final class ClientResponseHandler extends AbstractLDAPMessageHandler implements
+            LDAPBaseHandler {
+
+        private final LDAPReader<ASN1BufferReader> reader;
+        private FilterChainContext context;
+
+        /**
+         * Creates a handler with the provided reader.
+         *
+         * @param reader
+         *            LDAP reader to use for reading incoming messages
+         */
+        ClientResponseHandler(LDAPReader<ASN1BufferReader> reader) {
+            this.reader = reader;
+        }
+
+        void setFilterChainContext(FilterChainContext context) {
+            this.context = context;
+        }
+
+        /**
+         * Returns the LDAP reader.
+         *
+         * @return the reader to read incoming LDAP messages
+         */
+        @Override
+        public LDAPReader<ASN1BufferReader> getReader() {
+            return this.reader;
+        }
+
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        @Override
+        public void addResult(final int messageID, final Result result) throws DecodeException,
+                IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof AddRequest) {
+                        pendingRequest.setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @Override
+        public void bindResult(final int messageID, final BindResult result)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest instanceof BindResultLdapPromiseImpl) {
+                        final BindResultLdapPromiseImpl promise = (BindResultLdapPromiseImpl) pendingRequest;
+                        final BindClient bindClient = promise.getBindClient();
+
+                        try {
+                            if (!bindClient.evaluateResult(result)) {
+                                // The server is expecting a multi stage
+                                // bind response.
+                                final int msgID = ldapConnection.continuePendingBindRequest(promise);
+
+                                LDAPWriter<ASN1BufferWriter> ldapWriter = GrizzlyUtils.getWriter();
+                                try {
+                                    final GenericBindRequest nextRequest =
+                                            bindClient.nextBindRequest();
+                                    ldapWriter.writeBindRequest(msgID, 3, nextRequest);
+                                    context.write(ldapWriter.getASN1Writer().getBuffer(), null);
+                                } finally {
+                                    GrizzlyUtils.recycleWriter(ldapWriter);
+                                }
+                                return;
+                            }
+                        } catch (final LdapException e) {
+                            ldapConnection.setBindOrStartTLSInProgress(false);
+                            promise.adaptErrorResult(e.getResult());
+                            return;
+                        } catch (final IOException e) {
+                            // FIXME: I18N need to have a better error message.
+                            // FIXME: Is this the best result code?
+                            ldapConnection.setBindOrStartTLSInProgress(false);
+                            final Result errorResult =
+                                    Responses
+                                            .newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR)
+                                            .setDiagnosticMessage(
+                                                    "An error occurred during multi-stage authentication")
+                                            .setCause(e);
+                            promise.adaptErrorResult(errorResult);
+                            return;
+                        }
+
+                        if (result.getResultCode() == ResultCode.SUCCESS) {
+                            final ConnectionSecurityLayer l =
+                                    bindClient.getConnectionSecurityLayer();
+                            if (l != null) {
+                                // The connection needs to be secured by
+                                // the SASL mechanism.
+                                ldapConnection.installFilter(new ConnectionSecurityLayerFilter(l,
+                                        context.getConnection().getTransport().getMemoryManager()));
+                            }
+                        }
+
+                        ldapConnection.setBindOrStartTLSInProgress(false);
+                        promise.setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void compareResult(final int messageID, final CompareResult result)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof CompareRequest) {
+                        ((ResultLdapPromiseImpl<CompareRequest, CompareResult>) pendingRequest)
+                                .setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void deleteResult(final int messageID, final Result result) throws DecodeException,
+                IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof DeleteRequest) {
+                        ((ResultLdapPromiseImpl<DeleteRequest, Result>) pendingRequest).setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @Override
+        public void extendedResult(final int messageID, final ExtendedResult result)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                if (messageID == 0) {
+                    // Unsolicited notification received.
+                    if (LDAP.OID_NOTICE_OF_DISCONNECTION.equals(result.getOID())) {
+                        // Treat this as a connection error.
+                        final Result errorResult = newResult(result.getResultCode()).setDiagnosticMessage(
+                                        result.getDiagnosticMessage());
+                        ldapConnection.close(null, true, errorResult);
+                    } else {
+                        ldapConnection.handleUnsolicitedNotification(result);
+                    }
+                } else {
+                    final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                    if (pendingRequest != null) {
+                        if (pendingRequest.getRequest() instanceof ExtendedRequest) {
+                            final ExtendedResultLdapPromiseImpl<?> extendedPromise =
+                                    (ExtendedResultLdapPromiseImpl<?>) pendingRequest;
+                            try {
+                                handleExtendedResult0(ldapConnection, extendedPromise, result);
+                            } catch (final DecodeException de) {
+                                // FIXME: should the connection be closed as well?
+                                final Result errorResult = newResult(ResultCode.CLIENT_SIDE_DECODING_ERROR)
+                                                .setDiagnosticMessage(de.getLocalizedMessage())
+                                                .setCause(de);
+                                extendedPromise.adaptErrorResult(errorResult);
+                            }
+                        } else {
+                            throw newUnexpectedResponseException(messageID, result);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void intermediateResponse(final int messageID, final IntermediateResponse response)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.getPendingRequest(messageID);
+                if (pendingRequest != null) {
+                    pendingRequest.handleIntermediateResponse(response);
+                }
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void modifyDNResult(final int messageID, final Result result)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof ModifyDNRequest) {
+                        ((ResultLdapPromiseImpl<ModifyDNRequest, Result>) pendingRequest).setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void modifyResult(final int messageID, final Result result) throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof ModifyRequest) {
+                        ((ResultLdapPromiseImpl<ModifyRequest, Result>) pendingRequest).setResultOrError(result);
+                        return;
+                    }
+                    throw newUnexpectedResponseException(messageID, result);
+                }
+            }
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void searchResult(final int messageID, final Result result) throws DecodeException,
+                IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.removePendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest.getRequest() instanceof SearchRequest) {
+                        ((ResultLdapPromiseImpl<SearchRequest, Result>) pendingRequest).setResultOrError(result);
+                    } else {
+                        throw newUnexpectedResponseException(messageID, result);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void searchResultEntry(final int messageID, final SearchResultEntry entry)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.getPendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest instanceof SearchResultLdapPromiseImpl) {
+                        ((SearchResultLdapPromiseImpl) pendingRequest).handleEntry(entry);
+                    } else {
+                        throw newUnexpectedResponseException(messageID, entry);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void searchResultReference(final int messageID, final SearchResultReference reference)
+                throws DecodeException, IOException {
+            final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(context.getConnection());
+            if (ldapConnection != null) {
+                final ResultLdapPromiseImpl<?, ?> pendingRequest = ldapConnection.getPendingRequest(messageID);
+                if (pendingRequest != null) {
+                    if (pendingRequest instanceof SearchResultLdapPromiseImpl) {
+                        ((SearchResultLdapPromiseImpl) pendingRequest).handleReference(reference);
+                    } else {
+                        throw newUnexpectedResponseException(messageID, reference);
+                    }
+                }
+            }
+        }
+
+        /** Needed in order to expose type information. */
+        private <R extends ExtendedResult> void handleExtendedResult0(
+                final GrizzlyLDAPConnection conn, final ExtendedResultLdapPromiseImpl<R> promise,
+                final ExtendedResult result) throws DecodeException {
+            final R decodedResponse = promise.decodeResult(result, conn.getLDAPOptions().get(LDAP_DECODE_OPTIONS));
+
+            if (result.getResultCode() == ResultCode.SUCCESS
+                    && promise.getRequest() instanceof StartTLSExtendedRequest) {
+                try {
+                    final StartTLSExtendedRequest request = (StartTLSExtendedRequest) promise.getRequest();
+                    conn.startTLS(request.getSSLContext(), request.getEnabledProtocols(),
+                            request.getEnabledCipherSuites(),
+                            new EmptyCompletionHandler<SSLEngine>() {
+                                @Override
+                                public void completed(final SSLEngine result) {
+                                    conn.setBindOrStartTLSInProgress(false);
+                                    promise.setResultOrError(decodedResponse);
+                                }
+
+                                @Override
+                                public void failed(final Throwable throwable) {
+                                    final Result errorResult = newResult(CLIENT_SIDE_LOCAL_ERROR)
+                                            .setCause(throwable).setDiagnosticMessage("SSL handshake failed");
+                                    conn.setBindOrStartTLSInProgress(false);
+                                    conn.close(null, false, errorResult);
+                                    promise.adaptErrorResult(errorResult);
+                                }
+                            });
+                    return;
+                } catch (final IOException e) {
+                    final Result errorResult = newResult(CLIENT_SIDE_LOCAL_ERROR).setCause(e)
+                            .setDiagnosticMessage(e.getMessage());
+                    promise.adaptErrorResult(errorResult);
+                    conn.close(null, false, errorResult);
+                    return;
+                }
+            }
+
+            promise.setResultOrError(decodedResponse);
+        }
+    }
+
+    /**
+     * Creates a client filter with provided options and max size of ASN1
+     * elements.
+     *
+     * @param options
+     *            allow to control how request and responses are decoded
+     * @param maxASN1ElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     */
+    LDAPClientFilter(final DecodeOptions options, final int maxASN1ElementSize) {
+        super(options, maxASN1ElementSize);
+    }
+
+    @Override
+    public void exceptionOccurred(final FilterChainContext ctx, final Throwable error) {
+        final Connection<?> connection = ctx.getConnection();
+        if (!connection.isOpen()) {
+            // Grizzly doesn't not deregister the read interest from the
+            // selector so closing the connection results in an EOFException.
+            // Just ignore errors on closed connections.
+            return;
+        }
+        final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(connection);
+
+        Result errorResult;
+        if (error instanceof EOFException) {
+            // FIXME: Is this the best result code?
+            errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_SERVER_DOWN).setCause(error);
+        } else {
+            // FIXME: what other sort of IOExceptions can be thrown?
+            // FIXME: Is this the best result code?
+            errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR).setCause(error);
+        }
+        ldapConnection.close(null, false, errorResult);
+    }
+
+    @Override
+    public NextAction handleClose(final FilterChainContext ctx) throws IOException {
+        final Connection<?> connection = ctx.getConnection();
+        final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.remove(connection);
+        if (ldapConnection != null) {
+            final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_SERVER_DOWN);
+            ldapConnection.close(null, false, errorResult);
+        }
+        return ctx.getInvokeAction();
+    }
+
+    @Override
+    final void handleReadException(FilterChainContext ctx, IOException e) {
+        final GrizzlyLDAPConnection ldapConnection = LDAP_CONNECTION_ATTR.get(ctx.getConnection());
+        final Result errorResult =
+                Responses.newResult(ResultCode.CLIENT_SIDE_DECODING_ERROR).setCause(e)
+                        .setDiagnosticMessage(e.getMessage());
+        ldapConnection.close(null, false, errorResult);
+    }
+
+    /**
+     * Returns the response handler associated to the provided connection and
+     * context.
+     * <p>
+     * If no handler exists yet for this context, a new one is created and
+     * recorded for the connection.
+     *
+     * @param ctx
+     *            current filter chain context
+     * @return the response handler associated to the context, which can be a
+     *         new one if no handler have been created yet
+     */
+    @Override
+    final LDAPBaseHandler getLDAPHandler(final FilterChainContext ctx) {
+        Connection<?> connection = ctx.getConnection();
+        ClientResponseHandler handler = RESPONSE_HANDLER_ATTR.get(connection);
+        if (handler == null) {
+            LDAPReader<ASN1BufferReader> reader =
+                    GrizzlyUtils.createReader(decodeOptions, maxASN1ElementSize, connection
+                            .getTransport().getMemoryManager());
+            handler = new ClientResponseHandler(reader);
+            RESPONSE_HANDLER_ATTR.set(connection, handler);
+        }
+        handler.setFilterChainContext(ctx);
+        return handler;
+    }
+
+    /**
+     * Associate a LDAP connection to the provided Grizzly connection.
+     *
+     * @param connection
+     *            Grizzly connection
+     * @param ldapConnection
+     *            LDAP connection
+     */
+    void registerConnection(final Connection<?> connection,
+            final GrizzlyLDAPConnection ldapConnection) {
+        LDAP_CONNECTION_ATTR.set(connection, ldapConnection);
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
new file mode 100644
index 0000000..faa4cbb
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
@@ -0,0 +1,878 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2012-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.io.AbstractLDAPMessageHandler;
+import org.forgerock.opendj.io.LDAP;
+import org.forgerock.opendj.io.LDAPReader;
+import org.forgerock.opendj.io.LDAPWriter;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConnectionSecurityLayer;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.ServerConnection;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.GenericExtendedResult;
+import org.forgerock.opendj.ldap.responses.IntermediateResponse;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.Options;
+import org.forgerock.util.Reject;
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.Grizzly;
+import org.glassfish.grizzly.attributes.Attribute;
+import org.glassfish.grizzly.filterchain.Filter;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.filterchain.FilterChainContext;
+import org.glassfish.grizzly.filterchain.NextAction;
+import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
+import org.glassfish.grizzly.ssl.SSLFilter;
+import org.glassfish.grizzly.ssl.SSLUtils;
+
+import static org.forgerock.opendj.grizzly.GrizzlyUtils.*;
+
+/**
+ * Grizzly filter implementation for decoding LDAP requests and handling server
+ * side logic for SSL and SASL operations over LDAP.
+ */
+final class LDAPServerFilter extends LDAPBaseFilter {
+
+    /** Provides an arbitrary write operation on a LDAP writer. */
+    private interface LDAPWrite<T> {
+        void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID, T message)
+                throws IOException;
+    }
+
+    /** Write operation for intermediate responses. */
+    private static final LDAPWrite<IntermediateResponse> INTERMEDIATE =
+            new LDAPWrite<IntermediateResponse>() {
+                @Override
+                public void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID,
+                        IntermediateResponse resp) throws IOException {
+                    writer.writeIntermediateResponse(messageID, resp);
+                }
+            };
+
+    private static abstract class AbstractHandler<R extends Result> implements
+            IntermediateResponseHandler, LdapResultHandler<R> {
+        protected final ClientContextImpl context;
+        protected final int messageID;
+
+        protected AbstractHandler(final ClientContextImpl context, final int messageID) {
+            this.messageID = messageID;
+            this.context = context;
+        }
+
+        @Override
+        public void handleResult(final R result) {
+            defaultHandleResult(result);
+        }
+
+        @Override
+        public final boolean handleIntermediateResponse(final IntermediateResponse response) {
+            writeMessage(INTERMEDIATE, response);
+            return true;
+        }
+
+        /**
+         * Default implementation of result handling, that delegate the actual
+         * write operation to {@code writeResult} method.
+         */
+        private void defaultHandleResult(final R result) {
+            writeMessage(new LDAPWrite<R>() {
+                @Override
+                public void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID, R res)
+                        throws IOException {
+                    writeResult(writer, res);
+                }
+            }, result);
+        }
+
+        /**
+         * Write a result to provided LDAP writer.
+         *
+         * @param ldapWriter
+         *            provided writer
+         * @param result
+         *            to write
+         * @throws IOException
+         *             if an error occurs during writing
+         */
+        protected abstract void writeResult(final LDAPWriter<ASN1BufferWriter> ldapWriter,
+                final R result) throws IOException;
+
+        /**
+         * Write a message on LDAP writer.
+         *
+         * @param <T>
+         *            type of message to write
+         * @param ldapWrite
+         *            the specific write operation
+         * @param message
+         *            the message to write
+         */
+        protected final <T> void writeMessage(final LDAPWrite<T> ldapWrite, final T message) {
+            final LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+            try {
+                ldapWrite.perform(writer, messageID, message);
+                context.write(writer);
+            } catch (final IOException ioe) {
+                context.handleException(ioe);
+            } finally {
+                GrizzlyUtils.recycleWriter(writer);
+            }
+        }
+
+        /**
+         * Copy diagnostic message, matched DN and cause to new result from the
+         * given result.
+         *
+         * @param newResult
+         *            to update
+         * @param result
+         *            contains parameters to copy
+         */
+        protected final void populateNewResultFromResult(final R newResult, final Result result) {
+            newResult.setDiagnosticMessage(result.getDiagnosticMessage());
+            newResult.setMatchedDN(result.getMatchedDN());
+            newResult.setCause(result.getCause());
+            for (final Control control : result.getControls()) {
+                newResult.addControl(control);
+            }
+        }
+    }
+
+    private static final class AddHandler extends AbstractHandler<Result> {
+        private AddHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            handleResult(error.getResult());
+        }
+
+        @Override
+        public void writeResult(LDAPWriter<ASN1BufferWriter> writer, final Result result)
+                throws IOException {
+            writer.writeAddResult(messageID, result);
+        }
+    }
+
+    private static final class BindHandler extends AbstractHandler<BindResult> {
+        private BindHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            final Result result = error.getResult();
+            if (result instanceof BindResult) {
+                handleResult((BindResult) result);
+            } else {
+                final BindResult newResult = Responses.newBindResult(result.getResultCode());
+                populateNewResultFromResult(newResult, result);
+                handleResult(newResult);
+            }
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, BindResult result)
+                throws IOException {
+            writer.writeBindResult(messageID, result);
+        }
+    }
+
+    private static final class ClientContextImpl implements LDAPClientContext {
+        private final Connection<?> connection;
+        private final AtomicBoolean isClosed = new AtomicBoolean();
+        private ServerConnection<Integer> serverConnection;
+
+        private ClientContextImpl(final Connection<?> connection) {
+            this.connection = connection;
+        }
+
+        @Override
+        public void disconnect() {
+            disconnect0(null, null);
+        }
+
+        @Override
+        public void disconnect(final ResultCode resultCode, final String message) {
+            Reject.ifNull(resultCode);
+            final GenericExtendedResult notification =
+                    Responses.newGenericExtendedResult(resultCode).setOID(
+                            LDAP.OID_NOTICE_OF_DISCONNECTION).setDiagnosticMessage(message);
+            sendUnsolicitedNotification(notification);
+            disconnect0(resultCode, message);
+        }
+
+        @Override
+        public void enableConnectionSecurityLayer(final ConnectionSecurityLayer layer) {
+            synchronized (this) {
+                installFilter(new ConnectionSecurityLayerFilter(layer, connection.getTransport()
+                        .getMemoryManager()));
+            }
+        }
+
+        @Override
+        public void enableTLS(final SSLContext sslContext, final String[] protocols,
+                final String[] suites, final boolean wantClientAuth, final boolean needClientAuth) {
+            Reject.ifNull(sslContext);
+            synchronized (this) {
+                if (isTLSEnabled()) {
+                    throw new IllegalStateException("TLS already enabled");
+                }
+
+                final SSLEngineConfigurator sslEngineConfigurator =
+                        new SSLEngineConfigurator(sslContext, false, false, false);
+                sslEngineConfigurator.setEnabledCipherSuites(suites);
+                sslEngineConfigurator.setEnabledProtocols(protocols);
+                sslEngineConfigurator.setWantClientAuth(wantClientAuth);
+                sslEngineConfigurator.setNeedClientAuth(needClientAuth);
+                installFilter(new SSLFilter(sslEngineConfigurator, DUMMY_SSL_ENGINE_CONFIGURATOR));
+            }
+        }
+
+        @Override
+        public InetSocketAddress getLocalAddress() {
+            return (InetSocketAddress) connection.getLocalAddress();
+        }
+
+        @Override
+        public InetSocketAddress getPeerAddress() {
+            return (InetSocketAddress) connection.getPeerAddress();
+        }
+
+        @Override
+        public int getSecurityStrengthFactor() {
+            final SSLSession sslSession = getSSLSession();
+            if (sslSession != null) {
+                final String cipherString = sslSession.getCipherSuite();
+                for (final Object[] cipher : CIPHER_KEY_SIZES) {
+                    if (cipherString.contains((String) cipher[0])) {
+                        return (Integer) cipher[1];
+                    }
+                }
+            }
+            return 0;
+        }
+
+        @Override
+        public SSLSession getSSLSession() {
+            final SSLEngine sslEngine = SSLUtils.getSSLEngine(connection);
+            return sslEngine != null ? sslEngine.getSession() : null;
+        }
+
+        @Override
+        public boolean isClosed() {
+            return isClosed.get();
+        }
+
+        @Override
+        public void sendUnsolicitedNotification(final ExtendedResult notification) {
+            LDAPWriter<ASN1BufferWriter> writer = GrizzlyUtils.getWriter();
+            try {
+                writer.writeExtendedResult(0, notification);
+                connection.write(writer.getASN1Writer().getBuffer(), null);
+            } catch (final IOException ioe) {
+                handleException(ioe);
+            } finally {
+                GrizzlyUtils.recycleWriter(writer);
+            }
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append("LDAPClientContext(");
+            builder.append(getLocalAddress());
+            builder.append(',');
+            builder.append(getPeerAddress());
+            builder.append(')');
+            return builder.toString();
+        }
+
+        public void write(final LDAPWriter<ASN1BufferWriter> writer) {
+            connection.write(writer.getASN1Writer().getBuffer(), null);
+        }
+
+        private void disconnect0(final ResultCode resultCode, final String message) {
+            // Close this connection context.
+            if (isClosed.compareAndSet(false, true)) {
+                try {
+                    // Notify the server connection: it may be null if disconnect is
+                    // invoked during accept.
+                    if (serverConnection != null) {
+                        serverConnection.handleConnectionDisconnected(resultCode, message);
+                    }
+                } finally {
+                    // Close the connection.
+                    connection.closeSilently();
+                }
+            }
+        }
+
+        private ServerConnection<Integer> getServerConnection() {
+            return serverConnection;
+        }
+
+        private void handleClose(final int messageID, final UnbindRequest unbindRequest) {
+            // Close this connection context.
+            if (isClosed.compareAndSet(false, true)) {
+                try {
+                    // Notify the server connection: it may be null if disconnect is
+                    // invoked during accept.
+                    if (serverConnection != null) {
+                        serverConnection.handleConnectionClosed(messageID, unbindRequest);
+                    }
+                } finally {
+                    // If this close was a result of an unbind request then the
+                    // connection won't actually be closed yet. To avoid TIME_WAIT TCP
+                    // state, let the client disconnect.
+                    if (unbindRequest != null) {
+                        return;
+                    }
+
+                    // Close the connection.
+                    connection.closeSilently();
+                }
+            }
+        }
+
+        private void handleException(final Throwable error) {
+            // Close this connection context.
+            if (isClosed.compareAndSet(false, true)) {
+                try {
+                    // Notify the server connection: it may be null if disconnect is
+                    // invoked during accept.
+                    if (serverConnection != null) {
+                        serverConnection.handleConnectionError(error);
+                    }
+                } finally {
+                    // Close the connection.
+                    connection.closeSilently();
+                }
+            }
+        }
+
+        /**
+         * Installs a new Grizzly filter (e.g. SSL/SASL) beneath the top-level
+         * LDAP filter.
+         *
+         * @param filter
+         *            The filter to be installed.
+         */
+        private void installFilter(final Filter filter) {
+            GrizzlyUtils.addFilterToConnection(filter, connection);
+        }
+
+        /**
+         * Indicates whether or not TLS is enabled this provided connection.
+         *
+         * @return {@code true} if TLS is enabled on this connection, otherwise
+         *         {@code false}.
+         */
+        private boolean isTLSEnabled() {
+            synchronized (this) {
+                final FilterChain currentFilterChain = (FilterChain) connection.getProcessor();
+                for (final Filter filter : currentFilterChain) {
+                    if (filter instanceof SSLFilter) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+        private void setServerConnection(final ServerConnection<Integer> serverConnection) {
+            this.serverConnection = serverConnection;
+        }
+    }
+
+    private static final class CompareHandler extends AbstractHandler<CompareResult> {
+        private CompareHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            final Result result = error.getResult();
+            if (result instanceof CompareResult) {
+                handleResult((CompareResult) result);
+            } else {
+                final CompareResult newResult = Responses.newCompareResult(result.getResultCode());
+                populateNewResultFromResult(newResult, result);
+                handleResult(newResult);
+            }
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, CompareResult result)
+                throws IOException {
+            writer.writeCompareResult(messageID, result);
+        }
+    }
+
+    private static final class DeleteHandler extends AbstractHandler<Result> {
+        private DeleteHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            handleResult(error.getResult());
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, Result result)
+                throws IOException {
+            writer.writeDeleteResult(messageID, result);
+        }
+    }
+
+    private static final class ExtendedHandler<R extends ExtendedResult> extends AbstractHandler<R> {
+        private ExtendedHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            final Result result = error.getResult();
+            if (result instanceof ExtendedResult) {
+                handleResult((ExtendedResult) result);
+            } else {
+                final ExtendedResult newResult =
+                        Responses.newGenericExtendedResult(result.getResultCode());
+                newResult.setDiagnosticMessage(result.getDiagnosticMessage());
+                newResult.setMatchedDN(result.getMatchedDN());
+                newResult.setCause(result.getCause());
+                for (final Control control : result.getControls()) {
+                    newResult.addControl(control);
+                }
+                handleResult(newResult);
+            }
+        }
+
+        @Override
+        public void handleResult(final ExtendedResult result) {
+            writeMessage(new LDAPWrite<ExtendedResult>() {
+                @Override
+                public void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID,
+                        ExtendedResult message) throws IOException {
+                    writer.writeExtendedResult(messageID, message);
+                }
+            }, result);
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> ldapWriter, R result)
+                throws IOException {
+            // never called because handleResult(result) method is overriden in this class
+        }
+    }
+
+    private static final class ModifyDNHandler extends AbstractHandler<Result> {
+        private ModifyDNHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            handleResult(error.getResult());
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, Result result)
+                throws IOException {
+            writer.writeModifyDNResult(messageID, result);
+        }
+    }
+
+    private static final class ModifyHandler extends AbstractHandler<Result> {
+        private ModifyHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            handleResult(error.getResult());
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, Result result)
+                throws IOException {
+            writer.writeModifyResult(messageID, result);
+        }
+    }
+
+    private static final class SearchHandler extends AbstractHandler<Result> implements
+            SearchResultHandler {
+        private SearchHandler(final ClientContextImpl context, final int messageID) {
+            super(context, messageID);
+        }
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            writeMessage(new LDAPWrite<SearchResultEntry>() {
+                @Override
+                public void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID,
+                        SearchResultEntry sre) throws IOException {
+                    writer.writeSearchResultEntry(messageID, sre);
+                }
+            }, entry);
+            return true;
+        }
+
+        @Override
+        public void handleException(final LdapException error) {
+            handleResult(error.getResult());
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            writeMessage(new LDAPWrite<SearchResultReference>() {
+                @Override
+                public void perform(LDAPWriter<ASN1BufferWriter> writer, int messageID,
+                        SearchResultReference ref) throws IOException {
+                    writer.writeSearchResultReference(messageID, ref);
+                }
+            }, reference);
+            return true;
+        }
+
+        @Override
+        protected void writeResult(LDAPWriter<ASN1BufferWriter> writer, Result result)
+                throws IOException {
+            writer.writeSearchResult(messageID, result);
+        }
+    }
+    // @formatter:off
+
+    /**
+     * Map of cipher phrases to effective key size (bits). Taken from the
+     * following RFCs: 5289, 4346, 3268,4132 and 4162.
+     */
+    private static final Object[][] CIPHER_KEY_SIZES = {
+        { "_WITH_AES_256_CBC_",      256 },
+        { "_WITH_CAMELLIA_256_CBC_", 256 },
+        { "_WITH_AES_256_GCM_",      256 },
+        { "_WITH_3DES_EDE_CBC_",     112 },
+        { "_WITH_AES_128_GCM_",      128 },
+        { "_WITH_SEED_CBC_",         128 },
+        { "_WITH_CAMELLIA_128_CBC_", 128 },
+        { "_WITH_AES_128_CBC_",      128 },
+        { "_WITH_IDEA_CBC_",         128 },
+        { "_WITH_RC4_128_",          128 },
+        { "_WITH_FORTEZZA_CBC_",     96 },
+        { "_WITH_DES_CBC_",          56 },
+        { "_WITH_RC4_56_",           56 },
+        { "_WITH_RC2_CBC_40_",       40 },
+        { "_WITH_DES_CBC_40_",       40 },
+        { "_WITH_RC4_40_",           40 },
+        { "_WITH_DES40_CBC_",        40 },
+        { "_WITH_NULL_",             0 },
+    };
+
+    // @formatter:on
+    /** Default maximum request size for incoming requests. */
+    private static final int DEFAULT_MAX_REQUEST_SIZE = 5 * 1024 * 1024;
+
+    private static final Attribute<ClientContextImpl> LDAP_CONNECTION_ATTR =
+            Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("LDAPServerConnection");
+
+    private static final Attribute<ServerRequestHandler> REQUEST_HANDLER_ATTR =
+            Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("ServerRequestHandler");
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /**
+     * A dummy SSL client engine configurator as SSLFilter only needs server
+     * config. This prevents Grizzly from needlessly using JVM defaults which
+     * may be incorrectly configured.
+     */
+    private static final SSLEngineConfigurator DUMMY_SSL_ENGINE_CONFIGURATOR;
+    static {
+        try {
+            DUMMY_SSL_ENGINE_CONFIGURATOR =
+                    new SSLEngineConfigurator(new SSLContextBuilder().setTrustManager(
+                            TrustManagers.distrustAll()).getSSLContext());
+        } catch (GeneralSecurityException e) {
+            // This should never happen.
+            throw new IllegalStateException("Unable to create Dummy SSL Engine Configurator", e);
+        }
+    }
+
+    private final GrizzlyLDAPListener listener;
+
+    private static final class ServerRequestHandler extends AbstractLDAPMessageHandler implements
+            LDAPBaseHandler {
+        private final Connection<?> connection;
+        private final LDAPReader<ASN1BufferReader> reader;
+
+        /**
+         * Creates the handler with a connection.
+         *
+         * @param connection
+         *            connection this handler is associated with
+         * @param reader
+         *            LDAP reader to use for reading incoming messages
+         */
+        ServerRequestHandler(Connection<?> connection, LDAPReader<ASN1BufferReader> reader) {
+            this.connection = connection;
+            this.reader = reader;
+        }
+
+        /**
+         * Returns the LDAP reader.
+         *
+         * @return the reader to read incoming LDAP messages
+         */
+        @Override
+        public LDAPReader<ASN1BufferReader> getReader() {
+            return reader;
+        }
+
+        @Override
+        public void abandonRequest(final int messageID, final AbandonRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                conn.handleAbandon(messageID, request);
+            }
+        }
+
+        @Override
+        public void addRequest(final int messageID, final AddRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final AddHandler handler = new AddHandler(clientContext, messageID);
+                conn.handleAdd(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void bindRequest(final int messageID, final int version,
+                final GenericBindRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final AbstractHandler<BindResult> handler =
+                        new BindHandler(clientContext, messageID);
+                conn.handleBind(messageID, version, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void compareRequest(final int messageID, final CompareRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final CompareHandler handler = new CompareHandler(clientContext, messageID);
+                conn.handleCompare(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void deleteRequest(final int messageID, final DeleteRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final DeleteHandler handler = new DeleteHandler(clientContext, messageID);
+                conn.handleDelete(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public <R extends ExtendedResult> void extendedRequest(final int messageID,
+                final ExtendedRequest<R> request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final ExtendedHandler<R> handler = new ExtendedHandler<>(clientContext, messageID);
+                conn.handleExtendedRequest(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void modifyDNRequest(final int messageID, final ModifyDNRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final ModifyDNHandler handler = new ModifyDNHandler(clientContext, messageID);
+                conn.handleModifyDN(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void modifyRequest(final int messageID, final ModifyRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final ModifyHandler handler = new ModifyHandler(clientContext, messageID);
+                conn.handleModify(messageID, request, handler, handler);
+            }
+        }
+
+        @Override
+        public void searchRequest(final int messageID, final SearchRequest request) {
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.get(connection);
+            if (clientContext != null) {
+                final ServerConnection<Integer> conn = clientContext.getServerConnection();
+                final SearchHandler handler = new SearchHandler(clientContext, messageID);
+                conn.handleSearch(messageID, request, handler, handler, handler);
+            }
+        }
+
+        @Override
+        public void unbindRequest(final int messageID, final UnbindRequest request) {
+            // Remove the client context causing any subsequent LDAP
+            // traffic to be ignored.
+            final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.remove(connection);
+            if (clientContext != null) {
+                clientContext.handleClose(messageID, request);
+            }
+        }
+
+        @Override
+        public void unrecognizedMessage(final int messageID, final byte messageTag,
+                final ByteString messageBytes) {
+            exceptionOccurred(connection, newUnsupportedMessageException(messageID, messageTag,
+                    messageBytes));
+        }
+    }
+
+    /**
+     * Creates a server filter with provided listener, options and max size of
+     * ASN1 element.
+     *
+     * @param listener
+     *            listen for incoming connections
+     * @param options
+     *            control how to decode requests and responses
+     * @param maxASN1ElementSize
+     *            The maximum BER element size, or <code>0</code> to indicate
+     *            that there is no limit.
+     */
+    LDAPServerFilter(final GrizzlyLDAPListener listener, final DecodeOptions options,
+            final int maxASN1ElementSize) {
+        super(options, maxASN1ElementSize <= 0 ? DEFAULT_MAX_REQUEST_SIZE : maxASN1ElementSize);
+        this.listener = listener;
+    }
+
+    @Override
+    public void exceptionOccurred(final FilterChainContext ctx, final Throwable error) {
+        exceptionOccurred(ctx.getConnection(), error);
+    }
+
+    private static void exceptionOccurred(final Connection<?> connection, final Throwable error) {
+        final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.remove(connection);
+        if (clientContext != null) {
+            clientContext.handleException(error);
+        }
+    }
+
+    @Override
+    public NextAction handleAccept(final FilterChainContext ctx) throws IOException {
+        final Connection<?> connection = ctx.getConnection();
+        Options options = listener.getLDAPListenerOptions();
+        configureConnection(connection, logger, options);
+        try {
+            final ClientContextImpl clientContext = new ClientContextImpl(connection);
+            final ServerConnection<Integer> serverConn =
+                    listener.getConnectionFactory().handleAccept(clientContext);
+            clientContext.setServerConnection(serverConn);
+            LDAP_CONNECTION_ATTR.set(connection, clientContext);
+        } catch (final LdapException e) {
+            connection.close();
+        }
+
+        return ctx.getStopAction();
+    }
+
+    @Override
+    public NextAction handleClose(final FilterChainContext ctx) throws IOException {
+        final ClientContextImpl clientContext = LDAP_CONNECTION_ATTR.remove(ctx.getConnection());
+        if (clientContext != null) {
+            clientContext.handleClose(-1, null);
+        }
+        return ctx.getStopAction();
+    }
+
+    @Override
+    final void handleReadException(FilterChainContext ctx, IOException e) {
+        exceptionOccurred(ctx, e);
+    }
+
+    /**
+     * Returns the request handler associated to a connection.
+     * <p>
+     * If no handler exists yet for this context, a new one is created and
+     * recorded for the context.
+     *
+     * @param ctx
+     *            Context
+     * @return the response handler associated to the context, which can be a
+     *         new one if no handler have been created yet
+     */
+    @Override
+    final LDAPBaseHandler getLDAPHandler(final FilterChainContext ctx) {
+        Connection<?> connection = ctx.getConnection();
+        ServerRequestHandler handler = REQUEST_HANDLER_ATTR.get(connection);
+        if (handler == null) {
+            LDAPReader<ASN1BufferReader> reader =
+                    GrizzlyUtils.createReader(decodeOptions, maxASN1ElementSize, connection
+                            .getTransport().getMemoryManager());
+            handler = new ServerRequestHandler(connection, reader);
+            REQUEST_HANDLER_ATTR.set(connection, handler);
+        }
+        return handler;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/package-info.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/package-info.java
new file mode 100644
index 0000000..6437c04
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/package-info.java
@@ -0,0 +1,38 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+/**
+ * Provides an implementation of a transport provider using Grizzly as
+ * transport. This provider is named "Grizzly".
+ * <p>
+ * To be used, this implementation must be declared in the
+ * provider-configuration file
+ * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}
+ * with this single line:
+ *
+ * <pre>
+ * com.forgerock.opendj.ldap.GrizzlyTransportProvider
+ * </pre>
+ *
+ * To require that this implementation is used, you must set the transport
+ * provider to "Grizzly" using {@code LDAPOptions#setTransportProvider()}
+ * method if requesting a {@code LDAPConnectionFactory} or
+ * {@code LDAPListenerOptions#setTransportProvider()} method if requesting a
+ * {@code LDAPListener}. Otherwise there is no guarantee that this
+ * implementation will be used.
+ */
+package org.forgerock.opendj.grizzly;
+
diff --git a/opendj-sdk/opendj-grizzly/src/main/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider b/opendj-sdk/opendj-grizzly/src/main/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider
new file mode 100644
index 0000000..f16ebea
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/resources/META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider
@@ -0,0 +1,15 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2013 ForgeRock AS.
+com.forgerock.opendj.grizzly.GrizzlyTransportProvider
diff --git a/opendj-sdk/opendj-grizzly/src/main/resources/com/forgerock/opendj/grizzly/grizzly.properties b/opendj-sdk/opendj-grizzly/src/main/resources/com/forgerock/opendj/grizzly/grizzly.properties
new file mode 100644
index 0000000..9120648
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/main/resources/com/forgerock/opendj/grizzly/grizzly.properties
@@ -0,0 +1,24 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2013-2014 ForgeRock AS.
+LDAP_CONNECTION_REQUEST_TIMEOUT=The request has failed because no response \
+ was received from the server within the %d ms timeout
+LDAP_CONNECTION_CONNECT_TIMEOUT=The connection attempt to server %s has failed \
+ because the connection timeout period of %d ms was exceeded
+LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT=The bind or StartTLS request \
+ has failed because no response was received from the server within the %d ms \
+ timeout. The LDAP connection is now in an invalid state and can no longer be used
+LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT=The LDAP connection has \
+ failed because no bind or StartTLS response was received from the server \
+ within the %d ms timeout
diff --git a/opendj-sdk/opendj-grizzly/src/site/xdoc/index.xml.vm b/opendj-sdk/opendj-grizzly/src/site/xdoc/index.xml.vm
new file mode 100644
index 0000000..b210923
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/site/xdoc/index.xml.vm
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2013-2015 ForgeRock AS.
+-->
+<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+  <properties>
+    <title>About ${project.name}</title>
+    <author email="opendj-dev@forgerock.org">${project.organization.name}</author>
+  </properties>
+  <body>
+    <section name="About ${project.name}">
+      <p>
+        ${project.description}
+      </p>
+    </section>
+    <section name="Documentation for ${project.name}">
+      <p>
+        Javadoc for this module can be found <a href="apidocs/index.html">here</a>.
+      </p>
+    </section>
+    <section name="Get ${project.name}">
+      <p>
+        Start developing your applications by obtaining ${project.name}
+        using any of the following methods:
+    </p>
+      <subsection name="Maven">
+        <p>
+          By far the simplest method is to develop your application using Maven
+          and add the following settings to your <b>pom.xml</b>:
+        </p>
+        <source>&lt;repositories>
+  &lt;repository>
+    &lt;id>forgerock-staging-repository&lt;/id>
+    &lt;name>ForgeRock Release Repository&lt;/name>
+    &lt;url>${mavenRepoReleases}&lt;/url>
+    &lt;snapshots>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/snapshots>
+  &lt;/repository>
+  &lt;repository>
+    &lt;id>forgerock-snapshots-repository&lt;/id>
+    &lt;name>ForgeRock Snapshot Repository&lt;/name>
+    &lt;url>${mavenRepoSnapshots}&lt;/url>
+    &lt;releases>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/releases>
+  &lt;/repository>
+&lt;/repositories>
+
+...
+
+&lt;dependencies>
+  &lt;dependency>
+    &lt;groupId>${project.groupId}&lt;/groupId>
+    &lt;artifactId>${project.artifactId}&lt;/artifactId>
+    &lt;version>${project.version}&lt;/version>
+  &lt;/dependency>
+&lt;/dependencies></source>
+      </subsection>
+      <subsection name="Download">
+        <p>
+          If you are not using Maven then you will need to download a pre-built
+          binary from the ForgeRock Maven repository, along with any compile
+          time <a href="dependencies.html">dependencies</a>:
+        </p>
+        <ul>
+          <li><a href="${mavenRepoReleases}/org/forgerock/opendj/${project.artifactId}">Stable releases</a></li>
+          <li><a href="${mavenRepoSnapshots}/org/forgerock/opendj/${project.artifactId}/${project.version}">Latest development snapshot</a></li>
+        </ul>
+      </subsection>
+      <subsection name="Build">
+        <p>
+          For the DIY enthusiasts you can build it yourself by checking out the
+          latest code using <a href="source-repository.html">Subversion</a>
+          and building it with Maven 3.
+        </p>
+      </subsection>
+    </section>
+    <section name="Getting started">
+      <p>
+        The following example shows how ${project.name} may be used:
+      </p>
+      <source>TODO</source>
+    </section>
+  </body>
+</document>
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferReaderTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferReaderTestCase.java
new file mode 100644
index 0000000..c5cd7af
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferReaderTestCase.java
@@ -0,0 +1,40 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1ReaderTestCase;
+import org.glassfish.grizzly.memory.ByteBufferWrapper;
+import org.glassfish.grizzly.memory.MemoryManager;
+
+/**
+ * This class provides test cases for ASN1BufferReader.
+ */
+public class ASN1BufferReaderTestCase extends ASN1ReaderTestCase {
+    @Override
+    protected ASN1Reader getReader(final byte[] b, final int maxElementSize) throws IOException {
+        final ByteBufferWrapper buffer = new ByteBufferWrapper(ByteBuffer.wrap(b));
+        final ASN1BufferReader reader =
+                new ASN1BufferReader(maxElementSize, MemoryManager.DEFAULT_MEMORY_MANAGER);
+        reader.appendBytesRead(buffer);
+        return reader;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferWriterTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferWriterTestCase.java
new file mode 100644
index 0000000..bedc9f4
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ASN1BufferWriterTestCase.java
@@ -0,0 +1,61 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2011-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.io.ASN1WriterTestCase;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.memory.ByteBufferWrapper;
+import org.glassfish.grizzly.memory.MemoryManager;
+
+/**
+ * This class provides testcases for ASN1BufferWriter.
+ */
+public class ASN1BufferWriterTestCase extends ASN1WriterTestCase {
+
+    private final ASN1BufferWriter writer = new ASN1BufferWriter();
+
+    @Override
+    protected byte[] getEncodedBytes() throws IOException, DecodeException {
+        final Buffer buffer = writer.getBuffer();
+        final byte[] byteArray = new byte[buffer.remaining()];
+        buffer.get(byteArray);
+        return byteArray;
+    }
+
+    @Override
+    protected ASN1Reader getReader(final byte[] encodedBytes) throws DecodeException, IOException {
+        final ByteBufferWrapper buffer = new ByteBufferWrapper(ByteBuffer.wrap(encodedBytes));
+        final ASN1BufferReader reader =
+                new ASN1BufferReader(0, MemoryManager.DEFAULT_MEMORY_MANAGER);
+        reader.appendBytesRead(buffer);
+        return reader;
+    }
+
+    @Override
+    protected ASN1Writer getWriter() throws IOException {
+        writer.flush();
+        writer.recycle();
+        return writer;
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
new file mode 100644
index 0000000..a7ac5ed
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
@@ -0,0 +1,703 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
+import static org.forgerock.opendj.ldap.Connections.newFixedConnectionPool;
+import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.TestCaseUtils.findFreeSocketAddress;
+import static org.forgerock.opendj.ldap.TestCaseUtils.getServerSocketAddress;
+import static org.forgerock.opendj.ldap.requests.Requests.newCRAMMD5SASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newDigestMD5SASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.ConnectionPool;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.LDAPServer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.MockConnectionEventListener;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.ServerConnection;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.ResultHandler;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the {@code ConnectionFactory} classes.
+ */
+@SuppressWarnings("javadoc")
+public class ConnectionFactoryTestCase extends SdkTestCase {
+    /** Test timeout in ms for tests which need to wait for network events. */
+    private static final long TEST_TIMEOUT = 30L;
+    private static final long TEST_TIMEOUT_MS = TEST_TIMEOUT * 1000L;
+
+    /**
+     * Ensures that the LDAP Server is running.
+     *
+     * @throws Exception
+     *             If an unexpected problem occurs.
+     */
+    @BeforeClass
+    public void startServer() throws Exception {
+        TestCaseUtils.startServer();
+    }
+
+    @AfterClass
+    public void stopServer() throws Exception {
+        TestCaseUtils.stopServer();
+    }
+
+    /**
+     * Disables logging before the tests.
+     */
+    @BeforeClass
+    public void disableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.SEVERE);
+    }
+
+    /**
+     * Re-enable logging after the tests.
+     */
+    @AfterClass
+    public void enableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.INFO);
+    }
+
+    @DataProvider
+    Object[][] connectionFactories() throws Exception {
+        Object[][] factories = new Object[21][1];
+
+        // HeartBeatConnectionFactory
+        // Use custom search request.
+        SearchRequest request = Requests.newSearchRequest(
+            "uid=user.0,ou=people,o=test", SearchScope.BASE_OBJECT, "(objectclass=*)", "cn");
+
+        InetSocketAddress serverAddress = getServerSocketAddress();
+        factories[0][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions()
+                                                           .set(HEARTBEAT_ENABLED, true)
+                                                           .set(HEARTBEAT_INTERVAL, duration("1000 ms"))
+                                                           .set(HEARTBEAT_TIMEOUT, duration("2000 ms"))
+                                                           .set(HEARTBEAT_SEARCH_REQUEST, request));
+
+        // InternalConnectionFactory
+        factories[1][0] = Connections.newInternalConnectionFactory(LDAPServer.getInstance(), null);
+
+        // AuthenticatedConnectionFactory
+        final SimpleBindRequest anon = newSimpleBindRequest("", new char[0]);
+        factories[2][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions().set(AUTHN_BIND_REQUEST, anon));
+
+        // AuthenticatedConnectionFactory with multi-stage SASL
+        final CRAMMD5SASLBindRequest crammd5 = newCRAMMD5SASLBindRequest("id:user", "password".toCharArray());
+        factories[3][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions().set(AUTHN_BIND_REQUEST, crammd5));
+
+        // LDAPConnectionFactory with default options
+        factories[4][0] = new LDAPConnectionFactory(serverAddress.getHostName(), serverAddress.getPort());
+
+        // LDAPConnectionFactory with startTLS
+        SSLContext sslContext = new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()).getSSLContext();
+        final Options startTlsOptions = defaultOptions()
+                                   .set(SSL_CONTEXT, sslContext)
+                                   .set(SSL_USE_STARTTLS, true)
+                                   .set(SSL_ENABLED_CIPHER_SUITES,
+                                        asList("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
+                                                      "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
+                                                      "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
+                                                      "SSL_DH_anon_WITH_DES_CBC_SHA",
+                                                      "SSL_DH_anon_WITH_RC4_128_MD5",
+                                                      "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+                                                      "TLS_DH_anon_WITH_AES_256_CBC_SHA"));
+        factories[5][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    startTlsOptions);
+
+        // startTLS + SASL confidentiality
+        // Use IP address here so that DIGEST-MD5 host verification works if
+        // local host name is not localhost (e.g. on some machines it might be
+        // localhost.localdomain).
+        // FIXME: enable QOP once OPENDJ-514 is fixed.
+        final DigestMD5SASLBindRequest digestmd5 = newDigestMD5SASLBindRequest("id:user", "password".toCharArray())
+                .setCipher(DigestMD5SASLBindRequest.CIPHER_LOW);
+        factories[6][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    Options.copyOf(startTlsOptions).set(AUTHN_BIND_REQUEST, digestmd5));
+
+        // Connection pool and load balancing tests.
+        InetSocketAddress offlineSocketAddress1 = findFreeSocketAddress();
+        ConnectionFactory offlineServer1 =
+                Connections.newNamedConnectionFactory(
+                        new LDAPConnectionFactory(offlineSocketAddress1.getHostName(),
+                                offlineSocketAddress1.getPort()), "offline1");
+        InetSocketAddress offlineSocketAddress2 = findFreeSocketAddress();
+        ConnectionFactory offlineServer2 =
+                Connections.newNamedConnectionFactory(
+                        new LDAPConnectionFactory(offlineSocketAddress2.getHostName(),
+                                offlineSocketAddress2.getPort()), "offline2");
+        ConnectionFactory onlineServer =
+                Connections.newNamedConnectionFactory(
+                        new LDAPConnectionFactory(serverAddress.getHostName(),
+                                serverAddress.getPort()), "online");
+
+        // Connection pools.
+        factories[7][0] = newFixedConnectionPool(onlineServer, 10);
+
+        // Round robin.
+        factories[8][0] = newRoundRobinLoadBalancer(asList(onlineServer, offlineServer1), defaultOptions());
+        factories[9][0] = factories[8][0];
+        factories[10][0] = factories[8][0];
+        factories[11][0] = newRoundRobinLoadBalancer(asList(offlineServer1, onlineServer), defaultOptions());
+        factories[12][0] = newRoundRobinLoadBalancer(asList(offlineServer1, offlineServer2, onlineServer),
+                                                     defaultOptions());
+        factories[13][0] = newRoundRobinLoadBalancer(asList(newFixedConnectionPool(offlineServer1, 10),
+                                                            newFixedConnectionPool(onlineServer, 10)),
+                                                     defaultOptions());
+
+        // Fail-over.
+        factories[14][0] = newFailoverLoadBalancer(asList(onlineServer, offlineServer1), defaultOptions());
+        factories[15][0] = factories[14][0];
+        factories[16][0] = factories[14][0];
+        factories[17][0] = newFailoverLoadBalancer(asList(offlineServer1, onlineServer), defaultOptions());
+        factories[18][0] = newFailoverLoadBalancer(asList(offlineServer1, offlineServer2, onlineServer),
+                                                   defaultOptions());
+        factories[19][0] = newFailoverLoadBalancer(asList(newFixedConnectionPool(offlineServer1, 10),
+                                                          newFixedConnectionPool(onlineServer, 10)),
+                                                   defaultOptions());
+
+        factories[20][0] = newFixedConnectionPool(onlineServer, 10);
+
+        return factories;
+    }
+
+    /**
+     * Tests the async connection in the blocking mode. This is not fully async as it blocks on the promise.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "connectionFactories", timeOut = TEST_TIMEOUT_MS)
+    public void testBlockingPromiseNoHandler(ConnectionFactory factory) throws Exception {
+        final Promise<? extends Connection, LdapException> promise = factory.getConnectionAsync();
+        final Connection con = promise.get();
+        // quickly check if it is a valid connection.
+        // Don't use a result handler.
+        assertNotNull(con.readEntryAsync(DN.rootDN(), null).getOrThrow());
+        con.close();
+    }
+
+    /**
+     * Tests the non-blocking fully async connection using a handler.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "connectionFactories", timeOut = TEST_TIMEOUT_MS)
+    public void testNonBlockingPromiseWithHandler(ConnectionFactory factory) throws Exception {
+        // Use the promise to get the result asynchronously.
+        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
+
+        factory.getConnectionAsync().thenOnResult(new ResultHandler<Connection>() {
+            @Override
+            public void handleResult(Connection con) {
+                con.close();
+                promise.handleResult(con);
+            }
+        }).thenOnException(new ExceptionHandler<LdapException>() {
+            @Override
+            public void handleException(LdapException exception) {
+                promise.handleException(exception);
+            }
+
+        });
+
+        // Since we don't have anything to do, we would rather
+        // be notified by the promise when the other thread calls our handler.
+        // should do a timed wait rather?
+        promise.getOrThrow();
+    }
+
+    /**
+     * Tests the synchronous connection.
+     *
+     * @throws Exception
+     */
+    @Test(dataProvider = "connectionFactories", timeOut = TEST_TIMEOUT_MS)
+    public void testSynchronousConnection(ConnectionFactory factory) throws Exception {
+        final Connection con = factory.getConnection();
+        assertNotNull(con);
+        // quickly check if it is a valid connection.
+        assertTrue(con.readEntry("").getName().isRootDN());
+        con.close();
+    }
+
+    /**
+     * Verifies that LDAP connections take into consideration changes to the default schema post creation. See
+     * OPENDJ-159.
+     * <p/>
+     * This test is disabled because it cannot be run in parallel with rest of the test suite, because it modifies the
+     * global default schema.
+     *
+     * @throws Exception
+     *         If an unexpected error occurred.
+     */
+    @Test(enabled = false)
+    public void testSchemaUsage() throws Exception {
+        // Create a connection factory: this should always use the default
+        // schema, even if it is updated.
+        InetSocketAddress socketAddress = getServerSocketAddress();
+        final ConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
+                socketAddress.getPort());
+        final Schema defaultSchema = Schema.getDefaultSchema();
+
+        final Connection connection = factory.getConnection();
+        try {
+            // Simulate a client which reads the schema from the server and then
+            // sets it as the application default. We then want subsequent
+            // operations to use this schema, not the original default.
+            final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema());
+            builder.addAttributeType("( 0.9.2342.19200300.100.1.3 NAME 'mail' EQUALITY "
+                    + "caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch "
+                    + "SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )", false);
+            final Schema testSchema = builder.toSchema().asNonStrictSchema();
+            assertThat(testSchema.getWarnings()).isEmpty();
+            Schema.setDefaultSchema(testSchema);
+
+            // Read an entry containing the mail attribute.
+            final SearchResultEntry e = connection.readEntry("uid=user.0,ou=people,o=test");
+
+            assertThat(e.getAttribute("mail")).isNotNull();
+            assertThat(e.getAttribute(AttributeDescription.valueOf("mail", testSchema)))
+                    .isNotNull();
+        } finally {
+            // Restore original schema.
+            Schema.setDefaultSchema(defaultSchema);
+
+            // Close connection.
+            connection.close();
+        }
+    }
+
+    /**
+     * Tests connection pool closure.
+     *
+     * @throws Exception
+     *         If an unexpected exception occurred.
+     */
+    @Test
+    public void testConnectionPoolClose() throws Exception {
+        // We'll use a pool of 4 connections.
+        final int size = 4;
+
+        // Count number of real connections which are open.
+        final AtomicInteger realConnectionCount = new AtomicInteger();
+        final boolean[] realConnectionIsClosed = new boolean[size];
+        Arrays.fill(realConnectionIsClosed, true);
+
+        // Mock underlying connection factory which always succeeds.
+        final ConnectionFactory mockFactory = mock(ConnectionFactory.class);
+        when(mockFactory.getConnectionAsync()).thenAnswer(new Answer<LdapPromise<Connection>>() {
+
+            @Override
+            public LdapPromise<Connection> answer(InvocationOnMock invocation) throws Throwable {
+                // Update state.
+                final int connectionID = realConnectionCount.getAndIncrement();
+                realConnectionIsClosed[connectionID] = false;
+
+                // Mock connection decrements counter on close.
+                Connection mockConnection = mock(Connection.class);
+                doAnswer(new Answer<Void>() {
+                    @Override
+                    public Void answer(InvocationOnMock invocation) throws Throwable {
+                        realConnectionCount.decrementAndGet();
+                        realConnectionIsClosed[connectionID] = true;
+                        return null;
+                    }
+                }).when(mockConnection).close();
+                when(mockConnection.isValid()).thenReturn(true);
+                when(mockConnection.toString()).thenReturn("Mock connection " + connectionID);
+
+                return newSuccessfulLdapPromise(mockConnection);
+            }
+        });
+
+        ConnectionPool pool = newFixedConnectionPool(mockFactory, size);
+        Connection[] pooledConnections = new Connection[size];
+        for (int i = 0; i < size; i++) {
+            pooledConnections[i] = pool.getConnection();
+        }
+
+        // Pool is fully utilized.
+        assertThat(realConnectionCount.get()).isEqualTo(size);
+        assertThat(pooledConnections[0].isClosed()).isFalse();
+        assertThat(pooledConnections[1].isClosed()).isFalse();
+        assertThat(pooledConnections[2].isClosed()).isFalse();
+        assertThat(pooledConnections[3].isClosed()).isFalse();
+        assertThat(realConnectionIsClosed[0]).isFalse();
+        assertThat(realConnectionIsClosed[1]).isFalse();
+        assertThat(realConnectionIsClosed[2]).isFalse();
+        assertThat(realConnectionIsClosed[3]).isFalse();
+
+        // Release two connections.
+        pooledConnections[0].close();
+        pooledConnections[1].close();
+        assertThat(realConnectionCount.get()).isEqualTo(4);
+        assertThat(pooledConnections[0].isClosed()).isTrue();
+        assertThat(pooledConnections[1].isClosed()).isTrue();
+        assertThat(pooledConnections[2].isClosed()).isFalse();
+        assertThat(pooledConnections[3].isClosed()).isFalse();
+        assertThat(realConnectionIsClosed[0]).isFalse();
+        assertThat(realConnectionIsClosed[1]).isFalse();
+        assertThat(realConnectionIsClosed[2]).isFalse();
+        assertThat(realConnectionIsClosed[3]).isFalse();
+
+        // Close the pool closing the two connections immediately.
+        pool.close();
+        assertThat(realConnectionCount.get()).isEqualTo(2);
+        assertThat(pooledConnections[0].isClosed()).isTrue();
+        assertThat(pooledConnections[1].isClosed()).isTrue();
+        assertThat(pooledConnections[2].isClosed()).isFalse();
+        assertThat(pooledConnections[3].isClosed()).isFalse();
+        assertThat(realConnectionIsClosed[0]).isTrue();
+        assertThat(realConnectionIsClosed[1]).isTrue();
+        assertThat(realConnectionIsClosed[2]).isFalse();
+        assertThat(realConnectionIsClosed[3]).isFalse();
+
+        // Release two remaining connections and check that they get closed.
+        pooledConnections[2].close();
+        pooledConnections[3].close();
+        assertThat(realConnectionCount.get()).isEqualTo(0);
+        assertThat(pooledConnections[0].isClosed()).isTrue();
+        assertThat(pooledConnections[1].isClosed()).isTrue();
+        assertThat(pooledConnections[2].isClosed()).isTrue();
+        assertThat(pooledConnections[3].isClosed()).isTrue();
+        assertThat(realConnectionIsClosed[0]).isTrue();
+        assertThat(realConnectionIsClosed[1]).isTrue();
+        assertThat(realConnectionIsClosed[2]).isTrue();
+        assertThat(realConnectionIsClosed[3]).isTrue();
+    }
+
+    private static final class CloseNotify {
+        private final boolean closeOnAccept;
+        private final boolean doBindFirst;
+        private final boolean useEventListener;
+        private final boolean sendDisconnectNotification;
+
+        private CloseNotify(boolean closeOnAccept, boolean doBindFirst, boolean useEventListener,
+                boolean sendDisconnectNotification) {
+            this.closeOnAccept = closeOnAccept;
+            this.doBindFirst = doBindFirst;
+            this.useEventListener = useEventListener;
+            this.sendDisconnectNotification = sendDisconnectNotification;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("[");
+            if (closeOnAccept) {
+                builder.append(" closeOnAccept");
+            }
+            if (doBindFirst) {
+                builder.append(" doBindFirst");
+            }
+            if (useEventListener) {
+                builder.append(" useEventListener");
+            }
+            if (sendDisconnectNotification) {
+                builder.append(" sendDisconnectNotification");
+            }
+            builder.append(" ]");
+            return builder.toString();
+        }
+    }
+
+    @DataProvider
+    Object[][] closeNotifyConfig() {
+        // @formatter:off
+        return new Object[][] {
+            // closeOnAccept, doBindFirst, useEventListener, sendDisconnectNotification
+
+            // Close on accept.
+            { new CloseNotify(true,  false, false, false) },
+            { new CloseNotify(true,  false, true,  false) },
+
+            // Use disconnect.
+            { new CloseNotify(false, false, false, false) },
+            { new CloseNotify(false, false, false, true) },
+            { new CloseNotify(false, false, true,  false) },
+            { new CloseNotify(false, false, true,  true) },
+            { new CloseNotify(false, true,  false, false) },
+            { new CloseNotify(false, true,  false, true) },
+            { new CloseNotify(false, true,  true,  false) },
+            { new CloseNotify(false, true,  true,  true) },
+        };
+        // @formatter:on
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(dataProvider = "closeNotifyConfig")
+    public void testCloseNotify(final CloseNotify config) throws Exception {
+        final CountDownLatch connectLatch = new CountDownLatch(1);
+        final AtomicReference<LDAPClientContext> contextHolder = new AtomicReference<>();
+
+        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
+                mock(ServerConnectionFactory.class);
+        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
+                new Answer<ServerConnection<Integer>>() {
+
+                    @Override
+                    public ServerConnection<Integer> answer(InvocationOnMock invocation)
+                            throws Throwable {
+                        // Allow the context to be accessed from outside the mock.
+                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
+                        connectLatch.countDown(); /* is this needed? */
+                        if (config.closeOnAccept) {
+                            throw newLdapException(ResultCode.UNAVAILABLE);
+                        } else {
+                            // Return a mock connection which always succeeds for binds.
+                            ServerConnection<Integer> mockConnection = mock(ServerConnection.class);
+                            doAnswer(new Answer<Void>() {
+                                @Override
+                                public Void answer(InvocationOnMock invocation) throws Throwable {
+                                    LdapResultHandler<? super BindResult> resultHandler =
+                                            (LdapResultHandler<? super BindResult>) invocation
+                                                    .getArguments()[4];
+                                    resultHandler.handleResult(Responses
+                                            .newBindResult(ResultCode.SUCCESS));
+                                    return null;
+                                }
+                            }).when(mockConnection).handleBind(anyInt(), anyInt(),
+                                    any(BindRequest.class), any(IntermediateResponseHandler.class),
+                                    any(LdapResultHandler.class));
+                            return mockConnection;
+                        }
+                    }
+                });
+
+        LDAPListener listener = new LDAPListener(findFreeSocketAddress(), mockServer);
+        try {
+            LDAPConnectionFactory clientFactory =
+                    new LDAPConnectionFactory(listener.getHostName(), listener.getPort());
+            final Connection client = clientFactory.getConnection();
+            connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
+            MockConnectionEventListener mockListener = null;
+            try {
+                if (config.useEventListener) {
+                    mockListener = new MockConnectionEventListener();
+                    client.addConnectionEventListener(mockListener);
+                }
+                if (config.doBindFirst) {
+                    client.bind("cn=test", "password".toCharArray());
+                }
+                if (!config.closeOnAccept) {
+                    // Disconnect using client context.
+                    LDAPClientContext context = contextHolder.get();
+                    assertThat(context).isNotNull();
+                    assertThat(context.isClosed()).isFalse();
+                    if (config.sendDisconnectNotification) {
+                        context.disconnect(ResultCode.BUSY, "busy");
+                    } else {
+                        context.disconnect();
+                    }
+                    assertThat(context.isClosed()).isTrue();
+                }
+                // Block until remote close is signalled.
+                if (mockListener != null) {
+                    // Block using listener.
+                    mockListener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
+                    assertThat(mockListener.getInvocationCount()).isEqualTo(1);
+                    assertThat(mockListener.isDisconnectNotification()).isEqualTo(
+                            config.sendDisconnectNotification);
+                    assertThat(mockListener.getError()).isNotNull();
+                } else {
+                    // Block by spinning on isValid.
+                    waitForCondition(new Callable<Boolean>() {
+                        @Override
+                        public Boolean call() throws Exception {
+                            return !client.isValid();
+                        }
+                    });
+                }
+                assertThat(client.isValid()).isFalse();
+                assertThat(client.isClosed()).isFalse();
+            } finally {
+                client.close();
+            }
+            // Check state after remote close and local close.
+            assertThat(client.isValid()).isFalse();
+            assertThat(client.isClosed()).isTrue();
+            if (mockListener != null) {
+                mockListener.awaitClose(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(mockListener.getInvocationCount()).isEqualTo(2);
+            }
+        } finally {
+            listener.close();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testUnsolicitedNotifications() throws Exception {
+        final CountDownLatch connectLatch = new CountDownLatch(1);
+        final AtomicReference<LDAPClientContext> contextHolder = new AtomicReference<>();
+
+        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
+                mock(ServerConnectionFactory.class);
+        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
+                new Answer<ServerConnection<Integer>>() {
+
+                    @Override
+                    public ServerConnection<Integer> answer(InvocationOnMock invocation)
+                            throws Throwable {
+                        // Allow the context to be accessed from outside the mock.
+                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
+                        connectLatch.countDown(); /* is this needed? */
+                        return mock(ServerConnection.class);
+                    }
+                });
+
+        LDAPListener listener = new LDAPListener(findFreeSocketAddress(), mockServer);
+        try {
+            LDAPConnectionFactory clientFactory =
+                    new LDAPConnectionFactory(listener.getHostName(),
+                            listener.getPort());
+            final Connection client = clientFactory.getConnection();
+            connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
+            try {
+                MockConnectionEventListener mockListener = new MockConnectionEventListener();
+                client.addConnectionEventListener(mockListener);
+
+                // Send notification.
+                LDAPClientContext context = contextHolder.get();
+                assertThat(context).isNotNull();
+                context.sendUnsolicitedNotification(Responses.newGenericExtendedResult(
+                        ResultCode.OTHER).setOID("1.2.3.4"));
+                assertThat(context.isClosed()).isFalse();
+
+                // Block using listener.
+                mockListener.awaitNotification(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(mockListener.getInvocationCount()).isEqualTo(1);
+                assertThat(mockListener.getNotification()).isNotNull();
+                assertThat(mockListener.getNotification().getResultCode()).isEqualTo(
+                        ResultCode.OTHER);
+                assertThat(mockListener.getNotification().getOID()).isEqualTo("1.2.3.4");
+                assertThat(client.isValid()).isTrue();
+                assertThat(client.isClosed()).isFalse();
+            } finally {
+                client.close();
+            }
+        } finally {
+            listener.close();
+        }
+    }
+
+    @Test(description = "Test for OPENDJ-1121: Closing a connection after "
+            + "closing the connection factory causes NPE")
+    public void testFactoryCloseBeforeConnectionClose() throws Exception {
+        InetSocketAddress socketAddress = getServerSocketAddress();
+        final LDAPConnectionFactory ldap = new LDAPConnectionFactory(socketAddress.getHostName(),
+                                                                     socketAddress.getPort(),
+                                                                     defaultOptions()
+                                                                            .set(HEARTBEAT_ENABLED, true));
+        final ConnectionPool pool = newFixedConnectionPool(ldap, 2);
+        final ConnectionFactory factory = newFailoverLoadBalancer(singletonList(pool), defaultOptions());
+        Connection conn = null;
+        try {
+            conn = factory.getConnection();
+        } finally {
+            factory.close();
+            if (conn != null) {
+                conn.close();
+            }
+        }
+    }
+
+    private void waitForCondition(Callable<Boolean> condition) throws Exception {
+        long timeout = System.currentTimeMillis() + TEST_TIMEOUT_MS;
+        while (!condition.call()) {
+            Thread.yield();
+            if (System.currentTimeMillis() > timeout) {
+                throw new TimeoutException("Test timed out after " + TEST_TIMEOUT + " seconds");
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransportTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransportTestCase.java
new file mode 100644
index 0000000..605f748
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/DefaultTCPNIOTransportTestCase.java
@@ -0,0 +1,67 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012-2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
+import static org.forgerock.opendj.ldap.TestCaseUtils.findFreeSocketAddress;
+import static org.testng.Assert.assertTrue;
+
+import java.net.Socket;
+import java.net.SocketAddress;
+
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.ReferenceCountedObject;
+
+/**
+ * Tests DefaultTCPNIOTransport class.
+ */
+public class DefaultTCPNIOTransportTestCase extends SdkTestCase {
+    /**
+     * Tests the default transport.
+     * <p>
+     * FIXME: this test is disable because it does not clean up any of the
+     * connections it creates.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test(enabled = false)
+    public void testGetInstance() throws Exception {
+        // Create a transport.
+        final ReferenceCountedObject<TCPNIOTransport>.Reference transport =
+                DEFAULT_TRANSPORT.acquire();
+        SocketAddress socketAddress = findFreeSocketAddress();
+        transport.get().bind(socketAddress);
+
+        // Establish a socket connection to see if the transport factory works.
+        final Socket socket = new Socket();
+        try {
+            socket.connect(socketAddress);
+
+            // Successfully connected if there is no exception.
+            assertTrue(socket.isConnected());
+            // Don't stop the transport because it is shared with the ldap server.
+        } finally {
+            socket.close();
+            transport.release();
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java
new file mode 100644
index 0000000..349e2ca
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java
@@ -0,0 +1,447 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.ConnectionException;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.MockConnectionEventListener;
+import org.forgerock.opendj.ldap.ProviderNotFoundException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.ServerConnection;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.TimeoutResultException;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.Stubber;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.opendj.ldap.requests.Requests.*;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests the {@link LDAPConnectionFactory} class.
+ */
+@SuppressWarnings({ "javadoc", "unchecked" })
+public class GrizzlyLDAPConnectionFactoryTestCase extends SdkTestCase {
+    /**
+     * The number of test iterations for unit tests which attempt to expose
+     * potential race conditions. Manual testing has gone up to 10000
+     * iterations.
+     */
+    private static final int ITERATIONS = 100;
+
+    /** Test timeout for tests which need to wait for network events. */
+    private static final long TEST_TIMEOUT = 30L;
+
+    /*
+     * It is usually quite a bad code smell to share state between unit tests.
+     * However, in this case we want to re-use the same factories and listeners
+     * in order to avoid shutting down and restarting the transport for each
+     * iteration.
+     */
+
+    private final Semaphore abandonLatch = new Semaphore(0);
+    private final Semaphore bindLatch = new Semaphore(0);
+    private final Semaphore closeLatch = new Semaphore(0);
+    private final Semaphore connectLatch = new Semaphore(0);
+    private final Semaphore searchLatch = new Semaphore(0);
+    private final AtomicReference<LDAPClientContext> context = new AtomicReference<>();
+    private final LDAPListener server = createServer();
+    private final InetSocketAddress socketAddress = server.getSocketAddress();
+    public final ConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
+            socketAddress.getPort(), Options.defaultOptions().set(REQUEST_TIMEOUT, duration("1 ms")));
+    private final ConnectionFactory pool = Connections.newFixedConnectionPool(factory, 10);
+    private volatile ServerConnection<Integer> serverConnection;
+
+    @AfterClass
+    public void tearDown() {
+        pool.close();
+        factory.close();
+        server.close();
+    }
+
+    @Test(description = "OPENDJ-1197")
+    public void testClientSideConnectTimeout() throws Exception {
+        // Use an non-local unreachable network address.
+        final ConnectionFactory factory = new LDAPConnectionFactory("10.20.30.40", 1389,
+            Options.defaultOptions().set(CONNECT_TIMEOUT, duration("1 ms")));
+        try {
+            for (int i = 0; i < ITERATIONS; i++) {
+                final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
+                final Promise<? extends Connection, LdapException> connectionPromise = factory.getConnectionAsync();
+                connectionPromise.thenOnException(getExceptionHandler(promise));
+
+                ConnectionException e = (ConnectionException) promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+                // Wait for the connect to timeout.
+                try {
+                    connectionPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+                    fail("The connect request succeeded unexpectedly");
+                } catch (ConnectionException ce) {
+                    assertThat(ce.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+                }
+            }
+        } finally {
+            factory.close();
+        }
+    }
+
+    /**
+     * Unit test for OPENDJ-1247: a locally timed out bind request will leave a
+     * connection in an invalid state since a bind (or startTLS) is in progress
+     * and no other operations can be performed. Therefore, a timeout should
+     * cause the connection to become invalid and an appropriate connection
+     * event sent. In addition, no abandon request should be sent.
+     */
+    @Test
+    public void testClientSideTimeoutForBindRequest() throws Exception {
+        resetState();
+        registerBindEvent();
+        registerCloseEvent();
+
+        final Connection connection = factory.getConnection();
+        try {
+            waitForConnect();
+            final MockConnectionEventListener listener = new MockConnectionEventListener();
+            connection.addConnectionEventListener(listener);
+            final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
+            final LdapPromise<BindResult> bindPromise = connection.bindAsync(newSimpleBindRequest());
+            bindPromise.thenOnException(getExceptionHandler(promise));
+            waitForBind();
+
+            TimeoutResultException e = (TimeoutResultException) promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+            verifyResultCodeIsClientSideTimeout(e);
+
+            // Wait for the request to timeout.
+            try {
+                bindPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+                fail("The bind request succeeded unexpectedly");
+            } catch (TimeoutResultException te) {
+                verifyResultCodeIsClientSideTimeout(te);
+            }
+
+            /*
+             * The connection should no longer be valid, the event listener
+             * should have been notified, but no abandon should have been sent.
+             */
+            listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
+            assertThat(connection.isValid()).isFalse();
+            verifyResultCodeIsClientSideTimeout(listener.getError());
+            connection.close();
+            waitForClose();
+            verifyNoAbandonSent();
+        } finally {
+            connection.close();
+        }
+    }
+
+    /**
+     * Unit test for OPENDJ-1247: as per previous test, except this time verify
+     * that the connection failure removes the connection from a connection
+     * pool.
+     */
+    @Test
+    public void testClientSideTimeoutForBindRequestInConnectionPool() throws Exception {
+        resetState();
+        registerBindEvent();
+        registerCloseEvent();
+
+        for (int i = 0; i < ITERATIONS; i++) {
+            final Connection connection = pool.getConnection();
+            try {
+                waitForConnect();
+                final MockConnectionEventListener listener = new MockConnectionEventListener();
+                connection.addConnectionEventListener(listener);
+
+                // Now bind with timeout.
+                final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
+                final LdapPromise<BindResult> bindPromise = connection.bindAsync(newSimpleBindRequest());
+                bindPromise.thenOnException(getExceptionHandler(promise));
+                waitForBind();
+
+                // Wait for the request to timeout and check the handler was invoked.
+                TimeoutResultException e = (TimeoutResultException) promise.getOrThrow(5, TimeUnit.SECONDS);
+                verifyResultCodeIsClientSideTimeout(e);
+
+                // Now check the promise was completed as expected.
+                try {
+                    bindPromise.getOrThrow(5, TimeUnit.SECONDS);
+                    fail("The bind request succeeded unexpectedly");
+                } catch (TimeoutResultException te) {
+                    verifyResultCodeIsClientSideTimeout(te);
+                }
+
+                /*
+                 * The connection should no longer be valid, the event listener
+                 * should have been notified, but no abandon should have been
+                 * sent.
+                 */
+                listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(connection.isValid()).isFalse();
+                verifyResultCodeIsClientSideTimeout(listener.getError());
+                connection.close();
+                waitForClose();
+                verifyNoAbandonSent();
+            } finally {
+                connection.close();
+            }
+        }
+    }
+
+
+    /**
+     * Unit test for OPENDJ-1247: a locally timed out request which is not a
+     * bind or startTLS should result in a client side timeout error, but the
+     * connection should remain valid. In addition, no abandon request should be
+     * sent.
+     */
+    @Test
+    public void testClientSideTimeoutForSearchRequest() throws Exception {
+        resetState();
+        registerSearchEvent();
+        registerAbandonEvent();
+
+        for (int i = 0; i < ITERATIONS; i++) {
+            final Connection connection = factory.getConnection();
+            try {
+                waitForConnect();
+                final ConnectionEventListener listener = mock(ConnectionEventListener.class);
+                connection.addConnectionEventListener(listener);
+                final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
+                final LdapPromise<SearchResultEntry> connectionPromise =
+                        connection.readEntryAsync(DN.valueOf("cn=test"), null);
+                connectionPromise.thenOnException(getExceptionHandler(promise));
+                waitForSearch();
+
+                LdapException e = promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+                verifyResultCodeIsClientSideTimeout(e);
+                // Wait for the request to timeout.
+                try {
+                    connectionPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
+                    fail("The search request succeeded unexpectedly");
+                } catch (TimeoutResultException te) {
+                    verifyResultCodeIsClientSideTimeout(te);
+                }
+
+                // The connection should still be valid.
+                assertThat(connection.isValid()).isTrue();
+                verifyZeroInteractions(listener);
+                /*
+                 * FIXME: The search should have been abandoned (see comment in
+                 * LDAPConnection for explanation).
+                 */
+                // waitForAbandon();
+            } finally {
+                connection.close();
+            }
+        }
+    }
+
+    @Test
+    public void testCreateLDAPConnectionFactory() throws Exception {
+        // test no exception is thrown, which means transport provider is correctly loaded
+        InetSocketAddress socketAddress = findFreeSocketAddress();
+        LDAPConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(), socketAddress.getPort());
+        factory.close();
+    }
+
+    @Test(expectedExceptions = { ProviderNotFoundException.class },
+            expectedExceptionsMessageRegExp = "^The requested provider 'unknown' .*")
+    public void testCreateLDAPConnectionFactoryFailureProviderNotFound() throws Exception {
+        Options options = Options.defaultOptions().set(TRANSPORT_PROVIDER, "unknown");
+        InetSocketAddress socketAddress = findFreeSocketAddress();
+        LDAPConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
+                socketAddress.getPort(), options);
+        factory.close();
+    }
+
+    @Test
+    public void testCreateLDAPConnectionFactoryWithCustomClassLoader() throws Exception {
+        // test no exception is thrown, which means transport provider is correctly loaded
+        Options options = Options.defaultOptions()
+                                 .set(TRANSPORT_PROVIDER_CLASS_LOADER, Thread.currentThread().getContextClassLoader());
+        InetSocketAddress socketAddress = findFreeSocketAddress();
+        LDAPConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
+                socketAddress.getPort(), options);
+        factory.close();
+    }
+
+    /**
+     * This unit test exposes the bug raised in issue OPENDJ-1156: NPE in
+     * ReferenceCountedObject after shutting down directory.
+     */
+    @Test
+    public void testResourceManagement() throws Exception {
+        resetState();
+
+        for (int i = 0; i < ITERATIONS; i++) {
+            final Connection connection = factory.getConnection();
+            try {
+                waitForConnect();
+                final MockConnectionEventListener listener = new MockConnectionEventListener();
+                connection.addConnectionEventListener(listener);
+
+                // Perform remote disconnect which will trigger a client side connection error.
+                context.get().disconnect();
+
+                // Wait for the error notification to reach the client.
+                listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
+            } finally {
+                connection.close();
+            }
+        }
+    }
+
+    private LDAPListener createServer() {
+        try {
+            return new LDAPListener(findFreeSocketAddress(),
+                    new ServerConnectionFactory<LDAPClientContext, Integer>() {
+                        @Override
+                        public ServerConnection<Integer> handleAccept(
+                                final LDAPClientContext clientContext) throws LdapException {
+                            context.set(clientContext);
+                            connectLatch.release();
+                            return serverConnection;
+                        }
+                    });
+        } catch (IOException e) {
+            fail("Unable to create LDAP listener", e);
+            return null;
+        }
+    }
+
+    private ExceptionHandler<LdapException> getExceptionHandler(
+            final PromiseImpl<LdapException, NeverThrowsException> promise) {
+        return new ExceptionHandler<LdapException>() {
+            @Override
+            public void handleException(LdapException exception) {
+                promise.handleResult(exception);
+            }
+        };
+    }
+
+    private Stubber notifyEvent(final Semaphore latch) {
+        return doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                latch.release();
+                return null;
+            }
+        });
+    }
+
+    private void registerAbandonEvent() {
+        notifyEvent(abandonLatch).when(serverConnection).handleAbandon(any(Integer.class),
+                any(AbandonRequest.class));
+    }
+
+    private void registerBindEvent() {
+        notifyEvent(bindLatch).when(serverConnection).handleBind(any(Integer.class), anyInt(),
+                any(BindRequest.class), any(IntermediateResponseHandler.class),
+                any(LdapResultHandler.class));
+    }
+
+    private void registerCloseEvent() {
+        notifyEvent(closeLatch).when(serverConnection).handleConnectionClosed(any(Integer.class),
+                any(UnbindRequest.class));
+    }
+
+    private void registerSearchEvent() {
+        notifyEvent(searchLatch).when(serverConnection).handleSearch(any(Integer.class), any(SearchRequest.class),
+            any(IntermediateResponseHandler.class), any(SearchResultHandler.class), any(LdapResultHandler.class));
+    }
+
+    private void resetState() {
+        connectLatch.drainPermits();
+        abandonLatch.drainPermits();
+        bindLatch.drainPermits();
+        searchLatch.drainPermits();
+        closeLatch.drainPermits();
+        context.set(null);
+        serverConnection = mock(ServerConnection.class);
+    }
+
+    private void verifyNoAbandonSent() {
+        verify(serverConnection, never()).handleAbandon(any(Integer.class),
+                any(AbandonRequest.class));
+    }
+
+    private void verifyResultCodeIsClientSideTimeout(LdapException error) {
+        assertThat(error.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT);
+    }
+
+    @SuppressWarnings("unused")
+    private void waitForAbandon() throws InterruptedException {
+        waitForEvent(abandonLatch);
+    }
+
+    private void waitForBind() throws InterruptedException {
+        waitForEvent(bindLatch);
+    }
+
+    private void waitForClose() throws InterruptedException {
+        waitForEvent(closeLatch);
+    }
+
+    private void waitForConnect() throws InterruptedException {
+        waitForEvent(connectLatch);
+    }
+
+    private void waitForEvent(final Semaphore latch) throws InterruptedException {
+        assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue();
+    }
+
+    private void waitForSearch() throws InterruptedException {
+        waitForEvent(searchLatch);
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java
new file mode 100644
index 0000000..9e44e68
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java
@@ -0,0 +1,119 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.grizzly;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.REQUEST_TIMEOUT;
+import java.net.InetSocketAddress;
+
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.TimeoutResultException;
+import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.mockito.ArgumentCaptor;
+import org.testng.annotations.Test;
+
+/**
+ * Tests LDAP connection implementation class.
+ */
+@SuppressWarnings("javadoc")
+public class GrizzlyLDAPConnectionTestCase extends SdkTestCase {
+
+    /**
+     * Tests that a normal request is subject to client side timeout checking.
+     */
+    @Test
+    public void testRequestTimeout() throws Exception {
+        doTestRequestTimeout(false);
+    }
+
+    /**
+     * Tests that a persistent search request is not subject to client side
+     * timeout checking.
+     */
+    @Test
+    public void testRequestTimeoutPersistentSearch() throws Exception {
+        doTestRequestTimeout(true);
+    }
+
+    private void doTestRequestTimeout(boolean isPersistentSearch) throws Exception {
+        InetSocketAddress address = TestCaseUtils.findFreeSocketAddress();
+
+        /*
+         * Use a mock server implementation which will ignore incoming requests
+         * and leave the client waiting forever for a response.
+         */
+        @SuppressWarnings("unchecked")
+        LDAPListener listener =
+                new LDAPListener(address, Connections
+                        .newServerConnectionFactory(mock(RequestHandler.class)));
+
+        /*
+         * Use a very long time out in order to prevent the timeout thread from
+         * triggering the timeout.
+         */
+        GrizzlyLDAPConnectionFactory factory = new GrizzlyLDAPConnectionFactory(address.getHostName(),
+                                                                  address.getPort(),
+                                                                  Options.defaultOptions()
+                                                                         .set(REQUEST_TIMEOUT, duration("100 ms")));
+        GrizzlyLDAPConnection connection = (GrizzlyLDAPConnection) factory.getConnectionAsync().getOrThrow();
+        try {
+            SearchRequest request =
+                    Requests.newSearchRequest("dc=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+            if (isPersistentSearch) {
+                request.addControl(PersistentSearchRequestControl.newControl(true, true, true));
+            }
+            SearchResultHandler searchHandler = mock(SearchResultHandler.class);
+            @SuppressWarnings("unchecked")
+            ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
+            connection.searchAsync(request, null, searchHandler).thenOnException(exceptionHandler);
+
+            // Pass in a time which is guaranteed to trigger expiration.
+            connection.handleTimeout(System.currentTimeMillis() + 1000000);
+            if (isPersistentSearch) {
+                verifyZeroInteractions(searchHandler);
+            } else {
+                ArgumentCaptor<LdapException> arg = ArgumentCaptor.forClass(LdapException.class);
+                verify(exceptionHandler).handleException(arg.capture());
+                assertThat(arg.getValue()).isInstanceOf(TimeoutResultException.class);
+                assertThat(arg.getValue().getResult().getResultCode()).isEqualTo(
+                        ResultCode.CLIENT_SIDE_TIMEOUT);
+            }
+        } finally {
+            connection.close();
+            listener.close();
+            factory.close();
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
new file mode 100644
index 0000000..cd22bc3
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
@@ -0,0 +1,709 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionException;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ProviderNotFoundException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.ServerConnection;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.PromiseImpl;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static java.util.Arrays.asList;
+import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.Connections.newFixedConnectionPool;
+import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.LDAPListener.*;
+import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.mockito.Mockito.*;
+
+/** Tests the LDAPListener class. */
+@SuppressWarnings("javadoc")
+public class GrizzlyLDAPListenerTestCase extends SdkTestCase {
+
+    private static class MockServerConnection implements ServerConnection<Integer> {
+        final PromiseImpl<Throwable, LdapException> connectionError = PromiseImpl.create();
+        final PromiseImpl<LDAPClientContext, LdapException> context = PromiseImpl.create();
+        final CountDownLatch isClosed = new CountDownLatch(1);
+
+        MockServerConnection() {
+            // Do nothing.
+        }
+
+        @Override
+        public void handleAbandon(final Integer requestContext, final AbandonRequest request)
+                throws UnsupportedOperationException {
+            // Do nothing.
+        }
+
+        @Override
+        public void handleAdd(final Integer requestContext, final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public void handleBind(final Integer requestContext, final int version,
+                final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<BindResult> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public void handleCompare(final Integer requestContext, final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<CompareResult> resultHandler)
+                throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newCompareResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public void handleConnectionClosed(final Integer requestContext, final UnbindRequest request) {
+            isClosed.countDown();
+        }
+
+        @Override
+        public void handleConnectionDisconnected(final ResultCode resultCode, final String message) {
+            // Do nothing.
+        }
+
+        @Override
+        public void handleConnectionError(final Throwable error) {
+            connectionError.handleResult(error);
+        }
+
+        @Override
+        public void handleDelete(final Integer requestContext, final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public <R extends ExtendedResult> void handleExtendedRequest(final Integer requestContext,
+                final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<R> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleException(newLdapException(request
+                    .getResultDecoder().newExtendedErrorResult(ResultCode.PROTOCOL_ERROR, "",
+                            "Extended operation " + request.getOID() + " not supported")));
+        }
+
+        @Override
+        public void handleModify(final Integer requestContext, final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public void handleModifyDN(final Integer requestContext, final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
+        }
+
+        @Override
+        public void handleSearch(final Integer requestContext, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
+            final LdapResultHandler<Result> resultHandler) throws UnsupportedOperationException {
+            resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
+        }
+    }
+
+    private static class MockServerConnectionFactory implements
+            ServerConnectionFactory<LDAPClientContext, Integer> {
+        private final MockServerConnection serverConnection;
+
+        private MockServerConnectionFactory(final MockServerConnection serverConnection) {
+            this.serverConnection = serverConnection;
+        }
+
+        @Override
+        public ServerConnection<Integer> handleAccept(final LDAPClientContext clientContext) throws LdapException {
+            serverConnection.context.handleResult(clientContext);
+            return serverConnection;
+        }
+    }
+
+    /** Disables logging before the tests. */
+    @BeforeClass
+    public void disableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.SEVERE);
+    }
+
+    /** Re-enable logging after the tests. */
+    @AfterClass
+    public void enableLogging() {
+        TestCaseUtils.setDefaultLogLevel(Level.INFO);
+    }
+
+    /** Test creation of LDAP listener with default transport provider. */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testCreateLDAPListener() throws Exception {
+        // test no exception is thrown, which means transport provider is
+        // correctly loaded
+        LDAPListener listener = new LDAPListener(findFreeSocketAddress(), mock(ServerConnectionFactory.class));
+        listener.close();
+    }
+
+    /** Test creation of LDAP listener with default transport provider and custom class loader. */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testCreateLDAPListenerWithCustomClassLoader() throws Exception {
+        // test no exception is thrown, which means transport provider is correctly loaded
+        Options options = defaultOptions().set(TRANSPORT_PROVIDER_CLASS_LOADER,
+                                                       Thread.currentThread().getContextClassLoader());
+        LDAPListener listener = new LDAPListener(findFreeSocketAddress(),
+                mock(ServerConnectionFactory.class), options);
+        listener.close();
+    }
+
+    /** Test creation of LDAP listener with unknown transport provider. */
+    @SuppressWarnings({ "unchecked" })
+    @Test(expectedExceptions = ProviderNotFoundException.class,
+        expectedExceptionsMessageRegExp = "^The requested provider 'unknown' .*")
+    public void testCreateLDAPListenerFailureProviderNotFound() throws Exception {
+        Options options = defaultOptions().set(TRANSPORT_PROVIDER, "unknown");
+        LDAPListener listener = new LDAPListener(findFreeSocketAddress(), mock(ServerConnectionFactory.class), options);
+        listener.close();
+    }
+
+    /**
+     * Tests basic LDAP listener functionality.
+     *
+     * @throws Exception
+     *             If an unexpected exception occurred.
+     */
+    @Test(timeOut = 10000)
+    public void testLDAPListenerBasic() throws Exception {
+        final MockServerConnection serverConnection = new MockServerConnection();
+        final MockServerConnectionFactory serverConnectionFactory =
+                new MockServerConnectionFactory(serverConnection);
+        final LDAPListener listener =
+                new LDAPListener(new InetSocketAddress(0), serverConnectionFactory);
+        try {
+            // Connect and close.
+            final Connection connection =
+                    new LDAPConnectionFactory(listener.getHostName(),
+                            listener.getPort()).getConnection();
+            assertThat(serverConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+            assertThat(serverConnection.isClosed.getCount()).isEqualTo(1);
+            connection.close();
+            assertThat(serverConnection.isClosed.await(10, TimeUnit.SECONDS)).isTrue();
+        } finally {
+            listener.close();
+        }
+    }
+
+    /**
+     * Tests LDAP listener which attempts to open a connection to a remote
+     * offline server at the point when the listener accepts the client
+     * connection.
+     *
+     * @throws Exception
+     *             If an unexpected exception occurred.
+     */
+    @Test(enabled = false)
+    public void testLDAPListenerLoadBalanceDuringHandleAccept() throws Exception {
+        // Online server listener.
+        final MockServerConnection onlineServerConnection = new MockServerConnection();
+        final MockServerConnectionFactory onlineServerConnectionFactory =
+                new MockServerConnectionFactory(onlineServerConnection);
+        final LDAPListener onlineServerListener =
+                new LDAPListener(findFreeSocketAddress(), onlineServerConnectionFactory);
+
+        try {
+            // Connection pool and load balancing tests.
+            InetSocketAddress offlineAddress1 = findFreeSocketAddress();
+            final ConnectionFactory offlineServer1 =
+                    Connections.newNamedConnectionFactory(new LDAPConnectionFactory(
+                            offlineAddress1.getHostName(),
+                            offlineAddress1.getPort()), "offline1");
+            InetSocketAddress offlineAddress2 = findFreeSocketAddress();
+            final ConnectionFactory offlineServer2 =
+                    Connections.newNamedConnectionFactory(new LDAPConnectionFactory(
+                            offlineAddress2.getHostName(),
+                            offlineAddress2.getPort()), "offline2");
+            final ConnectionFactory onlineServer =
+                    Connections.newNamedConnectionFactory(new LDAPConnectionFactory(
+                            onlineServerListener.getHostName(),
+                            onlineServerListener.getPort()), "online");
+
+            // Round robin.
+            final ConnectionFactory loadBalancer =
+                    newRoundRobinLoadBalancer(asList(newFixedConnectionPool(offlineServer1, 10),
+                                                     newFixedConnectionPool(offlineServer2, 10),
+                                                     newFixedConnectionPool(onlineServer, 10)),
+                                              defaultOptions());
+
+            final MockServerConnection proxyServerConnection = new MockServerConnection();
+            final MockServerConnectionFactory proxyServerConnectionFactory =
+                    new MockServerConnectionFactory(proxyServerConnection) {
+
+                        @Override
+                        public ServerConnection<Integer> handleAccept(
+                                final LDAPClientContext clientContext) throws LdapException {
+                            // Get connection from load balancer, this should
+                            // fail over twice before getting connection to
+                            // online server.
+                            loadBalancer.getConnection().close();
+                            return super.handleAccept(clientContext);
+                        }
+
+                    };
+
+            final LDAPListener proxyListener =
+                    new LDAPListener(findFreeSocketAddress(), proxyServerConnectionFactory);
+            try {
+                // Connect and close.
+                final Connection connection =
+                        new LDAPConnectionFactory(proxyListener.getHostName(),
+                                proxyListener.getPort()).getConnection();
+
+                assertThat(proxyServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+                assertThat(onlineServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+
+                // Wait for connect/close to complete.
+                connection.close();
+
+                proxyServerConnection.isClosed.await();
+            } finally {
+                proxyListener.close();
+            }
+        } finally {
+            onlineServerListener.close();
+        }
+    }
+
+    /**
+     * Tests LDAP listener which attempts to open a connection to a load
+     * balancing pool at the point when the listener handles a bind request.
+     *
+     * @throws Exception
+     *             If an unexpected exception occurred.
+     */
+    @Test(timeOut = 10000)
+    public void testLDAPListenerLoadBalanceDuringHandleBind() throws Exception {
+        // Online server listener.
+        final MockServerConnection onlineServerConnection = new MockServerConnection();
+        final MockServerConnectionFactory onlineServerConnectionFactory =
+                new MockServerConnectionFactory(onlineServerConnection);
+        final LDAPListener onlineServerListener =
+                new LDAPListener(new InetSocketAddress(0), onlineServerConnectionFactory);
+
+        try {
+            // Connection pool and load balancing tests.
+            InetSocketAddress offlineAddress1 = findFreeSocketAddress();
+            final ConnectionFactory offlineServer1 =
+                    Connections.newNamedConnectionFactory(
+                            new LDAPConnectionFactory(offlineAddress1.getHostName(),
+                                    offlineAddress1.getPort()), "offline1");
+            InetSocketAddress offlineAddress2 = findFreeSocketAddress();
+            final ConnectionFactory offlineServer2 =
+                    Connections.newNamedConnectionFactory(
+                            new LDAPConnectionFactory(offlineAddress2.getHostName(),
+                                    offlineAddress2.getPort()), "offline2");
+            final ConnectionFactory onlineServer =
+                    Connections.newNamedConnectionFactory(
+                            new LDAPConnectionFactory(onlineServerListener.getHostName(),
+                                    onlineServerListener.getPort()), "online");
+
+            // Round robin.
+            final ConnectionFactory loadBalancer =
+                    newRoundRobinLoadBalancer(asList(newFixedConnectionPool(offlineServer1, 10),
+                                                     newFixedConnectionPool(offlineServer2, 10),
+                                                     newFixedConnectionPool(onlineServer, 10)),
+                                              defaultOptions());
+
+            final MockServerConnection proxyServerConnection = new MockServerConnection() {
+
+                @Override
+                public void handleBind(final Integer requestContext, final int version,
+                        final BindRequest request,
+                        final IntermediateResponseHandler intermediateResponseHandler,
+                        final LdapResultHandler<BindResult> resultHandler)
+                        throws UnsupportedOperationException {
+                    // Get connection from load balancer, this should fail over
+                    // twice before getting connection to online server.
+                    try {
+                        loadBalancer.getConnection().close();
+                        resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
+                    } catch (final Exception e) {
+                        // Unexpected.
+                        resultHandler.handleException(newLdapException(ResultCode.OTHER,
+                                "Unexpected exception when connecting to load balancer", e));
+                    }
+                }
+
+            };
+            final MockServerConnectionFactory proxyServerConnectionFactory =
+                    new MockServerConnectionFactory(proxyServerConnection);
+
+            final LDAPListener proxyListener =
+                    new LDAPListener(new InetSocketAddress(0), proxyServerConnectionFactory);
+            try {
+                // Connect, bind, and close.
+                final Connection connection =
+                        new LDAPConnectionFactory(proxyListener.getHostName(),
+                                proxyListener.getPort()).getConnection();
+                try {
+                    connection.bind("cn=test", "password".toCharArray());
+
+                    assertThat(proxyServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+                    assertThat(onlineServerConnection.context.get(10, TimeUnit.SECONDS))
+                            .isNotNull();
+                } finally {
+                    connection.close();
+                }
+
+                // Wait for connect/close to complete.
+                proxyServerConnection.isClosed.await();
+            } finally {
+                proxyListener.close();
+            }
+        } finally {
+            onlineServerListener.close();
+        }
+    }
+
+    /**
+     * Tests LDAP listener which attempts to open a connection to a remote
+     * offline server at the point when the listener accepts the client
+     * connection.
+     *
+     * @throws Exception
+     *             If an unexpected exception occurred.
+     */
+    @Test(enabled = false)
+    public void testLDAPListenerProxyDuringHandleAccept() throws Exception {
+        final MockServerConnection onlineServerConnection = new MockServerConnection();
+        final MockServerConnectionFactory onlineServerConnectionFactory =
+                new MockServerConnectionFactory(onlineServerConnection);
+        final LDAPListener onlineServerListener =
+                new LDAPListener(findFreeSocketAddress(), onlineServerConnectionFactory);
+
+        try {
+            final MockServerConnection proxyServerConnection = new MockServerConnection();
+            final MockServerConnectionFactory proxyServerConnectionFactory =
+                    new MockServerConnectionFactory(proxyServerConnection) {
+
+                        @Override
+                        public ServerConnection<Integer> handleAccept(
+                                final LDAPClientContext clientContext) throws LdapException {
+                            // First attempt offline server.
+                            InetSocketAddress offlineAddress = findFreeSocketAddress();
+                            LDAPConnectionFactory lcf =
+                                    new LDAPConnectionFactory(offlineAddress.getHostName(),
+                                            offlineAddress.getPort());
+                            try {
+                                // This is expected to fail.
+                                lcf.getConnection().close();
+                                throw newLdapException(ResultCode.OTHER,
+                                        "Connection to offline server succeeded unexpectedly");
+                            } catch (final ConnectionException ce) {
+                                // This is expected - so go to online server.
+                                try {
+                                    lcf =
+                                            new LDAPConnectionFactory(
+                                                    onlineServerListener.getHostName(),
+                                                    onlineServerListener.getPort());
+                                    lcf.getConnection().close();
+                                } catch (final Exception e) {
+                                    // Unexpected.
+                                    throw newLdapException(
+                                                    ResultCode.OTHER,
+                                                    "Unexpected exception when connecting to online server",
+                                                    e);
+                                }
+                            } catch (final Exception e) {
+                                // Unexpected.
+                                throw newLdapException(
+                                                ResultCode.OTHER,
+                                                "Unexpected exception when connecting to offline server",
+                                                e);
+                            }
+
+                            return super.handleAccept(clientContext);
+                        }
+
+                    };
+            final LDAPListener proxyListener =
+                    new LDAPListener(findFreeSocketAddress(), proxyServerConnectionFactory);
+            try {
+                // Connect and close.
+                final Connection connection =
+                        new LDAPConnectionFactory(proxyListener.getHostName(),
+                                proxyListener.getPort()).getConnection();
+
+                assertThat(proxyServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+                assertThat(onlineServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+
+                connection.close();
+
+                // Wait for connect/close to complete.
+                proxyServerConnection.isClosed.await();
+            } finally {
+                proxyListener.close();
+            }
+        } finally {
+            onlineServerListener.close();
+        }
+    }
+
+    /**
+     * Tests LDAP listener which attempts to open a connection to a remote
+     * offline server at the point when the listener handles a bind request.
+     *
+     * @throws Exception
+     *             If an unexpected exception occurred.
+     */
+    @Test(timeOut = 10000)
+    public void testLDAPListenerProxyDuringHandleBind() throws Exception {
+        final MockServerConnection onlineServerConnection = new MockServerConnection();
+        final MockServerConnectionFactory onlineServerConnectionFactory =
+                new MockServerConnectionFactory(onlineServerConnection);
+        final LDAPListener onlineServerListener =
+                new LDAPListener(findFreeSocketAddress(), onlineServerConnectionFactory);
+
+        try {
+            final MockServerConnection proxyServerConnection = new MockServerConnection() {
+
+                @Override
+                public void handleBind(final Integer requestContext, final int version,
+                        final BindRequest request,
+                        final IntermediateResponseHandler intermediateResponseHandler,
+                        final LdapResultHandler<BindResult> resultHandler)
+                        throws UnsupportedOperationException {
+                    // First attempt offline server.
+                    InetSocketAddress offlineAddress = findFreeSocketAddress();
+                    LDAPConnectionFactory lcf = new LDAPConnectionFactory(offlineAddress.getHostName(),
+                            offlineAddress.getPort());
+                    try {
+                        // This is expected to fail.
+                        lcf.getConnection().close();
+                        resultHandler.handleException(newLdapException(
+                                ResultCode.OTHER,
+                                "Connection to offline server succeeded unexpectedly"));
+                    } catch (final ConnectionException ce) {
+                        // This is expected - so go to online server.
+                        try {
+                            lcf = new LDAPConnectionFactory(onlineServerListener.getHostName(),
+                                    onlineServerListener.getPort());
+                            lcf.getConnection().close();
+                            resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
+                        } catch (final Exception e) {
+                            // Unexpected.
+                            resultHandler.handleException(newLdapException(
+                                    ResultCode.OTHER,
+                                    "Unexpected exception when connecting to online server", e));
+                        }
+                    } catch (final Exception e) {
+                        // Unexpected.
+                        resultHandler.handleException(newLdapException(
+                                ResultCode.OTHER,
+                                "Unexpected exception when connecting to offline server", e));
+                    }
+                }
+
+            };
+            final MockServerConnectionFactory proxyServerConnectionFactory =
+                    new MockServerConnectionFactory(proxyServerConnection);
+            final LDAPListener proxyListener =
+                    new LDAPListener(findFreeSocketAddress(), proxyServerConnectionFactory);
+            try {
+                // Connect, bind, and close.
+                final Connection connection =
+                        new LDAPConnectionFactory(proxyListener.getHostName(),
+                                proxyListener.getPort()).getConnection();
+                try {
+                    connection.bind("cn=test", "password".toCharArray());
+
+                    assertThat(proxyServerConnection.context.get(10, TimeUnit.SECONDS)).isNotNull();
+                    assertThat(onlineServerConnection.context.get(10, TimeUnit.SECONDS))
+                            .isNotNull();
+                } finally {
+                    connection.close();
+                }
+
+                // Wait for connect/close to complete.
+                proxyServerConnection.isClosed.await();
+            } finally {
+                proxyListener.close();
+            }
+        } finally {
+            onlineServerListener.close();
+        }
+    }
+
+    /**
+     * Tests that an incoming request which is too big triggers the connection
+     * to be closed and an error notification to occur.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testMaxRequestSize() throws Exception {
+        final MockServerConnection serverConnection = new MockServerConnection();
+        final MockServerConnectionFactory factory =
+                new MockServerConnectionFactory(serverConnection);
+        final Options options = defaultOptions().set(REQUEST_MAX_SIZE_IN_BYTES, 2048);
+        final LDAPListener listener = new LDAPListener(findFreeSocketAddress(), factory, options);
+
+        Connection connection = null;
+        try {
+            connection = new LDAPConnectionFactory(listener.getHostName(), listener.getPort()).
+                    getConnection();
+
+            // Small request
+            connection.bind("cn=test", "password".toCharArray());
+            assertThat(serverConnection.context.get().isClosed()).isFalse();
+            assertThat(serverConnection.connectionError.isDone()).isFalse();
+
+            // Big but valid request.
+            final char[] password1 = new char[2000];
+            Arrays.fill(password1, 'a');
+            connection.bind("cn=test", password1);
+            assertThat(serverConnection.context.get().isClosed()).isFalse();
+            assertThat(serverConnection.connectionError.isDone()).isFalse();
+
+            // Big invalid request.
+            final char[] password2 = new char[2048];
+            Arrays.fill(password2, 'a');
+            try {
+                connection.bind("cn=test", password2);
+                fail("Big bind unexpectedly succeeded");
+            } catch (final LdapException e) {
+                // Expected exception.
+                assertThat(e.getResult().getResultCode()).isEqualTo(
+                        ResultCode.CLIENT_SIDE_SERVER_DOWN);
+
+                assertThat(serverConnection.connectionError.get(10, TimeUnit.SECONDS)).isNotNull();
+                assertThat(serverConnection.connectionError.get()).isInstanceOf(
+                        DecodeException.class);
+                assertThat(((DecodeException) serverConnection.connectionError.get()).isFatal())
+                        .isTrue();
+                assertThat(serverConnection.isClosed.getCount()).isEqualTo(1);
+                assertThat(serverConnection.context.get().isClosed()).isTrue();
+            }
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+            listener.close();
+        }
+    }
+
+    /**
+     * Tests server-side disconnection.
+     *
+     * @throws Exception
+     *             If an unexpected error occurred.
+     */
+    @Test
+    public void testServerDisconnect() throws Exception {
+        final MockServerConnection serverConnection = new MockServerConnection();
+        final MockServerConnectionFactory factory =
+                new MockServerConnectionFactory(serverConnection);
+        final LDAPListener listener = new LDAPListener(findFreeSocketAddress(), factory);
+
+        final Connection connection;
+        try {
+            // Connect and bind.
+            connection = new LDAPConnectionFactory(listener.getHostName(), listener.getPort()).getConnection();
+            try {
+                connection.bind("cn=test", "password".toCharArray());
+            } catch (final LdapException e) {
+                connection.close();
+                throw e;
+            }
+        } finally {
+            serverConnection.context.get().disconnect();
+            listener.close();
+        }
+
+        try {
+            // Connect and bind.
+            final Connection failedConnection =
+                    new LDAPConnectionFactory(listener.getHostName(),
+                            listener.getPort()).getConnection();
+            failedConnection.close();
+            connection.close();
+            fail("Connection attempt to closed listener succeeded unexpectedly");
+        } catch (final ConnectionException e) {
+            // Expected.
+        }
+
+        try {
+            connection.bind("cn=test", "password".toCharArray());
+            fail("Bind attempt on closed connection succeeded unexpectedly");
+        } catch (final LdapException e) {
+            // Expected.
+            assertThat(connection.isValid()).isFalse();
+            assertThat(connection.isClosed()).isFalse();
+        } finally {
+            connection.close();
+            assertThat(connection.isValid()).isFalse();
+            assertThat(connection.isClosed()).isTrue();
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java
new file mode 100644
index 0000000..73c7bae
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import org.forgerock.opendj.io.ASN1Reader;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.io.LDAPReader;
+import org.forgerock.opendj.io.LDAPReaderWriterTestCase;
+import org.forgerock.opendj.io.LDAPWriter;
+import org.forgerock.util.Options;
+import org.glassfish.grizzly.memory.HeapMemoryManager;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
+
+/**
+ * Tests for LDAPWriter / LDAPReader classes using specific implementations of
+ * ASN1 writer and ASN1 reader with Grizzly.
+ */
+public class GrizzlyLDAPReaderWriterTestCase extends LDAPReaderWriterTestCase {
+
+    @Override
+    protected LDAPWriter<? extends ASN1Writer> getLDAPWriter() {
+        return GrizzlyUtils.getWriter();
+    }
+
+    @Override
+    protected LDAPReader<? extends ASN1Reader> getLDAPReader() {
+        return GrizzlyUtils.createReader(Options.defaultOptions().get(LDAP_DECODE_OPTIONS), 0, new HeapMemoryManager());
+    }
+
+    @Override
+    protected void transferFromWriterToReader(LDAPWriter<? extends ASN1Writer> writer,
+            LDAPReader<? extends ASN1Reader> reader) {
+        ASN1BufferReader asn1Reader = (ASN1BufferReader) reader.getASN1Reader();
+        ASN1BufferWriter asn1Writer = (ASN1BufferWriter) writer.getASN1Writer();
+        asn1Reader.appendBytesRead(asn1Writer.getBuffer());
+    }
+
+}
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyUtilsTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyUtilsTestCase.java
new file mode 100644
index 0000000..c25eb8c
--- /dev/null
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyUtilsTestCase.java
@@ -0,0 +1,103 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.grizzly;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.opendj.ldap.SdkTestCase;
+import org.glassfish.grizzly.StandaloneProcessor;
+import org.glassfish.grizzly.filterchain.BaseFilter;
+import org.glassfish.grizzly.filterchain.FilterChain;
+import org.glassfish.grizzly.filterchain.FilterChainBuilder;
+import org.glassfish.grizzly.filterchain.TransportFilter;
+import org.glassfish.grizzly.ssl.SSLFilter;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class GrizzlyUtilsTestCase extends SdkTestCase {
+
+    private static final class DummyLDAPFilter extends BaseFilter { // only need type
+    }
+
+    private static final class FilterOne extends BaseFilter { // only need type
+    }
+
+    private static final class DummySSLFilter extends SSLFilter { // only need type
+    }
+
+    /**
+     * Default filter chain contains a transport filter and a ldap filter.
+     */
+    private FilterChain getDefaultFilterChain() {
+        return FilterChainBuilder.stateless().
+                add(new TransportFilter()).add(new DummyLDAPFilter()).build();
+    }
+
+    @Test
+    public void addFilterToChain() throws Exception {
+        final FilterChain chain = GrizzlyUtils.addFilterToChain(new FilterOne(), getDefaultFilterChain());
+
+        assertThat(chain.indexOfType(TransportFilter.class)).isEqualTo(0);
+        assertThat(chain.indexOfType(FilterOne.class)).isEqualTo(1);
+        assertThat(chain.indexOfType(DummyLDAPFilter.class)).isEqualTo(2);
+        assertThat(chain.size()).isEqualTo(3);
+    }
+
+    @Test
+    public void addSSLFilterToChain() throws Exception {
+        final FilterChain chain = GrizzlyUtils.addFilterToChain(new DummySSLFilter(), getDefaultFilterChain());
+
+        assertThat(chain.indexOfType(TransportFilter.class)).isEqualTo(0);
+        assertThat(chain.indexOfType(DummySSLFilter.class)).isEqualTo(1);
+        assertThat(chain.indexOfType(DummyLDAPFilter.class)).isEqualTo(2);
+        assertThat(chain.size()).isEqualTo(3);
+    }
+
+    @Test
+    public void addConnectionSecurityLayerAndSSLFilterToChain() throws Exception {
+        final FilterChain chain = GrizzlyUtils.addFilterToChain(new ConnectionSecurityLayerFilter(null, null),
+                getDefaultFilterChain());
+        final FilterChain sslChain = GrizzlyUtils.addFilterToChain(new DummySSLFilter(), chain);
+
+        // SSLFilter must be beneath ConnectionSecurityLayerFilter
+        assertThat(sslChain.indexOfType(TransportFilter.class)).isEqualTo(0);
+        assertThat(sslChain.indexOfType(DummySSLFilter.class)).isEqualTo(1);
+        assertThat(sslChain.indexOfType(ConnectionSecurityLayerFilter.class)).isEqualTo(2);
+        assertThat(sslChain.indexOfType(DummyLDAPFilter.class)).isEqualTo(3);
+        assertThat(sslChain.size()).isEqualTo(4);
+    }
+
+    @Test
+    public void buildFilterChainFromFilterChainProcessor() throws Exception {
+        final FilterChain chain = GrizzlyUtils.buildFilterChain(
+                FilterChainBuilder.stateless().add(new TransportFilter()).build(), new DummyLDAPFilter());
+
+        assertThat(chain.indexOfType(TransportFilter.class)).isEqualTo(0);
+        assertThat(chain.indexOfType(DummyLDAPFilter.class)).isEqualTo(1);
+        assertThat(chain.size()).isEqualTo(2);
+    }
+
+    @Test
+    public void buildFilterChainFromNonFilterChainProcessor() throws Exception {
+        final FilterChain chain = GrizzlyUtils.buildFilterChain(new StandaloneProcessor(), new DummyLDAPFilter());
+
+        assertThat(chain.indexOfType(TransportFilter.class)).isEqualTo(0);
+        assertThat(chain.indexOfType(DummyLDAPFilter.class)).isEqualTo(1);
+        assertThat(chain.size()).isEqualTo(2);
+    }
+
+
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/pom.xml b/opendj-sdk/opendj-ldap-sdk-examples/pom.xml
new file mode 100644
index 0000000..661de1a
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-ldap-sdk-examples</artifactId>
+    <name>OpenDJ SDK Examples</name>
+    <description>Examples illustrating usage of the OpenDJ LDAP SDK</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-grizzly</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <configuration>
+                    <createDependencyReducedPom>false</createDependencyReducedPom>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <links>
+                        <link>http://commons.forgerock.org/i18n-framework/i18n-core/apidocs</link>
+                    </links>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jxr-plugin</artifactId>
+                <version>2.2</version>
+                <reportSets>
+                    <reportSet>
+                        <reports>
+                            <report>jxr</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/assembly/examples.xml b/opendj-sdk/opendj-ldap-sdk-examples/src/main/assembly/examples.xml
new file mode 100644
index 0000000..56f4c54
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/assembly/examples.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2015 ForgeRock AS.
+ -->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
+                              http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+  <id>examples</id>
+
+  <includeBaseDirectory>false</includeBaseDirectory>
+
+  <fileSets>
+   <fileSet>
+    <outputDirectory>resources</outputDirectory>
+    <directory>src/main/java</directory>
+   </fileSet>
+   <fileSet>
+    <outputDirectory>resources</outputDirectory>
+    <directory>src/main/javadoc</directory>
+   </fileSet>
+  </fileSets>
+
+  <formats>
+    <format>jar</format>
+  </formats>
+</assembly>
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Controls.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Controls.java
new file mode 100644
index 0000000..a209b22
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Controls.java
@@ -0,0 +1,1076 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.RootDSE;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchResultReferenceIOException;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.SortKey;
+import org.forgerock.opendj.ldap.controls.ADNotificationRequestControl;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl;
+import org.forgerock.opendj.ldap.controls.AuthorizationIdentityResponseControl;
+import org.forgerock.opendj.ldap.controls.EntryChangeNotificationResponseControl;
+import org.forgerock.opendj.ldap.controls.GetEffectiveRightsRequestControl;
+import org.forgerock.opendj.ldap.controls.ManageDsaITRequestControl;
+import org.forgerock.opendj.ldap.controls.MatchedValuesRequestControl;
+import org.forgerock.opendj.ldap.controls.PasswordExpiredResponseControl;
+import org.forgerock.opendj.ldap.controls.PasswordExpiringResponseControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyRequestControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyResponseControl;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.controls.PersistentSearchChangeType;
+import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.controls.ServerSideSortRequestControl;
+import org.forgerock.opendj.ldap.controls.ServerSideSortResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
+import org.forgerock.opendj.ldap.controls.SubentriesRequestControl;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.controls.VirtualListViewRequestControl;
+import org.forgerock.opendj.ldap.controls.VirtualListViewResponseControl;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * This command-line client demonstrates use of LDAP controls. The client takes
+ * as arguments the host and port for the directory server, and expects to find
+ * the entries and access control instructions as defined in <a
+ * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>.
+ *
+ * This client connects as <code>cn=Directory Manager</code> with password
+ * <code>password</code>. Not a best practice; in real code use application
+ * specific credentials to connect, and ensure that your application has access
+ * to use the LDAP controls needed.
+ */
+public final class Controls {
+
+    /**
+     * Connect to the server, and then try to use some LDAP controls.
+     *
+     * @param args
+     *            The command line arguments: host, port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: host port");
+            System.err.println("For example: localhost 1389");
+            System.exit(1);
+        }
+        final String host = args[0];
+        final int port = Integer.parseInt(args[1]);
+
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        try (Connection connection = factory.getConnection()) {
+            checkSupportedControls(connection);
+
+            final String user = "cn=Directory Manager";
+            final char[] password = "password".toCharArray();
+            connection.bind(user, password);
+
+            // Uncomment a method to run one of the examples.
+
+            //useADNotificationRequestControl(connection);
+            //useAssertionControl(connection);
+            useAuthorizationIdentityRequestControl(connection);
+            // For the EntryChangeNotificationResponseControl see
+            // usePersistentSearchRequestControl()
+            //useGetEffectiveRightsRequestControl(connection);
+            //useManageDsaITRequestControl(connection);
+            //useMatchedValuesRequestControl(connection);
+            //usePasswordExpiredResponseControl(connection);
+            //usePasswordExpiringResponseControl(connection);
+            //usePasswordPolicyRequestControl(connection);
+            //usePermissiveModifyRequestControl(connection);
+            //usePersistentSearchRequestControl(connection);
+            //usePostReadRequestControl(connection);
+            //usePreReadRequestControl(connection);
+            //useProxiedAuthV2RequestControl(connection);
+            //useServerSideSortRequestControl(connection);
+            //useSimplePagedResultsControl(connection);
+            //useSubentriesRequestControl(connection);
+            //useSubtreeDeleteRequestControl(connection);
+            //useVirtualListViewRequestControl(connection);
+
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        }
+    }
+
+    /**
+     * Use the <a
+     * href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms676877(v=vs.85).aspx"
+     * >Microsoft LDAP Notification control</a>
+     * to register a change notification request for a search
+     * on Microsoft Active Directory.
+     * <p/>
+     * This client binds to Active Directory as
+     * {@code cn=Administrator,cn=users,dc=example,dc=com}
+     * with password {@code password},
+     * and expects entries under {@code dc=example,dc=com}.
+     *
+     * @param connection Active connection to Active Directory server.
+     * @throws LdapException Operation failed.
+     */
+    static void useADNotificationRequestControl(Connection connection) throws LdapException {
+
+        // --- JCite ADNotification ---
+        final String user = "cn=Administrator,cn=users,dc=example,dc=com";
+        final char[] password = "password".toCharArray();
+        connection.bind(user, password);
+
+        final String[] attributes = {"cn",
+            ADNotificationRequestControl.IS_DELETED_ATTR,
+            ADNotificationRequestControl.WHEN_CHANGED_ATTR,
+            ADNotificationRequestControl.WHEN_CREATED_ATTR};
+
+        SearchRequest request =
+                Requests.newSearchRequest("dc=example,dc=com",
+                        SearchScope.WHOLE_SUBTREE, "(objectclass=*)", attributes)
+                        .addControl(ADNotificationRequestControl.newControl(true));
+
+        ConnectionEntryReader reader = connection.search(request);
+
+        try {
+            while (reader.hasNext()) {
+                if (!reader.isReference()) {
+                    SearchResultEntry entry = reader.readEntry(); // Updated entry
+                    final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+
+                    Boolean isDeleted = entry.parseAttribute(
+                            ADNotificationRequestControl.IS_DELETED_ATTR
+                    ).asBoolean();
+                    if (isDeleted != null && isDeleted) {
+                        // Handle entry deletion
+                        writer.writeComment("Deleted entry: " + entry.getName());
+                        writer.writeEntry(entry);
+                        writer.flush();
+                    }
+                    String whenCreated = entry.parseAttribute(
+                            ADNotificationRequestControl.WHEN_CREATED_ATTR)
+                            .asString();
+                    String whenChanged = entry.parseAttribute(
+                            ADNotificationRequestControl.WHEN_CHANGED_ATTR)
+                            .asString();
+                    if (whenCreated != null && whenChanged != null) {
+                        if (whenCreated.equals(whenChanged)) {
+                            // Handle entry addition
+                            writer.writeComment("Added entry: " + entry.getName());
+                            writer.writeEntry(entry);
+                            writer.flush();
+                        } else {
+                            // Handle entry modification
+                            writer.writeComment("Modified entry: " + entry.getName());
+                            writer.writeEntry(entry);
+                            writer.flush();
+                        }
+                    }
+                } else {
+                    reader.readReference(); // Read and ignore reference
+                }
+            }
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (final SearchResultReferenceIOException e) {
+            System.err.println("Got search reference(s): " + e.getReference().getURIs());
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        }
+        // --- JCite ADNotification ---
+    }
+
+    /**
+     * Use the LDAP assertion control to modify Babs Jensen's description if
+     * her entry does not have a description, yet.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useAssertionControl(Connection connection) throws LdapException {
+        // --- JCite assertion ---
+        if (isSupported(AssertionRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+
+            final ModifyRequest request =
+                    Requests.newModifyRequest(dn)
+                        .addControl(AssertionRequestControl.newControl(
+                                true, Filter.valueOf("!(description=*)")))
+                        .addModification(ModificationType.ADD, "description",
+                                "Created using LDAP assertion control");
+
+            connection.modify(request);
+
+            try (final LDIFEntryWriter writer = new LDIFEntryWriter(System.out)) {
+                writer.writeEntry(connection.readEntry(dn, "description"));
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("AssertionRequestControl not supported.");
+        }
+        // --- JCite assertion ---
+    }
+
+    /**
+     * Use the LDAP Authorization Identity Controls to get the authorization ID.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useAuthorizationIdentityRequestControl(Connection connection) throws LdapException {
+        // --- JCite authzid ---
+        if (isSupported(AuthorizationIdentityRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final char[] pwd = "hifalutin".toCharArray();
+
+            System.out.println("Binding as " + dn);
+            final BindRequest request =
+                    Requests.newSimpleBindRequest(dn, pwd)
+                        .addControl(AuthorizationIdentityRequestControl.newControl(true));
+
+            final BindResult result = connection.bind(request);
+            try {
+                final AuthorizationIdentityResponseControl control =
+                        result.getControl(AuthorizationIdentityResponseControl.DECODER,
+                                new DecodeOptions());
+                System.out.println("Authorization ID returned: "
+                                + control.getAuthorizationID());
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            }
+        } else {
+            System.err.println("AuthorizationIdentityRequestControl not supported.");
+        }
+        // --- JCite authzid ---
+    }
+
+    /**
+     * Use the GetEffectiveRights Request Control to determine what sort of
+     * access a user has to particular attributes on an entry.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useGetEffectiveRightsRequestControl(Connection connection) throws LdapException {
+        // --- JCite effective rights ---
+        if (isSupported(GetEffectiveRightsRequestControl.OID)) {
+            final String authDN = "uid=kvaughan,ou=People,dc=example,dc=com";
+
+            final SearchRequest request =
+                    Requests.newSearchRequest(
+                            "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                            "(uid=bjensen)", "cn", "aclRights", "aclRightsInfo")
+                            .addControl(GetEffectiveRightsRequestControl.newControl(
+                                    true, authDN, "cn"));
+
+            final ConnectionEntryReader reader = connection.search(request);
+            try (final LDIFEntryWriter writer = new LDIFEntryWriter(System.out)) {
+                while (reader.hasNext()) {
+                    if (!reader.isReference()) {
+                        final SearchResultEntry entry = reader.readEntry();
+                        writer.writeEntry(entry);
+                    }
+                }
+            } catch (final LdapException e) {
+                System.err.println(e.getMessage());
+                System.exit(e.getResult().getResultCode().intValue());
+            } catch (final SearchResultReferenceIOException e) {
+                System.err.println("Got search reference(s): " + e.getReference().getURIs());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("GetEffectiveRightsRequestControl not supported.");
+        }
+        // --- JCite effective rights ---
+    }
+
+    /**
+     * Use the ManageDsaIT Request Control to show the difference between a
+     * referral accessed with and without use of the control.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useManageDsaITRequestControl(Connection connection) throws LdapException {
+        // --- JCite manage DsaIT ---
+        if (isSupported(ManageDsaITRequestControl.OID)) {
+            final String dn = "dc=ref,dc=com";
+
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            try {
+                System.out.println("Referral without the ManageDsaIT control.");
+                SearchRequest request = Requests.newSearchRequest(dn,
+                        SearchScope.SUBORDINATES, "(objectclass=*)", "");
+                final ConnectionEntryReader reader = connection.search(request);
+                while (reader.hasNext()) {
+                    if (reader.isReference()) {
+                        final SearchResultReference ref = reader.readReference();
+                        System.out.println("Reference: " + ref.getURIs());
+                    }
+                }
+
+                System.out.println("Referral with the ManageDsaIT control.");
+                request.addControl(ManageDsaITRequestControl.newControl(true));
+                final SearchResultEntry entry = connection.searchSingleEntry(request);
+                writer.writeEntry(entry);
+                writer.close();
+            } catch (final LdapException e) {
+                System.err.println(e.getMessage());
+                System.exit(e.getResult().getResultCode().intValue());
+            } catch (final SearchResultReferenceIOException e) {
+                System.err.println("Got search reference(s): " + e.getReference().getURIs());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("ManageDsaITRequestControl not supported.");
+        }
+        // --- JCite manage DsaIT ---
+    }
+
+    /**
+     * Use the Matched Values Request Control to show read only one attribute
+     * value.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useMatchedValuesRequestControl(Connection connection) throws LdapException {
+        // --- JCite matched values ---
+        if (isSupported(MatchedValuesRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final SearchRequest request =
+                    Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT,
+                            "(objectclass=*)", "cn")
+                            .addControl(MatchedValuesRequestControl.newControl(
+                                    true, "(cn=Babs Jensen)"));
+
+            final SearchResultEntry entry = connection.searchSingleEntry(request);
+            System.out.println("Reading entry with matched values request.");
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            try {
+                writer.writeEntry(entry);
+                writer.close();
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("MatchedValuesRequestControl not supported.");
+        }
+        // --- JCite matched values ---
+    }
+
+    /**
+     * Check the Password Expired Response Control. To get this code to output
+     * something, you must first set up an appropriate password policy and wait
+     * for Barbara Jensen's password to expire.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     */
+    static void usePasswordExpiredResponseControl(Connection connection) {
+        // --- JCite password expired ---
+        if (isSupported(PasswordExpiredResponseControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final char[] pwd = "hifalutin".toCharArray();
+
+            try {
+                connection.bind(dn, pwd);
+            } catch (final LdapException e) {
+                final Result result = e.getResult();
+                try {
+                    final PasswordExpiredResponseControl control =
+                            result.getControl(PasswordExpiredResponseControl.DECODER,
+                                    new DecodeOptions());
+                    if (control != null && control.hasValue()) {
+                        System.out.println("Password expired for " + dn);
+                    }
+                } catch (final DecodeException de) {
+                    System.err.println(de.getMessage());
+                    System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+                }
+            }
+        } else {
+            System.err.println("PasswordExpiredResponseControl not supported.");
+        }
+        // --- JCite password expired ---
+    }
+
+    /**
+     * Check the Password Expiring Response Control. To get this code to output
+     * something, you must first set up an appropriate password policy and wait
+     * for Barbara Jensen's password to get old enough that the server starts
+     * warning about expiration.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePasswordExpiringResponseControl(Connection connection) throws LdapException {
+        // --- JCite password expiring ---
+        if (isSupported(PasswordExpiringResponseControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final char[] pwd = "hifalutin".toCharArray();
+
+            final BindResult result = connection.bind(dn, pwd);
+            try {
+                final PasswordExpiringResponseControl control =
+                        result.getControl(PasswordExpiringResponseControl.DECODER,
+                                new DecodeOptions());
+                if (control != null && control.hasValue()) {
+                    System.out.println("Password for " + dn + " expires in "
+                            + control.getSecondsUntilExpiration() + " seconds.");
+                }
+            } catch (final DecodeException de) {
+                System.err.println(de.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            }
+        } else {
+            System.err.println("PasswordExpiringResponseControl not supported");
+        }
+        // --- JCite password expiring ---
+    }
+
+    /**
+     * Use the Password Policy Request and Response Controls. To get this code
+     * to output something, you must first set up an appropriate password policy
+     * and wait for Barbara Jensen's password to get old enough that the server
+     * starts warning about expiration, or for the password to expire.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     */
+    static void usePasswordPolicyRequestControl(Connection connection) {
+        // --- JCite password policy ---
+        if (isSupported(PasswordPolicyRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final char[] pwd = "hifalutin".toCharArray();
+
+            try {
+                final BindRequest request = Requests.newSimpleBindRequest(dn, pwd)
+                        .addControl(PasswordPolicyRequestControl.newControl(true));
+
+                final BindResult result = connection.bind(request);
+
+                final PasswordPolicyResponseControl control =
+                        result.getControl(PasswordPolicyResponseControl.DECODER,
+                                new DecodeOptions());
+                if (control != null && control.getWarningType() != null) {
+                    System.out.println("Password policy warning "
+                            + control.getWarningType() + ", value "
+                            + control.getWarningValue() + " for " + dn);
+                }
+            } catch (final LdapException e) {
+                final Result result = e.getResult();
+                try {
+                    final PasswordPolicyResponseControl control =
+                            result.getControl(PasswordPolicyResponseControl.DECODER,
+                                    new DecodeOptions());
+                    if (control != null) {
+                        System.out.println("Password policy error "
+                                + control.getErrorType() + " for " + dn);
+                    }
+                } catch (final DecodeException de) {
+                    System.err.println(de.getMessage());
+                    System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+                }
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            }
+        } else {
+            System.err.println("PasswordPolicyRequestControl not supported");
+        }
+        // --- JCite password policy ---
+    }
+
+    /**
+     * Use Permissive Modify Request Control to try to add an attribute that
+     * already exists.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePermissiveModifyRequestControl(Connection connection) throws LdapException {
+        // --- JCite permissive modify ---
+        if (isSupported(PermissiveModifyRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+
+            final ModifyRequest request =
+                    Requests.newModifyRequest(dn)
+                        .addControl(PermissiveModifyRequestControl.newControl(true))
+                        .addModification(ModificationType.ADD, "uid", "bjensen");
+
+            connection.modify(request);
+            System.out.println("Permissive modify did not complain about "
+                    + "attempt to add uid: bjensen to " + dn + ".");
+        } else {
+            System.err.println("PermissiveModifyRequestControl not supported");
+        }
+        // --- JCite permissive modify ---
+    }
+
+    /**
+     * Use the LDAP PersistentSearchRequestControl to set up a persistent
+     * search. Also use the Entry Change Notification Response Control to get
+     * details about why an entry was returned for a persistent search.
+     *
+     * After you set this up, use another application to make changes to user
+     * entries under dc=example,dc=com.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePersistentSearchRequestControl(Connection connection) throws LdapException {
+        // --- JCite psearch ---
+        if (isSupported(PersistentSearchRequestControl.OID)) {
+            final SearchRequest request =
+                    Requests.newSearchRequest(
+                            "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                            "(objectclass=inetOrgPerson)", "cn")
+                            .addControl(PersistentSearchRequestControl.newControl(
+                                    true, true, true, // isCritical, changesOnly, returnECs
+                                    PersistentSearchChangeType.ADD,
+                                    PersistentSearchChangeType.DELETE,
+                                    PersistentSearchChangeType.MODIFY,
+                                    PersistentSearchChangeType.MODIFY_DN));
+
+            final ConnectionEntryReader reader = connection.search(request);
+
+            try {
+                while (reader.hasNext()) {
+                    if (!reader.isReference()) {
+                        final SearchResultEntry entry = reader.readEntry();
+                        System.out.println("Entry changed: " + entry.getName());
+
+                        final EntryChangeNotificationResponseControl control =
+                                entry.getControl(
+                                        EntryChangeNotificationResponseControl.DECODER,
+                                        new DecodeOptions());
+
+                        final PersistentSearchChangeType type = control.getChangeType();
+                        System.out.println("Change type: " + type);
+                        if (type.equals(PersistentSearchChangeType.MODIFY_DN)) {
+                            System.out.println("Previous DN: " + control.getPreviousName());
+                        }
+                        System.out.println("Change number: " + control.getChangeNumber());
+                        System.out.println(); // Add a blank line.
+                    }
+                }
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            } catch (final LdapException e) {
+                System.err.println(e.getMessage());
+                System.exit(e.getResult().getResultCode().intValue());
+            } catch (final SearchResultReferenceIOException e) {
+                System.err.println("Got search reference(s): " + e.getReference().getURIs());
+            }
+        } else {
+            System.err.println("PersistentSearchRequestControl not supported.");
+        }
+        // --- JCite psearch ---
+    }
+
+
+    /**
+     * Use Post Read Controls to get entry content after a modification.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePostReadRequestControl(Connection connection) throws LdapException {
+        // --- JCite post read ---
+        if (isSupported(PostReadRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+
+            final ModifyRequest request =
+                    Requests.newModifyRequest(dn)
+                    .addControl(PostReadRequestControl.newControl(true, "description"))
+                    .addModification(ModificationType.REPLACE,
+                            "description", "Using the PostReadRequestControl");
+
+            final Result result = connection.modify(request);
+            try {
+                final PostReadResponseControl control =
+                        result.getControl(PostReadResponseControl.DECODER,
+                                new DecodeOptions());
+                final Entry entry = control.getEntry();
+
+                final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+                writer.writeEntry(entry);
+                writer.close();
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("PostReadRequestControl not supported");
+        }
+        // --- JCite post read ---
+    }
+
+    /**
+     * Use Pre Read Controls to get entry content before a modification.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePreReadRequestControl(Connection connection) throws LdapException {
+        // --- JCite pre read ---
+        if (isSupported(PreReadRequestControl.OID)) {
+            final String dn = "uid=bjensen,ou=People,dc=example,dc=com";
+
+            final ModifyRequest request =
+                    Requests.newModifyRequest(dn)
+                    .addControl(PreReadRequestControl.newControl(true, "mail"))
+                    .addModification(
+                            ModificationType.REPLACE, "mail", "modified@example.com");
+
+            final Result result = connection.modify(request);
+            try {
+                final PreReadResponseControl control =
+                        result.getControl(PreReadResponseControl.DECODER,
+                                new DecodeOptions());
+                final Entry entry = control.getEntry();
+
+                final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+                writer.writeEntry(entry);
+                writer.close();
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("PreReadRequestControl not supported");
+        }
+        // --- JCite pre read ---
+    }
+
+    /**
+     * Use proxied authorization to modify an identity as another user.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useProxiedAuthV2RequestControl(Connection connection) throws LdapException {
+        // --- JCite proxied authzv2 ---
+        if (isSupported(ProxiedAuthV2RequestControl.OID)) {
+            final String bindDN = "cn=My App,ou=Apps,dc=example,dc=com";
+            final String targetDn = "uid=bjensen,ou=People,dc=example,dc=com";
+            final String authzId = "dn:uid=kvaughan,ou=People,dc=example,dc=com";
+
+            final ModifyRequest request =
+                    Requests.newModifyRequest(targetDn)
+                    .addControl(ProxiedAuthV2RequestControl.newControl(authzId))
+                    .addModification(ModificationType.REPLACE, "description",
+                            "Done with proxied authz");
+
+            connection.bind(bindDN, "password".toCharArray());
+            connection.modify(request);
+            final Entry entry = connection.readEntry(targetDn, "description");
+
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            try {
+                writer.writeEntry(entry);
+                writer.close();
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("ProxiedAuthV2RequestControl not supported");
+        }
+        // --- JCite proxied authzv2 ---
+    }
+
+    /**
+     * Use the server-side sort controls.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    // --- JCite server-side sort ---
+    static void useServerSideSortRequestControl(Connection connection) throws LdapException {
+        if (isSupported(ServerSideSortRequestControl.OID)) {
+            final SearchRequest request =
+                    Requests.newSearchRequest("ou=People,dc=example,dc=com",
+                            SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
+                            .addControl(ServerSideSortRequestControl.newControl(
+                                            true, new SortKey("cn")));
+
+            final SearchResultHandler resultHandler = new MySearchResultHandler();
+            final Result result = connection.search(request, resultHandler);
+
+            try {
+                final ServerSideSortResponseControl control =
+                        result.getControl(ServerSideSortResponseControl.DECODER,
+                                new DecodeOptions());
+                if (control != null && control.getResult() == ResultCode.SUCCESS) {
+                    System.out.println("# Entries are sorted.");
+                } else {
+                    System.out.println("# Entries not necessarily sorted");
+                }
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            }
+        } else {
+            System.err.println("ServerSideSortRequestControl not supported");
+        }
+    }
+
+    private static class MySearchResultHandler implements SearchResultHandler {
+
+        @Override
+        public boolean handleEntry(SearchResultEntry entry) {
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            try {
+                writer.writeEntry(entry);
+                writer.flush();
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public boolean handleReference(SearchResultReference reference) {
+            System.out.println("Got a reference: " + reference);
+            return false;
+        }
+    }
+    // --- JCite server-side sort ---
+
+    /**
+     * Use the simple paged results mechanism.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useSimplePagedResultsControl(Connection connection) throws LdapException {
+        // --- JCite simple paged results ---
+        if (isSupported(SimplePagedResultsControl.OID)) {
+            ByteString cookie = ByteString.empty();
+            SearchRequest request;
+            final SearchResultHandler resultHandler = new MySearchResultHandler();
+            Result result;
+
+            int page = 1;
+            do {
+                System.out.println("# Simple paged results: Page " + page);
+
+                request =
+                        Requests.newSearchRequest("dc=example,dc=com",
+                                SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
+                                .addControl(SimplePagedResultsControl.newControl(
+                                        true, 3, cookie));
+
+                result = connection.search(request, resultHandler);
+                try {
+                    SimplePagedResultsControl control =
+                            result.getControl(SimplePagedResultsControl.DECODER,
+                                    new DecodeOptions());
+                    cookie = control.getCookie();
+                } catch (final DecodeException e) {
+                    System.err.println(e.getMessage());
+                    System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+                }
+
+                ++page;
+            } while (cookie.length() != 0);
+        } else {
+            System.err.println("SimplePagedResultsControl not supported");
+        }
+        // --- JCite simple paged results ---
+    }
+
+    /**
+     * Use the subentries request control.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useSubentriesRequestControl(Connection connection) throws LdapException {
+        // --- JCite subentries ---
+        if (isSupported(SubentriesRequestControl.OID)) {
+            final SearchRequest request =
+                    Requests.newSearchRequest("dc=example,dc=com",
+                                SearchScope.WHOLE_SUBTREE,
+                                "cn=*Class of Service", "cn", "subtreeSpecification")
+                            .addControl(SubentriesRequestControl.newControl(
+                                true, true));
+
+            final ConnectionEntryReader reader = connection.search(request);
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            try {
+                while (reader.hasNext()) {
+                    if (reader.isEntry()) {
+                        final SearchResultEntry entry = reader.readEntry();
+                        writer.writeEntry(entry);
+                    }
+                }
+                writer.close();
+            } catch (final LdapException e) {
+                System.err.println(e.getMessage());
+                System.exit(e.getResult().getResultCode().intValue());
+            } catch (final SearchResultReferenceIOException e) {
+                System.err.println("Got search reference(s): " + e.getReference().getURIs());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+        } else {
+            System.err.println("SubentriesRequestControl not supported");
+        }
+        // --- JCite subentries ---
+    }
+
+    /**
+     * Use the subtree delete control.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useSubtreeDeleteRequestControl(Connection connection) throws LdapException {
+        // --- JCite tree delete ---
+        if (isSupported(SubtreeDeleteRequestControl.OID)) {
+
+            final String dn = "ou=Apps,dc=example,dc=com";
+            final DeleteRequest request =
+                    Requests.newDeleteRequest(dn)
+                            .addControl(SubtreeDeleteRequestControl.newControl(true));
+
+            final Result result = connection.delete(request);
+            if (result.isSuccess()) {
+                System.out.println("Successfully deleted " + dn
+                        + " and all entries below.");
+            } else {
+                System.err.println("Result: " + result.getDiagnosticMessage());
+            }
+        } else {
+            System.err.println("SubtreeDeleteRequestControl not supported");
+        }
+        // --- JCite tree delete ---
+    }
+
+    /**
+     * Use the virtual list view controls. In order to set up OpenDJ directory
+     * server to produce the following output with the example code, use OpenDJ
+     * Control Panel &gt; Manage Indexes &gt; New VLV Index... to set up a
+     * virtual list view index for people by last name, using the filter
+     * {@code (|(givenName=*)(sn=*))}, and sorting first by surname, {@code sn},
+     * in ascending order, then by given name also in ascending order
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useVirtualListViewRequestControl(Connection connection) throws LdapException {
+        // --- JCite vlv ---
+        if (isSupported(VirtualListViewRequestControl.OID)) {
+            ByteString contextID = ByteString.empty();
+
+            // Add a window of 2 entries on either side of the first sn=Jensen entry.
+            final SearchRequest request =
+                    Requests.newSearchRequest("ou=People,dc=example,dc=com",
+                            SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName")
+                            .addControl(ServerSideSortRequestControl.newControl(
+                                    true, new SortKey("sn")))
+                            .addControl(
+                                    VirtualListViewRequestControl.newAssertionControl(
+                                            true,
+                                            ByteString.valueOfUtf8("Jensen"),
+                                            2, 2, contextID));
+
+            final SearchResultHandler resultHandler = new MySearchResultHandler();
+            final Result result = connection.search(request, resultHandler);
+
+            try {
+                final ServerSideSortResponseControl sssControl =
+                        result.getControl(ServerSideSortResponseControl.DECODER,
+                                new DecodeOptions());
+                if (sssControl != null && sssControl.getResult() == ResultCode.SUCCESS) {
+                    System.out.println("# Entries are sorted.");
+                } else {
+                    System.out.println("# Entries not necessarily sorted");
+                }
+
+                final VirtualListViewResponseControl vlvControl =
+                        result.getControl(VirtualListViewResponseControl.DECODER,
+                                new DecodeOptions());
+                System.out.println("# Position in list: "
+                        + vlvControl.getTargetPosition() + "/"
+                        + vlvControl.getContentCount());
+            } catch (final DecodeException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+            }
+        } else {
+            System.err.println("VirtualListViewRequestControl not supported");
+        }
+        // --- JCite vlv ---
+    }
+
+    // --- JCite check support ---
+    /**
+     * Controls supported by the LDAP server.
+     */
+    private static Collection<String> controls;
+
+    /**
+     * Populate the list of supported LDAP control OIDs.
+     *
+     * @param connection
+     *            Active connection to the LDAP server.
+     * @throws LdapException
+     *             Failed to get list of controls.
+     */
+    static void checkSupportedControls(Connection connection) throws LdapException {
+        controls = RootDSE.readRootDSE(connection).getSupportedControls();
+    }
+
+    /**
+     * Check whether a control is supported. Call {@code checkSupportedControls}
+     * first.
+     *
+     * @param control
+     *            Check support for this control, provided by OID.
+     * @return True if the control is supported.
+     */
+    static boolean isSupported(final String control) {
+        return controls != null && controls.contains(control);
+    }
+    // --- JCite check support ---
+
+    /**
+     * Constructor not used.
+     */
+    private Controls() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ExtendedOperations.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ExtendedOperations.java
new file mode 100644
index 0000000..152a76a
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ExtendedOperations.java
@@ -0,0 +1,197 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.RootDSE;
+import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.WhoAmIExtendedRequest;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.WhoAmIExtendedResult;
+
+/**
+ * This command-line client demonstrates use of LDAP extended operations. The
+ * client takes as arguments the host and port for the directory server, and
+ * expects to find the entries and access control instructions as defined in <a
+ * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>.
+ *
+ * This client connects as <code>cn=Directory Manager</code> with password
+ * <code>password</code>. Not a best practice; in real code use application
+ * specific credentials to connect, and ensure that your application has access
+ * to use the LDAP extended operations needed.
+ */
+public final class ExtendedOperations {
+
+    /**
+     * Connect to the server, and then try to use some LDAP extended operations.
+     *
+     * @param args
+     *            The command line arguments: host, port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: host port");
+            System.err.println("For example: localhost 1389");
+            System.exit(1);
+        }
+        final String host = args[0];
+        final int port = Integer.parseInt(args[1]);
+
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            checkSupportedExtendedOperations(connection);
+
+            final String user = "cn=Directory Manager";
+            final char[] password = "password".toCharArray();
+            connection.bind(user, password);
+
+            // Uncomment a method to run one of the examples.
+
+            // For a Cancel Extended request, see the SearchAsync example.
+            //usePasswordModifyExtendedRequest(connection);
+            // For StartTLS, see the authentication examples.
+            useWhoAmIExtendedRequest(connection);
+
+        } catch (LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+
+    /**
+     * Use the password modify extended request.
+     *
+     * @param connection
+     *            Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void usePasswordModifyExtendedRequest(Connection connection) throws LdapException {
+        // --- JCite password modify ---
+        if (isSupported(PasswordModifyExtendedRequest.OID)) {
+            final String userIdentity = "u:scarter";
+            final char[] oldPassword = "sprain".toCharArray();
+            final char[] newPassword = "secret12".toCharArray();
+
+            final PasswordModifyExtendedRequest request =
+                    Requests.newPasswordModifyExtendedRequest()
+                        .setUserIdentity(userIdentity)
+                        .setOldPassword(oldPassword)
+                        .setNewPassword(newPassword);
+
+            final PasswordModifyExtendedResult result =
+                    connection.extendedRequest(request);
+            if (result.isSuccess()) {
+                System.out.println("Changed password for " + userIdentity);
+            } else {
+                System.err.println(result.getDiagnosticMessage());
+            }
+        } else {
+            System.err.println("PasswordModifyExtendedRequest not supported");
+        }
+        // --- JCite password modify ---
+    }
+
+    /**
+     * Use the Who Am I? extended request.
+     *
+     * @param connection Active connection to LDAP server containing <a
+     *            href="http://opendj.forgerock.org/Example.ldif"
+     *            >Example.ldif</a> content.
+     * @throws LdapException
+     *             Operation failed.
+     */
+    static void useWhoAmIExtendedRequest(Connection connection) throws LdapException {
+        // --- JCite who am I ---
+        if (isSupported(WhoAmIExtendedRequest.OID)) {
+
+            final String name = "uid=bjensen,ou=People,dc=example,dc=com";
+            final char[] password = "hifalutin".toCharArray();
+
+            final Result result = connection.bind(name, password);
+            if (result.isSuccess()) {
+
+                final WhoAmIExtendedRequest request =
+                        Requests.newWhoAmIExtendedRequest();
+                final WhoAmIExtendedResult extResult =
+                        connection.extendedRequest(request);
+
+                if (extResult.isSuccess()) {
+                    System.out.println("Authz ID: "  + extResult.getAuthorizationID());
+                }
+            }
+        } else {
+            System.err.println("WhoAmIExtendedRequest not supported");
+        }
+        // --- JCite who am I ---
+    }
+
+    // --- JCite check support ---
+    /**
+     * Controls supported by the LDAP server.
+     */
+    private static Collection<String> extendedOperations;
+
+    /**
+     * Populate the list of supported LDAP extended operation OIDs.
+     *
+     * @param connection
+     *            Active connection to the LDAP server.
+     * @throws LdapException
+     *             Failed to get list of extended operations.
+     */
+    static void checkSupportedExtendedOperations(Connection connection) throws LdapException {
+        extendedOperations = RootDSE.readRootDSE(connection)
+                .getSupportedExtendedOperations();
+    }
+
+    /**
+     * Check whether an extended operation is supported. Call
+     * {@code checkSupportedExtendedOperations} first.
+     *
+     * @param extendedOperation
+     *            Check support for this extended operation, provided by OID.
+     * @return True if the control is supported.
+     */
+    static boolean isSupported(final String extendedOperation) {
+        return extendedOperations != null && extendedOperations.contains(extendedOperation);
+    }
+    // --- JCite check support ---
+
+    /**
+     * Constructor not used.
+     */
+    private ExtendedOperations() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetADChangeNotifications.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetADChangeNotifications.java
new file mode 100644
index 0000000..ef5abb5
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetADChangeNotifications.java
@@ -0,0 +1,134 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * An example client application which searches Microsoft Active Directory
+ * synchronously, using {@link GenericControl} to pass the <a
+ * href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms676877(v=vs.85).aspx"
+ * >Microsoft LDAP Notification control</a>.
+ *
+ * <p>This example is a near copy of {@link Search}, but works only with
+ * directory servers like Active Directory that support the control with OID
+ * 1.2.840.113556.1.4.528.
+ *
+ * <p>This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password> <baseDN>}
+ * </pre>
+ *
+ * <p>The {@code baseDN} must be the root of a naming context in this example.
+ */
+public final class GetADChangeNotifications {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password,
+     *            base DN, where the base DN is the root of a naming context.
+     */
+    public static void main(final String[] args) {
+        if (args.length < 5) {
+            System.err.println("Usage: host port username password baseDN");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final String password = args[3];
+        final String baseDN = args[4];
+
+        // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa772153(v=vs.85).aspx
+        // --- JCite ---
+        final SearchScope scope = SearchScope.WHOLE_SUBTREE;
+        final String filter = "(objectclass=*)";
+        final String[] attributes = {
+            "objectclass", "objectGUID", "isDeleted", "uSNChanged"
+        };
+
+        // Create an LDIF writer which will write the search results to stdout.
+        final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory =
+                new LDAPConnectionFactory(hostName, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(userName, password.toCharArray());
+
+            // Read the entries and output them as LDIF.
+            final SearchRequest request =
+                    Requests
+                            .newSearchRequest(baseDN, scope, filter, attributes)
+                            .addControl(
+                                    GenericControl
+                                            .newControl(
+                                                    "1.2.840.113556.1.4.528",
+                                                    true));
+            final ConnectionEntryReader reader = connection.search(request);
+            while (reader.hasNext()) {
+                if (!reader.isReference()) {
+                    final SearchResultEntry entry = reader.readEntry();
+                    writer.writeComment("Search result entry: " + entry.getName());
+                    writer.writeEntry(entry);
+                    writer.flush();
+                } else {
+                    final SearchResultReference ref = reader.readReference();
+
+                    // Got a continuation reference.
+                    writer.writeComment("Search result reference: " + ref.getURIs());
+                }
+            }
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private GetADChangeNotifications() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetInfo.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetInfo.java
new file mode 100644
index 0000000..057b721
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/GetInfo.java
@@ -0,0 +1,135 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * Demonstrates accessing server information about capabilities and schema.
+ */
+public final class GetInfo {
+    /** Connection information. */
+    private static String host;
+    private static int port;
+    /** The kind of server information to request (all, controls, extops). */
+    private static String infoType;
+
+    /**
+     * Access the directory over LDAP to request information about capabilities
+     * and schema.
+     *
+     * @param args
+     *            The command line arguments
+     */
+    public static void main(final String[] args) {
+        parseArgs(args);
+        connect();
+    }
+
+    /**
+     * Authenticate over LDAP.
+     */
+    private static void connect() {
+        // --- JCite ---
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind("", "".toCharArray()); // Anonymous bind
+
+            final String attributeList;
+            if ("controls".equals(infoType.toLowerCase())) {
+                attributeList = "supportedControl";
+            } else if ("extops".equals(infoType.toLowerCase())) {
+                attributeList = "supportedExtension";
+            } else {
+                attributeList = "+"; // All operational attributes
+            }
+
+            final SearchResultEntry entry = connection.searchSingleEntry(
+                    "", // DN is "" for root DSE.
+                    SearchScope.BASE_OBJECT, // Read only the root DSE.
+                    "(objectclass=*)", // Every object matches this filter.
+                    attributeList); // Return these requested attributes.
+
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            writer.writeComment("Root DSE for LDAP server at " + host + ":" + port);
+            if (entry != null) {
+                writer.writeEntry(entry);
+            }
+            writer.flush();
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private static void giveUp() {
+        printUsage();
+        System.exit(1);
+    }
+
+    /**
+     * Parse command line arguments.
+     *
+     * @param args
+     *            host port bind-dn bind-password info-type
+     */
+    private static void parseArgs(final String[] args) {
+        if (args.length != 3) {
+            giveUp();
+        }
+
+        host = args[0];
+        port = Integer.parseInt(args[1]);
+        infoType = args[2];
+        final String infoTypeLc = infoType.toLowerCase();
+        if (!"all".equals(infoTypeLc)
+                && !"controls".equals(infoTypeLc)
+                && !"extops".equals(infoTypeLc)) {
+            giveUp();
+        }
+    }
+
+    private static void printUsage() {
+        System.err.println("Usage: host port info-type");
+        System.err.println("\tAll arguments are required.");
+        System.err.println("\tinfo-type to get can be either all, controls, or extops.");
+    }
+
+    private GetInfo() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Modify.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Modify.java
new file mode 100644
index 0000000..a5d7174
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Modify.java
@@ -0,0 +1,122 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ConnectionChangeRecordWriter;
+import org.forgerock.opendj.ldif.LDIFChangeRecordReader;
+
+/**
+ * An example client application which applies update operations to a Directory
+ * Server. The update operations will be read from an LDIF file, or stdin if no
+ * filename is provided. This example takes the following command line
+ * parameters (it will read from stdin if no LDIF file is provided):
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password> [<ldifFile>]}
+ * </pre>
+ */
+public final class Modify {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password,
+     *            LDIF file name containing the update operations (will use
+     *            stdin if not provided).
+     */
+    public static void main(final String[] args) {
+        if (args.length < 4 || args.length > 5) {
+            System.err.println("Usage: host port username password [ldifFileName]");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final String password = args[3];
+
+        // Create the LDIF reader which will either used the named file, if
+        // provided, or stdin.
+        InputStream ldif;
+        if (args.length > 4) {
+            try {
+                ldif = new FileInputStream(args[4]);
+            } catch (final FileNotFoundException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+                return;
+            }
+        } else {
+            ldif = System.in;
+        }
+        // --- JCite ---
+        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldif);
+
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(hostName, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(userName, password.toCharArray());
+
+            // Write the changes.
+            final ConnectionChangeRecordWriter writer =
+                    new ConnectionChangeRecordWriter(connection);
+            while (reader.hasNext()) {
+                ChangeRecord changeRecord = reader.readChangeRecord();
+                writer.writeChangeRecord(changeRecord);
+                System.err.println("Successfully modified entry " + changeRecord.getName());
+            }
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+
+            try {
+                reader.close();
+            } catch (final IOException ignored) {
+                // Ignore.
+            }
+        }
+        // --- JCite ---
+    }
+
+    private Modify() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ModifyAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ModifyAsync.java
new file mode 100644
index 0000000..6a14b5a
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ModifyAsync.java
@@ -0,0 +1,167 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.util.Utils.closeSilently;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * An example client application which applies update operations to a directory server
+ * using the asynchronous APIs.
+ * The update operations are read from an LDIF file, or stdin if no filename is provided.
+ * This example takes the following command line parameters,
+ * reading from stdin if no LDIF file is provided:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password> [<ldifFile>]}
+ * </pre>
+ */
+public final class ModifyAsync {
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Result for the modify operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password,
+     *            LDIF file name containing the update operations.
+     *            Stdin is used if no LDIF file name is provided.
+     */
+    public static void main(final String[] args) {
+        if (args.length < 4 || args.length > 5) {
+            System.err.println("Usage: host port username password [ldifFileName]");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final char[] password = args[3].toCharArray();
+
+        // Create the LDIF reader using either the named file, if provided, or stdin.
+        InputStream ldif;
+        if (args.length > 4) {
+            try {
+                ldif = new FileInputStream(args[4]);
+            } catch (final FileNotFoundException e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+                return;
+            }
+        } else {
+            ldif = System.in;
+        }
+        final String[] ldifLines = getInputLines(ldif);
+
+        // Connect to the server, bind, and request the modifications.
+        new LDAPConnectionFactory(hostName, port)
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        ModifyAsync.connection = connection;
+                        return connection.bindAsync(
+                                Requests.newSimpleBindRequest(userName, password));
+                    }
+                })
+                .thenAsync(new AsyncFunction<BindResult, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(BindResult bindResult)
+                            throws LdapException {
+                        return connection.modifyAsync(
+                                Requests.newModifyRequest(ldifLines));
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        resultCode = result.getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        } catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    /**
+     * Returns the lines from the input stream.
+     * @param in    The input stream.
+     * @return The lines from the input stream.
+     */
+    private static String[] getInputLines(final InputStream in) {
+        String line;
+        final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+        final List<String> lines = new ArrayList<>();
+        try {
+            while ((line = reader.readLine()) != null) {
+                lines.add(line);
+            }
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        }
+        return lines.toArray(new String[lines.size()]);
+    }
+
+    private ModifyAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ParseAttributes.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ParseAttributes.java
new file mode 100644
index 0000000..93ad61b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ParseAttributes.java
@@ -0,0 +1,153 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * This command-line client demonstrates parsing entry attribute values to
+ * objects. The client takes as arguments the host and port for the directory
+ * server, and expects to find the entries and access control instructions as
+ * defined in <a
+ * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>.
+ */
+public final class ParseAttributes {
+
+    /**
+     * Connect to the server, and then try to use some LDAP controls.
+     *
+     * @param args
+     *            The command line arguments: host, port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: host port");
+            System.err.println("For example: localhost 1389");
+            System.exit(1);
+        }
+        final String host = args[0];
+        final int port = Integer.parseInt(args[1]);
+
+        // --- JCite ---
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+        try {
+            connection = factory.getConnection();
+
+            // Use Kirsten Vaughan's credentials and her entry.
+            String name = "uid=kvaughan,ou=People,dc=example,dc=com";
+            char[] password = "bribery".toCharArray();
+            connection.bind(name, password);
+
+            // Make sure we have a timestamp to play with.
+            updateEntry(connection, name, "description");
+
+            // Read Kirsten's entry.
+            final SearchResultEntry entry = connection.readEntry(name,
+                    "cn", "objectClass", "hasSubordinates", "numSubordinates",
+                    "isMemberOf", "modifyTimestamp");
+
+            // Get the entry DN and some attribute values as objects.
+            DN dn = entry.getName();
+
+            Set<String> cn = entry.parseAttribute("cn").asSetOfString("");
+            Set<AttributeDescription> objectClasses =
+                    entry.parseAttribute("objectClass").asSetOfAttributeDescription();
+            boolean hasChildren = entry.parseAttribute("hasSubordinates").asBoolean();
+            int numChildren = entry.parseAttribute("numSubordinates").asInteger(0);
+            Set<DN> groups = entry
+                    .parseAttribute("isMemberOf")
+                    .usingSchema(Schema.getDefaultSchema()).asSetOfDN();
+            Calendar timestamp = entry
+                    .parseAttribute("modifyTimestamp")
+                    .asGeneralizedTime().toCalendar();
+
+            // Do something with the objects.
+            // ...
+
+            // This example simply dumps what was obtained.
+            entry.setName(dn);
+            Entry newEntry = new LinkedHashMapEntry(name)
+                .addAttribute("cn", cn.toArray())
+                .addAttribute("objectClass", objectClasses.toArray())
+                .addAttribute("hasChildren", hasChildren)
+                .addAttribute("numChildren", numChildren)
+                .addAttribute("groups", groups.toArray())
+                .addAttribute("timestamp", timestamp.getTimeInMillis());
+
+            final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+            writer.writeEntry(newEntry);
+            writer.close();
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    /**
+     * Update and entry to generate a time stamp.
+     *
+     * @param connection
+     *            Connection to the directory server with rights to perform a
+     *            modification on the entry.
+     * @param name
+     *            DN of the entry to modify.
+     * @param attributeDescription
+     *            Attribute to modify. Must take a String value.
+     * @throws LdapException
+     *             Modify failed.
+     */
+    private static void updateEntry(final Connection connection, final String name,
+            final String attributeDescription) throws LdapException {
+        ModifyRequest request = Requests.newModifyRequest(name)
+                .addModification(ModificationType.REPLACE, attributeDescription, "This is a String.");
+        connection.modify(request);
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private ParseAttributes() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/PasswordResetForAD.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/PasswordResetForAD.java
new file mode 100644
index 0000000..deb6d05
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/PasswordResetForAD.java
@@ -0,0 +1,171 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.util.Options;
+
+import javax.net.ssl.SSLContext;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+
+/**
+ * This command-line client demonstrates how to reset a user password in
+ * Microsoft Active Directory.
+ * <br>
+ * The client takes as arguments the host and port of the Active Directory
+ * server, a flag indicating whether this is a self-reset (user changing own
+ * password) or an administrative reset (administrator changing a password),
+ * the DN and password of the user performing the reset, and target user DN
+ * and new user password.
+ */
+public final class PasswordResetForAD {
+
+    /**
+     * Reset a user password in Microsoft Active Directory.
+     * <br>
+     * The connection should be LDAPS, not LDAP, in order to perform the
+     * modification.
+     *
+     * @param args The command line arguments: host, port, "admin"|"self",
+     *             DN, password, targetDN, newPassword
+     */
+    public static void main(final String[] args) {
+        // --- JCite main ---
+        if (args.length != 7) {
+            System.err.println("Usage: host port \"admin\"|\"self\" DN "
+                    + "password targetDN newPassword");
+            System.err.println("For example: ad.example.com 636 admin "
+                    + "cn=administrator,cn=Users,DC=ad,DC=example,DC=com "
+                    + "Secret123 cn=testuser,cn=Users,DC=ad,DC=example,DC=com "
+                    + "NewP4s5w0rd");
+            System.exit(1);
+        }
+        final String host = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String mode = args[2];
+        final String bindDN = args[3];
+        final String bindPassword = args[4];
+        final String targetDN = args[5];
+        final String newPassword = args[6];
+
+        Connection connection = null;
+        try {
+            final LDAPConnectionFactory factory =
+                    new LDAPConnectionFactory(host, port, getTrustAllOptions());
+            connection = factory.getConnection();
+            connection.bind(bindDN, bindPassword.toCharArray());
+
+            ModifyRequest request =
+                    Requests.newModifyRequest(DN.valueOf(targetDN));
+            String passwordAttribute = "unicodePwd";
+
+            if ("admin".equalsIgnoreCase(mode)) {
+                // Request modify, replacing the password with the new.
+
+                request.addModification(
+                        ModificationType.REPLACE,
+                        passwordAttribute,
+                        encodePassword(newPassword)
+                );
+            } else if ("self".equalsIgnoreCase(mode)) {
+                // Request modify, deleting the old password, adding the new.
+
+                // The default password policy for Active Directory domain
+                // controller systems sets minimum password age to 1 (day).
+                // If you get a constraint violation error when trying this
+                // example, set this minimum password age to 0 by executing
+                // cmd.exe as Administrator and entering the following
+                // command at the prompt:
+                //
+                // net accounts /MINPWAGE:0
+
+                request.addModification(
+                        ModificationType.DELETE,
+                        passwordAttribute,
+                        encodePassword(bindPassword)
+                );
+                request.addModification(
+                        ModificationType.ADD,
+                        passwordAttribute,
+                        encodePassword(newPassword)
+                );
+            } else {
+                System.err.println("Mode must be admin or self, not " + mode);
+                System.exit(1);
+            }
+
+            connection.modify(request);
+
+            System.out.println("Successfully changed password for "
+                    + targetDN + " to " + newPassword + ".");
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (final GeneralSecurityException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite main ---
+    }
+
+    // --- JCite encodePassword ---
+    /**
+     * Encode new password in UTF-16LE format for use with Active Directory.
+     *
+     * @param password String representation of the password
+     * @return Byte array containing encoded password
+     */
+    public static byte[] encodePassword(final String password) {
+        return ("\"" + password + "\"").getBytes(Charset.forName("UTF-16LE"));
+    }
+    // --- JCite encodePassword ---
+
+    /**
+     * For SSL the connection factory needs SSL context options. This
+     * implementation simply trusts all server certificates.
+     */
+    private static Options getTrustAllOptions() throws GeneralSecurityException {
+        Options options = Options.defaultOptions();
+        SSLContext sslContext = new SSLContextBuilder()
+              .setTrustManager(TrustManagers.trustAll()).getSSLContext();
+        options.set(SSL_CONTEXT, sslContext);
+        return options;
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private PasswordResetForAD() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java
new file mode 100644
index 0000000..da1f1f8
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java
@@ -0,0 +1,203 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.HEARTBEAT_ENABLED;
+import static org.forgerock.opendj.ldap.LDAPListener.CONNECT_MAX_BACKLOG;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandlerFactory;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.util.Options;
+
+/**
+ * An LDAP load balancing proxy which forwards requests to one or more remote
+ * Directory Servers. This is implementation is very simple and is only intended
+ * as an example:
+ * <ul>
+ * <li>It does not support SSL connections
+ * <li>It does not support StartTLS
+ * <li>It does not support Abandon or Cancel requests
+ * <li>Very basic authentication and authorization support.
+ * </ul>
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *     {@code [--load-balancer <mode>] <listenAddress> <listenPort> <proxyDN> <proxyPassword>
+ *         <remoteAddress1> <remotePort1> [<remoteAddress2> <remotePort2> ...]}
+ * </pre>
+ *
+ * Where {@code <mode>} is one of "round-robin", "fail-over", or "sharded". The default is round-robin.
+ */
+public final class Proxy {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: [--load-balancer <mode>] listen address, listen port,
+     *            remote address1, remote port1, remote address2, remote port2,
+     *            ...
+     */
+    public static void main(final String[] args) {
+        if (args.length < 6 || args.length % 2 != 0) {
+            System.err.println("Usage: [--load-balancer <mode>] listenAddress listenPort "
+                    + "proxyDN proxyPassword remoteAddress1 remotePort1 remoteAddress2 remotePort2 ...");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        int i = 0;
+
+        final LoadBalancingAlgorithm algorithm;
+        if ("--load-balancer".equals(args[i])) {
+            algorithm = getLoadBalancingAlgorithm(args[i + 1]);
+            i += 2;
+        } else {
+            algorithm = LoadBalancingAlgorithm.ROUND_ROBIN;
+        }
+
+        final String localAddress = args[i++];
+        final int localPort = Integer.parseInt(args[i++]);
+
+        final String proxyDN = args[i++];
+        final String proxyPassword = args[i++];
+
+        // Create load balancer.
+        // --- JCite pools ---
+        final List<ConnectionFactory> factories = new LinkedList<>();
+        final BindRequest bindRequest = newSimpleBindRequest(proxyDN, proxyPassword.toCharArray());
+        final Options factoryOptions = Options.defaultOptions()
+                                              .set(HEARTBEAT_ENABLED, true)
+                                              .set(AUTHN_BIND_REQUEST, bindRequest);
+
+        final List<ConnectionFactory> bindFactories = new LinkedList<>();
+        final Options bindFactoryOptions = Options.defaultOptions().set(HEARTBEAT_ENABLED, true);
+
+        for (; i < args.length; i += 2) {
+            final String remoteAddress = args[i];
+            final int remotePort = Integer.parseInt(args[i + 1]);
+
+            factories.add(Connections.newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                        remotePort,
+                                                                                        factoryOptions)));
+
+            bindFactories.add(Connections.newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                            remotePort,
+                                                                                            bindFactoryOptions)));
+        }
+        // --- JCite pools ---
+
+        final ConnectionFactory factory = algorithm.newLoadBalancer(factories, factoryOptions);
+        final ConnectionFactory bindFactory = algorithm.newLoadBalancer(bindFactories, bindFactoryOptions);
+
+        // --- JCite backend ---
+        /*
+         * Create a server connection adapter which will create a new proxy
+         * backend for each inbound client connection. This is required because
+         * we need to maintain authorization state between client requests.
+         */
+        final RequestHandlerFactory<LDAPClientContext, RequestContext> proxyFactory =
+                new RequestHandlerFactory<LDAPClientContext, RequestContext>() {
+                    @Override
+                    public ProxyBackend handleAccept(LDAPClientContext clientContext)
+                            throws LdapException {
+                        return new ProxyBackend(factory, bindFactory);
+                    }
+                };
+        final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
+                Connections.newServerConnectionFactory(proxyFactory);
+        // --- JCite backend ---
+
+        // --- JCite listener ---
+        // Create listener.
+        final Options options = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
+        LDAPListener listener = null;
+        try {
+            listener = new LDAPListener(localAddress, localPort, connectionHandler, options);
+            System.out.println("Press any key to stop the server...");
+            System.in.read();
+        } catch (final IOException e) {
+            System.out.println("Error listening on " + localAddress + ":" + localPort);
+            e.printStackTrace();
+        } finally {
+            if (listener != null) {
+                listener.close();
+            }
+        }
+        // --- JCite listener ---
+    }
+
+    private static LoadBalancingAlgorithm getLoadBalancingAlgorithm(final String algorithmName) {
+        switch (algorithmName) {
+        case "round-robin":
+            return LoadBalancingAlgorithm.ROUND_ROBIN;
+        case "fail-over":
+            return LoadBalancingAlgorithm.FAIL_OVER;
+        case "sharded":
+            return LoadBalancingAlgorithm.SHARDED;
+        default:
+            System.err.println("Unrecognized load-balancing algorithm '" + algorithmName + "'. Should be one of "
+                                       + "'round-robin', 'fail-over', or 'sharded'.");
+            System.exit(1);
+        }
+        return LoadBalancingAlgorithm.ROUND_ROBIN; // keep compiler happy.
+    }
+
+    private enum LoadBalancingAlgorithm {
+        ROUND_ROBIN {
+            @Override
+            ConnectionFactory newLoadBalancer(final Collection<ConnectionFactory> factories, final Options options) {
+                // --- JCite load balancer ---
+                return Connections.newRoundRobinLoadBalancer(factories, options);
+                // --- JCite load balancer ---
+            }
+        },
+        FAIL_OVER {
+            @Override
+            ConnectionFactory newLoadBalancer(final Collection<ConnectionFactory> factories, final Options options) {
+                return Connections.newFailoverLoadBalancer(factories, options);
+            }
+        },
+        SHARDED {
+            @Override
+            ConnectionFactory newLoadBalancer(final Collection<ConnectionFactory> factories, final Options options) {
+                return Connections.newShardedRequestLoadBalancer(factories, options);
+            }
+        };
+
+        abstract ConnectionFactory newLoadBalancer(Collection<ConnectionFactory> factories, Options options);
+    }
+
+    private Proxy() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ProxyBackend.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ProxyBackend.java
new file mode 100644
index 0000000..7073c8e
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ProxyBackend.java
@@ -0,0 +1,260 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.examples;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CancelExtendedRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.util.Utils.*;
+
+/**
+ * A simple proxy back-end which forwards requests to a connection factory using
+ * proxy authorization. Simple bind requests are performed on a separate
+ * connection factory dedicated for authentication.
+ * <p>
+ * This is implementation is very simple and is only intended as an example:
+ * <ul>
+ * <li>It does not support SSL connections
+ * <li>It does not support StartTLS
+ * <li>It does not support Abandon or Cancel requests
+ * <li>Very basic authentication and authorization support.
+ * </ul>
+ * <b>NOTE:</b> a proxy back-end is stateful due to its use of proxy
+ * authorization. Therefore, a proxy backend must be created for each inbound
+ * client connection. The following code illustrates how this may be achieved:
+ *
+ * <pre>
+ * final RequestHandlerFactory<LDAPClientContext, RequestContext> proxyFactory =
+ *     new RequestHandlerFactory<LDAPClientContext, RequestContext>() {
+ *         @Override
+ *         public ProxyBackend handleAccept(LDAPClientContext clientContext) throws LdapException {
+ *             return new ProxyBackend(factory, bindFactory);
+ *         }
+ *     };
+ * final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler = Connections
+ *     .newServerConnectionFactory(proxyFactory);}
+ * </pre>
+ */
+final class ProxyBackend implements RequestHandler<RequestContext> {
+
+    private final ConnectionFactory bindFactory;
+    private final ConnectionFactory factory;
+    private volatile ProxiedAuthV2RequestControl proxiedAuthControl;
+
+    ProxyBackend(final ConnectionFactory factory, final ConnectionFactory bindFactory) {
+        this.factory = factory;
+        this.bindFactory = bindFactory;
+    }
+
+    @Override
+    public void handleAdd(final RequestContext requestContext, final AddRequest request,
+        final IntermediateResponseHandler intermediateResponseHandler, final LdapResultHandler<Result> resultHandler) {
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        addProxiedAuthControl(request);
+
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.addAsync(request, intermediateResponseHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    @Override
+    public void handleBind(final RequestContext requestContext, final int version, final BindRequest request,
+        final IntermediateResponseHandler intermediateResponseHandler,
+        final LdapResultHandler<BindResult> resultHandler) {
+
+        if (request.getAuthenticationType() != BindRequest.AUTHENTICATION_TYPE_SIMPLE) {
+            // TODO: SASL authentication not implemented.
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR,
+                    "non-SIMPLE authentication not supported: " + request.getAuthenticationType()));
+        } else {
+            // Authenticate using a separate bind connection pool, because
+            // we don't want to change the state of the pooled connection.
+            final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+            proxiedAuthControl = null;
+            bindFactory.getConnectionAsync()
+                    .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                        @Override
+                        public Promise<BindResult, LdapException> apply(Connection connection) throws LdapException {
+                            connectionHolder.set(connection);
+                            return connection.bindAsync(request, intermediateResponseHandler);
+                        }
+                    }).thenOnResult(new ResultHandler<BindResult>() {
+                        @Override
+                        public final void handleResult(final BindResult result) {
+                            proxiedAuthControl = ProxiedAuthV2RequestControl.newControl("dn:" + request.getName());
+                            resultHandler.handleResult(result);
+                        }
+                    }).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+        }
+    }
+
+    @Override
+    public void handleCompare(final RequestContext requestContext, final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<CompareResult> resultHandler) {
+        addProxiedAuthControl(request);
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, CompareResult, LdapException>() {
+            @Override
+            public Promise<CompareResult, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.compareAsync(request, intermediateResponseHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    @Override
+    public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        addProxiedAuthControl(request);
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.deleteAsync(request, intermediateResponseHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    @Override
+    public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext,
+        final ExtendedRequest<R> request, final IntermediateResponseHandler intermediateResponseHandler,
+        final LdapResultHandler<R> resultHandler) {
+        if (CancelExtendedRequest.OID.equals(request.getOID())) {
+            // TODO: not implemented.
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR,
+                "Cancel extended request operation not supported"));
+        } else if (StartTLSExtendedRequest.OID.equals(request.getOID())) {
+            // TODO: not implemented.
+            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR,
+                "StartTLS extended request operation not supported"));
+        } else {
+            // Forward all other extended operations.
+            addProxiedAuthControl(request);
+
+            final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+            factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, R, LdapException>() {
+                @Override
+                public Promise<R, LdapException> apply(Connection connection) throws LdapException {
+                    connectionHolder.set(connection);
+                    return connection.extendedRequestAsync(request, intermediateResponseHandler);
+                }
+            }).thenOnResult(resultHandler).thenOnException(resultHandler)
+                .thenAlways(close(connectionHolder));
+        }
+    }
+
+    @Override
+    public void handleModify(final RequestContext requestContext, final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        addProxiedAuthControl(request);
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.modifyAsync(request, intermediateResponseHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    @Override
+    public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
+        final IntermediateResponseHandler intermediateResponseHandler, final LdapResultHandler<Result> resultHandler) {
+        addProxiedAuthControl(request);
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.modifyDNAsync(request, intermediateResponseHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    @Override
+    public void handleSearch(final RequestContext requestContext, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
+            final LdapResultHandler<Result> resultHandler) {
+        addProxiedAuthControl(request);
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(Connection connection) throws LdapException {
+                connectionHolder.set(connection);
+                return connection.searchAsync(request, intermediateResponseHandler, entryHandler);
+            }
+        }).thenOnResult(resultHandler).thenOnException(resultHandler).thenAlways(close(connectionHolder));
+    }
+
+    private void addProxiedAuthControl(final Request request) {
+        final ProxiedAuthV2RequestControl control = proxiedAuthControl;
+        if (control != null) {
+            request.addControl(control);
+        }
+    }
+
+    private Runnable close(final AtomicReference<Connection> c) {
+        return new Runnable() {
+            @Override
+            public void run() {
+                closeSilently(c.get());
+            }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ReadSchema.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ReadSchema.java
new file mode 100644
index 0000000..47a6129
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ReadSchema.java
@@ -0,0 +1,117 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.Syntax;
+
+/**
+ * An example client application which prints a summary of the schema on the
+ * named server as well as any warnings encountered while parsing the schema.
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password>}
+ * </pre>
+ */
+public final class ReadSchema {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password.
+     */
+    public static void main(final String[] args) {
+        if (args.length != 4) {
+            System.err.println("Usage: host port username password");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final String password = args[3];
+
+        // --- JCite ---
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(hostName, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(userName, password.toCharArray());
+
+            // Read the schema.
+            Schema schema = Schema.readSchemaForEntry(connection, DN.rootDN());
+
+            System.out.println("Attribute types");
+            for (AttributeType at : schema.getAttributeTypes()) {
+                System.out.println("  " + at.getNameOrOID());
+            }
+            System.out.println();
+
+            System.out.println("Object classes");
+            for (ObjectClass oc : schema.getObjectClasses()) {
+                System.out.println("  " + oc.getNameOrOID());
+            }
+            System.out.println();
+
+            System.out.println("Matching rules");
+            for (MatchingRule mr : schema.getMatchingRules()) {
+                System.out.println("  " + mr.getNameOrOID());
+            }
+            System.out.println();
+
+            System.out.println("Syntaxes");
+            for (Syntax s : schema.getSyntaxes()) {
+                System.out.println("  " + s.getDescription());
+            }
+            System.out.println();
+
+            // Etc...
+
+            System.out.println("WARNINGS");
+            for (LocalizableMessage m : schema.getWarnings()) {
+                System.out.println("  " + m);
+            }
+            System.out.println();
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private ReadSchema() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java
new file mode 100644
index 0000000..108b307
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java
@@ -0,0 +1,438 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
+import static org.forgerock.opendj.ldap.LDAPListener.CONNECT_MAX_BACKLOG;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Attributes;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.RequestHandlerFactory;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.util.Options;
+
+/**
+ * This example is based on the {@link Proxy}. This example does no load
+ * balancing, but instead rewrites attribute descriptions and DN suffixes in
+ * requests to and responses from a directory server using hard coded
+ * configuration.
+ * <ul>
+ * <li>It transforms DNs ending in {@code o=example} on the client side to end
+ * in {@code dc=example,dc=com} on the server side and vice versa.
+ * <li>It transforms the attribute description {@code fullname} on the client
+ * side to {@code cn} on the server side and vice versa.
+ * </ul>
+ *
+ * This example has a number of restrictions.
+ * <ul>
+ * <li>It does not support SSL connections.
+ * <li>It does not support StartTLS.
+ * <li>It does not support Abandon or Cancel requests.
+ * <li>It has very basic authentication and authorization support.
+ * <li>It does not rewrite bind DNs.
+ * <li>It uses proxied authorization, so if you use OpenDJ directory server, you
+ * must set the {@code proxied-auth} privilege for the proxy user.
+ * <li>It does not touch matched DNs in results.
+ * <li>It does not rewrite attributes with options in search result entries.
+ * <li>It does not touch search result references.
+ * </ul>
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <localAddress> <localPort> <proxyDN> <proxyPassword> <serverAddress> <serverPort>}
+ * </pre>
+ *
+ * If you have imported the users from <a
+ * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>, then you
+ * can set {@code proxyUserDN} to {@code cn=My App,ou=Apps,dc=example,dc=com}
+ * and {@code proxyUserPassword} to {@code password}.
+ */
+public final class RewriterProxy {
+    private static final class Rewriter implements RequestHandler<RequestContext> {
+
+        /** This example hard codes the attribute... */
+        private static final String CLIENT_ATTRIBUTE = "fullname";
+        private static final String SERVER_ATTRIBUTE = "cn";
+
+        /** ...and DN rewriting configuration. */
+        private static final String CLIENT_SUFFIX = "o=example";
+        private static final String SERVER_SUFFIX = "dc=example,dc=com";
+
+        private final AttributeDescription clientAttributeDescription = AttributeDescription
+                .valueOf(CLIENT_ATTRIBUTE);
+        private final AttributeDescription serverAttributeDescription = AttributeDescription
+                .valueOf(SERVER_ATTRIBUTE);
+
+        /** Next request handler in the chain. */
+        private final RequestHandler<RequestContext> nextHandler;
+
+        private Rewriter(final RequestHandler<RequestContext> nextHandler) {
+            this.nextHandler = nextHandler;
+        }
+
+        @Override
+        public void handleAdd(final RequestContext requestContext, final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            nextHandler.handleAdd(requestContext, rewrite(request), intermediateResponseHandler,
+                    resultHandler);
+        }
+
+        @Override
+        public void handleBind(final RequestContext requestContext, final int version,
+                final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<BindResult> resultHandler) {
+            nextHandler.handleBind(requestContext, version, rewrite(request),
+                    intermediateResponseHandler, resultHandler);
+        }
+
+        @Override
+        public void handleCompare(final RequestContext requestContext,
+                final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<CompareResult> resultHandler) {
+            nextHandler.handleCompare(requestContext, rewrite(request),
+                    intermediateResponseHandler, resultHandler);
+        }
+
+        @Override
+        public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            nextHandler.handleDelete(requestContext, rewrite(request), intermediateResponseHandler,
+                    resultHandler);
+        }
+
+        @Override
+        public <R extends ExtendedResult> void handleExtendedRequest(
+                final RequestContext requestContext, final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<R> resultHandler) {
+            nextHandler.handleExtendedRequest(requestContext, rewrite(request),
+                    intermediateResponseHandler, resultHandler);
+        }
+
+        @Override
+        public void handleModify(final RequestContext requestContext, final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            nextHandler.handleModify(requestContext, rewrite(request), intermediateResponseHandler,
+                    resultHandler);
+        }
+
+        @Override
+        public void handleModifyDN(final RequestContext requestContext,
+                final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final LdapResultHandler<Result> resultHandler) {
+            nextHandler.handleModifyDN(requestContext, rewrite(request),
+                    intermediateResponseHandler, resultHandler);
+        }
+
+        @Override
+        public void handleSearch(final RequestContext requestContext, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final SearchResultHandler entryHandler, final LdapResultHandler<Result> resultHandler) {
+            nextHandler.handleSearch(requestContext, rewrite(request), intermediateResponseHandler,
+                new SearchResultHandler() {
+                    @Override
+                    public boolean handleReference(SearchResultReference reference) {
+                        return entryHandler.handleReference(reference);
+                    }
+
+                    @Override
+                    public boolean handleEntry(SearchResultEntry entry) {
+                        return entryHandler.handleEntry(rewrite(entry));
+                    }
+                }, resultHandler);
+        }
+
+        private AddRequest rewrite(final AddRequest request) {
+            // Transform the client DN into a server DN.
+            final AddRequest rewrittenRequest = Requests.copyOfAddRequest(request);
+            rewrittenRequest.setName(request.getName().toString().replace(CLIENT_SUFFIX,
+                    SERVER_SUFFIX));
+            /*
+             * Transform the client attribute names into server attribute names,
+             * fullname;lang-fr ==> cn;lang-fr.
+             */
+            for (final Attribute a : request.getAllAttributes(clientAttributeDescription)) {
+                if (a != null) {
+                    final String ad =
+                            a.getAttributeDescriptionAsString().replaceFirst(
+                                    CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
+                    final Attribute serverAttr =
+                            Attributes.renameAttribute(a, AttributeDescription.valueOf(ad));
+                    rewrittenRequest.addAttribute(serverAttr);
+                    rewrittenRequest.removeAttribute(a.getAttributeDescription());
+                }
+            }
+            return rewrittenRequest;
+        }
+
+        private BindRequest rewrite(final BindRequest request) {
+            // TODO: Transform client DN into server DN.
+            return request;
+        }
+
+        private CompareRequest rewrite(final CompareRequest request) {
+            /*
+             * Transform the client attribute name into a server attribute name,
+             * fullname;lang-fr ==> cn;lang-fr.
+             */
+            final String ad = request.getAttributeDescription().toString();
+            if (ad.toLowerCase().startsWith(CLIENT_ATTRIBUTE.toLowerCase())) {
+                final String serverAttrDesc =
+                        ad.replaceFirst(CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
+                request.setAttributeDescription(AttributeDescription.valueOf(serverAttrDesc));
+            }
+
+            // Transform the client DN into a server DN.
+            return request
+                    .setName(request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX));
+        }
+
+        private DeleteRequest rewrite(final DeleteRequest request) {
+            // Transform the client DN into a server DN.
+            return request
+                    .setName(request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX));
+        }
+
+        private <S extends ExtendedResult> ExtendedRequest<S> rewrite(
+                final ExtendedRequest<S> request) {
+            // TODO: Transform password modify, etc.
+            return request;
+        }
+
+        private ModifyDNRequest rewrite(final ModifyDNRequest request) {
+            // Transform the client DNs into server DNs.
+            if (request.getNewSuperior() != null) {
+                return request.setName(
+                        request.getName().toString().replace(CLIENT_SUFFIX, SERVER_SUFFIX))
+                        .setNewSuperior(
+                                request.getNewSuperior().toString().replace(CLIENT_SUFFIX,
+                                        SERVER_SUFFIX));
+            } else {
+                return request.setName(request.getName().toString().replace(CLIENT_SUFFIX,
+                        SERVER_SUFFIX));
+            }
+        }
+
+        private ModifyRequest rewrite(final ModifyRequest request) {
+            // Transform the client DN into a server DN.
+            final ModifyRequest rewrittenRequest =
+                    Requests.newModifyRequest(request.getName().toString().replace(CLIENT_SUFFIX,
+                            SERVER_SUFFIX));
+
+            /*
+             * Transform the client attribute names into server attribute names,
+             * fullname;lang-fr ==> cn;lang-fr.
+             */
+            final List<Modification> mods = request.getModifications();
+            for (final Modification mod : mods) {
+                final Attribute a = mod.getAttribute();
+                final AttributeDescription ad = a.getAttributeDescription();
+                final AttributeType at = ad.getAttributeType();
+
+                if (at.equals(clientAttributeDescription.getAttributeType())) {
+                    final AttributeDescription serverAttrDesc =
+                            AttributeDescription.valueOf(ad.toString().replaceFirst(
+                                    CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE));
+                    rewrittenRequest.addModification(new Modification(mod.getModificationType(),
+                            Attributes.renameAttribute(a, serverAttrDesc)));
+                } else {
+                    rewrittenRequest.addModification(mod);
+                }
+            }
+            for (final Control control : request.getControls()) {
+                rewrittenRequest.addControl(control);
+            }
+
+            return rewrittenRequest;
+        }
+
+        private SearchRequest rewrite(final SearchRequest request) {
+            /*
+             * Transform the client attribute names to a server attribute names,
+             * fullname;lang-fr ==> cn;lang-fr.
+             */
+            final String[] a = new String[request.getAttributes().size()];
+            int count = 0;
+            for (final String attrName : request.getAttributes()) {
+                if (attrName.toLowerCase().startsWith(CLIENT_ATTRIBUTE.toLowerCase())) {
+                    a[count] =
+                            attrName.replaceFirst(CLIENT_ATTRIBUTE, SERVER_ATTRIBUTE);
+                } else {
+                    a[count] = attrName;
+                }
+                ++count;
+            }
+
+            /*
+             * Rewrite the baseDN, and rewrite the Filter in dangerously lazy
+             * fashion. All the filter rewrite does is a string replace, so if
+             * the client attribute name appears in the value part of the AVA,
+             * this implementation will not work.
+             */
+            return Requests.newSearchRequest(DN.valueOf(request.getName().toString().replace(
+                    CLIENT_SUFFIX, SERVER_SUFFIX)), request.getScope(), Filter.valueOf(request
+                    .getFilter().toString().replace(CLIENT_ATTRIBUTE,
+                            SERVER_ATTRIBUTE)), a);
+        }
+
+        private SearchResultEntry rewrite(final SearchResultEntry entry) {
+            // Replace server attributes with client attributes.
+            final Set<Attribute> attrsToAdd = new HashSet<>();
+            final Set<AttributeDescription> attrsToRemove = new HashSet<>();
+
+            for (final Attribute a : entry.getAllAttributes(serverAttributeDescription)) {
+                final AttributeDescription ad = a.getAttributeDescription();
+                final AttributeType at = ad.getAttributeType();
+                if (at.equals(serverAttributeDescription.getAttributeType())) {
+                    final AttributeDescription clientAttrDesc =
+                            AttributeDescription.valueOf(ad.toString().replaceFirst(
+                                    SERVER_ATTRIBUTE, CLIENT_ATTRIBUTE));
+                    attrsToAdd.add(Attributes.renameAttribute(a, clientAttrDesc));
+                    attrsToRemove.add(ad);
+                }
+            }
+
+            if (!attrsToAdd.isEmpty() && !attrsToRemove.isEmpty()) {
+                for (final Attribute a : attrsToAdd) {
+                    entry.addAttribute(a);
+                }
+                for (final AttributeDescription ad : attrsToRemove) {
+                    entry.removeAttribute(ad);
+                }
+            }
+
+            // Transform the server DN suffix into a client DN suffix.
+            return entry.setName(entry.getName().toString().replace(SERVER_SUFFIX, CLIENT_SUFFIX));
+
+        }
+
+    }
+
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: local address, local port, proxy
+     *            user DN, proxy user password, server address, server port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 6) {
+            System.err.println("Usage:" + "\tlocalAddress localPort proxyDN proxyPassword "
+                    + "serverAddress serverPort");
+            System.exit(1);
+        }
+
+        final String localAddress = args[0];
+        final int localPort = Integer.parseInt(args[1]);
+        final String proxyDN = args[2];
+        final String proxyPassword = args[3];
+        final String remoteAddress = args[4];
+        final int remotePort = Integer.parseInt(args[5]);
+
+        // Create connection factories.
+        final Options factoryOptions = Options.defaultOptions()
+                                   .set(LDAPConnectionFactory.AUTHN_BIND_REQUEST,
+                                        newSimpleBindRequest(proxyDN, proxyPassword.toCharArray()));
+        final ConnectionFactory factory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                            remotePort,
+                                                                                            factoryOptions));
+        final ConnectionFactory bindFactory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                                remotePort));
+
+        /*
+         * Create a server connection adapter which will create a new proxy
+         * backend for each inbound client connection. This is required because
+         * we need to maintain authorization state between client requests. The
+         * proxy bound will be wrapped in a rewriter in order to transform
+         * inbound requests and their responses.
+         */
+        final RequestHandlerFactory<LDAPClientContext, RequestContext> proxyFactory =
+                new RequestHandlerFactory<LDAPClientContext, RequestContext>() {
+                    @Override
+                    public Rewriter handleAccept(final LDAPClientContext clientContext) throws LdapException {
+                        return new Rewriter(new ProxyBackend(factory, bindFactory));
+                    }
+                };
+        final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
+                Connections.newServerConnectionFactory(proxyFactory);
+
+        // Create listener.
+        final Options listenerOptions = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
+        LDAPListener listener = null;
+        try {
+            listener = new LDAPListener(localAddress, localPort, connectionHandler, listenerOptions);
+            System.out.println("Press any key to stop the server...");
+            System.in.read();
+        } catch (final IOException e) {
+            System.out.println("Error listening on " + localAddress + ":" + localPort);
+            e.printStackTrace();
+        } finally {
+            if (listener != null) {
+                listener.close();
+            }
+        }
+    }
+
+    private RewriterProxy() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java
new file mode 100644
index 0000000..ea4c563
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java
@@ -0,0 +1,171 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2015 ForgeRock AS.
+ */
+
+/**
+ * An example client application which performs SASL authentication to a
+ * directory server, displays a result, and closes the connection.
+ *
+ * Set up StartTLS before using this example.
+ */
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
+
+
+import java.security.GeneralSecurityException;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.util.Options;
+
+/**
+ * An example client application which performs SASL PLAIN authentication to a
+ * directory server over LDAP with StartTLS. This example takes the following
+ * command line parameters:
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server for StartTLS</li>
+ * <li>authzid - (Optional) Authorization identity</li>
+ * <li>authcid - Authentication identity</li>
+ * <li>passwd - Password of the user to authenticate</li>
+ * </ul>
+ * The host, port, authcid, and passwd are required. SASL PLAIN is described in
+ * <a href="http://www.ietf.org/rfc/rfc4616.txt">RFC 4616</a>.
+ * <p>
+ * The authzid and authcid are prefixed as described in <a
+ * href="http://tools.ietf.org/html/rfc4513#section-5.2.1.8">RFC 4513, section
+ * 5.2.1.8</a>, with "dn:" if you pass in a distinguished name, or with "u:" if
+ * you pass in a user ID.
+ * <p>
+ * By default, OpenDJ is set up for SASL PLAIN to use the Exact Match Identity
+ * Mapper to find entries by searching uid values for the user ID. In other
+ * words, the following examples are equivalent.
+ *
+ * <pre>
+ * dn:uid=bjensen,ou=people,dc=example,dc=com
+ * u:bjensen
+ * </pre>
+ */
+public final class SASLAuth {
+    /**
+     * Authenticate to the directory using SASL PLAIN.
+     *
+     * @param args
+     *            The command line arguments
+     */
+    public static void main(String[] args) {
+        parseArgs(args);
+        Connection connection = null;
+
+        // --- JCite ---
+        try {
+            final LDAPConnectionFactory factory =
+                    new LDAPConnectionFactory(host, port, getTrustAllOptions());
+            connection = factory.getConnection();
+            PlainSASLBindRequest request =
+                    Requests.newPlainSASLBindRequest(authcid, passwd.toCharArray())
+                    .setAuthorizationID(authzid);
+            connection.bind(request);
+            System.out.println("Authenticated as " + authcid + ".");
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final GeneralSecurityException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    /**
+     * For StartTLS the connection factory needs SSL context options. In the
+     * general case, a trust manager in the SSL context serves to check server
+     * certificates, and a key manager handles client keys when the server
+     * checks certificates from our client.
+     *
+     * OpenDJ directory server lets you install by default with a self-signed
+     * certificate that is not in the system trust store. To simplify this
+     * implementation trusts all server certificates.
+     */
+    private static Options getTrustAllOptions() throws GeneralSecurityException {
+        Options options = Options.defaultOptions();
+        SSLContext sslContext =
+                new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()).getSSLContext();
+        options.set(SSL_CONTEXT, sslContext);
+        options.set(SSL_USE_STARTTLS, true);
+        return options;
+    }
+
+    private static String host;
+    private static int port;
+    private static String authzid;
+    private static String authcid;
+    private static String passwd;
+
+    /**
+     * Parse command line arguments.
+     *
+     * @param args
+     *            host port [authzid] authcid passwd
+     */
+    private static void parseArgs(String[] args) {
+        if (args.length < 4 || args.length > 5) {
+            giveUp();
+        }
+
+        host = args[0];
+        port = Integer.parseInt(args[1]);
+
+        if (args.length == 5) {
+            authzid = args[2];
+            authcid = args[3];
+            passwd = args[4];
+        } else {
+            authzid = null;
+            authcid = args[2];
+            passwd = args[3];
+        }
+    }
+
+    private static void giveUp() {
+        printUsage();
+        System.exit(1);
+    }
+
+    private static void printUsage() {
+        System.err.println("Usage: host port [authzid] authcid passwd");
+        System.err.println("\tThe port must be able to handle LDAP with StartTLS.");
+        System.err.println("\tSee http://www.ietf.org/rfc/rfc4616.txt for more on SASL PLAIN.");
+    }
+
+    private SASLAuth() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Search.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Search.java
new file mode 100644
index 0000000..da511b4
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Search.java
@@ -0,0 +1,125 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * An example client application which searches a Directory Server. This example
+ * takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password>
+ *      <baseDN> <scope> <filter> [<attribute> <attribute> ...]}
+ * </pre>
+ */
+public final class Search {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password,
+     *            base DN, scope, filter, and zero or more attributes to be
+     *            retrieved.
+     */
+    public static void main(final String[] args) {
+        if (args.length < 7) {
+            System.err.println("Usage: host port username password baseDN scope "
+                    + "filter [attribute ...]");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final String password = args[3];
+        final String baseDN = args[4];
+        final String scopeString = args[5];
+        final String filter = args[6];
+        String[] attributes;
+        if (args.length > 7) {
+            attributes = Arrays.copyOfRange(args, 7, args.length);
+        } else {
+            attributes = new String[0];
+        }
+
+        final SearchScope scope = SearchScope.valueOf(scopeString);
+        if (scope == null) {
+            System.err.println("Unknown scope: " + scopeString);
+            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+            return;
+        }
+
+        // --- JCite ---
+        // Create an LDIF writer which will write the search results to stdout.
+        final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(hostName, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(userName, password.toCharArray());
+
+            // Read the entries and output them as LDIF.
+            final ConnectionEntryReader reader =
+                    connection.search(baseDN, scope, filter, attributes);
+            while (reader.hasNext()) {
+                if (!reader.isReference()) {
+                    final SearchResultEntry entry = reader.readEntry();
+                    writer.writeComment("Search result entry: " + entry.getName());
+                    writer.writeEntry(entry);
+                } else {
+                    final SearchResultReference ref = reader.readReference();
+
+                    // Got a continuation reference.
+                    writer.writeComment("Search result reference: " + ref.getURIs());
+                }
+            }
+            writer.flush();
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private Search() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchAsync.java
new file mode 100644
index 0000000..9d52a64
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchAsync.java
@@ -0,0 +1,241 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.CancelExtendedRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+/**
+ * An example client application which searches a Directory Server using the
+ * asynchronous APIs. This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password>
+ *      <baseDN> <scope> <filter> [<attribute> <attribute> ...]}
+ * </pre>
+ */
+public final class SearchAsync {
+    // --- JCite search result handler ---
+    private static final class SearchResultHandlerImpl implements SearchResultHandler {
+        @Override
+        public synchronized boolean handleEntry(final SearchResultEntry entry) {
+            try {
+                if (entryCount < 10) {
+                    WRITER.writeComment("Search result entry: " + entry.getName());
+                    WRITER.writeEntry(entry);
+                    ++entryCount;
+                } else { // Cancel the search.
+                    CancelExtendedRequest request =
+                            Requests.newCancelExtendedRequest(requestID);
+                    connection.extendedRequestAsync(request)
+                            .thenOnResult(new ResultHandler<ExtendedResult>() {
+                                @Override
+                                public void handleResult(ExtendedResult result) {
+                                    System.err.println("Cancel request succeeded");
+                                    CANCEL_LATCH.countDown();
+                                }
+                            })
+                            .thenOnException(new ExceptionHandler<LdapException>() {
+                                @Override
+                                public void handleException(LdapException exception) {
+                                    System.err.println("Cancel request failed: "
+                                            + exception.getResult().getResultCode().intValue()
+                                            + " "
+                                            + exception.getResult().getDiagnosticMessage());
+                                    CANCEL_LATCH.countDown();
+                                }
+                            });
+                    return false;
+                }
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                resultCode = ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+                COMPLETION_LATCH.countDown();
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public synchronized boolean handleReference(final SearchResultReference reference) {
+            try {
+                WRITER.writeComment("Search result reference: " + reference.getURIs());
+            } catch (final IOException e) {
+                System.err.println(e.getMessage());
+                resultCode = ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+                COMPLETION_LATCH.countDown();
+                return false;
+            }
+            return true;
+        }
+
+    }
+    // --- JCite search result handler ---
+
+    // --- JCite decl1 ---
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+    private static final CountDownLatch CANCEL_LATCH = new CountDownLatch(1);
+    private static final LDIFEntryWriter WRITER = new LDIFEntryWriter(System.out);
+    // --- JCite decl1 ---
+    private static String userName;
+    private static String password;
+    private static String baseDN;
+    private static SearchScope scope;
+    private static String filter;
+    private static String[] attributes;
+    private static Connection connection;
+    private static int resultCode;
+
+    // --- JCite decl2 ---
+    static int requestID;
+    static int entryCount;
+    // --- JCite decl2 ---
+
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, username, password,
+     *            base DN, scope, filter, and zero or more attributes to be
+     *            retrieved.
+     */
+    public static void main(final String[] args) {
+        if (args.length < 7) {
+            System.err.println("Usage: host port username password baseDN scope " + "filter [attribute ...]");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        userName = args[2];
+        password = args[3];
+        baseDN = args[4];
+        final String scopeString = args[5];
+        filter = args[6];
+        if (args.length > 7) {
+            attributes = Arrays.copyOfRange(args, 7, args.length);
+        } else {
+            attributes = new String[0];
+        }
+
+        scope = SearchScope.valueOf(scopeString);
+        if (scope == null) {
+            System.err.println("Unknown scope: " + scopeString);
+            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+            return;
+        }
+
+        // --- Using Promises ---
+        // Initiate the asynchronous connect, bind, and search.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(hostName, port);
+
+        factory.getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        SearchAsync.connection = connection;
+                        return connection.bindAsync(Requests
+                                .newSimpleBindRequest(userName, password.toCharArray()));
+                    }
+                })
+                .thenAsync(new AsyncFunction<BindResult, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(BindResult result)
+                            throws LdapException {
+                        LdapPromise<Result> promise = connection.searchAsync(
+                                Requests.newSearchRequest(baseDN, scope, filter, attributes),
+                                new SearchResultHandlerImpl());
+                        requestID = promise.getRequestID();
+                        return promise;
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        resultCode = result.getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException exception) {
+                        System.err.println(exception.getMessage());
+                        resultCode = exception.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+        // --- Using Promises ---
+
+        // Await completion.
+        try {
+            COMPLETION_LATCH.await();
+        } catch (final InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        try {
+            WRITER.flush();
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            return;
+        }
+
+        // Await completion of the cancel request.
+        try {
+            CANCEL_LATCH.await();
+        } catch (final InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        if (connection != null) {
+            connection.close();
+        }
+
+        System.exit(resultCode);
+    }
+
+    private SearchAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBind.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBind.java
new file mode 100644
index 0000000..75bfd78
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBind.java
@@ -0,0 +1,103 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.Console;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+
+/**
+ * An interactive command-line client that performs a search and subsequent
+ * simple bind. The client prompts for email address and for a password, and
+ * then searches based on the email address, to bind as the user with the
+ * password. If successful, the client displays the common name from the user's
+ * entry.
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server</li>
+ * <li>base-dn - base DN for the search, e.g. dc=example,dc=com</li>
+ * </ul>
+ * All arguments are required.
+ */
+public final class SearchBind {
+    /**
+     * Prompt for email and password, search and bind, then display message.
+     *
+     * @param args
+     *            The command line arguments: host, port, base-dn.
+     */
+    public static void main(final String[] args) {
+        if (args.length != 3) {
+            System.err.println("Usage: host port base-dn");
+            System.err.println("For example: localhost 1389 dc=example,dc=com");
+            System.exit(1);
+        }
+        String host = args[0];
+        int port = Integer.parseInt(args[1]);
+        String baseDN = args[2];
+
+        // --- JCite ---
+        // Prompt for mail and password.
+        Console c = System.console();
+        if (c == null) {
+            System.err.println("No console.");
+            System.exit(1);
+        }
+
+        String mail = c.readLine("Email address: ");
+        char[] password = c.readPassword("Password: ");
+
+        // Search using mail address, and then bind with the DN and password.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+        try {
+            connection = factory.getConnection();
+            SearchResultEntry entry =
+                    connection.searchSingleEntry(baseDN,
+                            SearchScope.WHOLE_SUBTREE,
+                            Filter.equality("mail", mail).toString(),
+                            "cn");
+            DN bindDN = entry.getName();
+            connection.bind(bindDN.toString(), password);
+
+            String cn = entry.getAttribute("cn").firstValueAsString();
+            System.out.println("Hello, " + cn + "!");
+        } catch (final LdapException e) {
+            System.err.println("Failed to bind.");
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private SearchBind() {
+        // Not used
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBindAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBindAsync.java
new file mode 100644
index 0000000..3d59c2c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SearchBindAsync.java
@@ -0,0 +1,173 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.util.Utils.closeSilently;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * An interactive command-line client that performs a search and simple bind
+ * using the asynchronous APIs.
+ * <br>
+ * The client prompts for email address and for a password,
+ * and then searches based on the email address,
+ * to bind as the user with the password.
+ * <br>
+ * If successful, the client displays the common name from the user's entry.
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server</li>
+ * <li>base-dn - base DN for the search, e.g. dc=example,dc=com</li>
+ * </ul>
+ * All arguments are required.
+ */
+public final class SearchBindAsync {
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Email address provided by user. */
+    private static String mail;
+    /** Password provided by user. */
+    private static char[] password;
+    /** Bind DN returned by the search. */
+    private static String bindDn;
+    /** Result for the operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Prompts for email and password, search and bind, then display message.
+     *
+     * @param args
+     *            The command line arguments: host, port, base-dn.
+     */
+    public static void main(final String[] args) {
+        if (args.length != 3) {
+            System.err.println("Usage: host port base-dn");
+            System.err.println("For example: localhost 1389 dc=example,dc=com");
+            System.exit(1);
+        }
+        final String host   = args[0];
+        final int    port   = Integer.parseInt(args[1]);
+        final String baseDn = args[2];
+
+        // Prompt for email address and password.
+        try {
+            mail = getInputLine("Email address:");
+            password = getInputLine("Password:").toCharArray();
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+            return;
+        }
+
+        // Connect to the server, search for the user entry based on email address, and bind.
+        new LDAPConnectionFactory(host, port)
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
+                    @Override
+                    public Promise<SearchResultEntry, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        SearchBindAsync.connection = connection;
+                        return connection.searchSingleEntryAsync(
+                                Requests.newSingleEntrySearchRequest(
+                                        baseDn,
+                                        SearchScope.WHOLE_SUBTREE,
+                                        Filter.equality("mail", mail).toString(),
+                                        "cn"));
+                    }
+                })
+                .thenAsync(new AsyncFunction<SearchResultEntry, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(SearchResultEntry searchResultEntry)
+                            throws LdapException {
+                        SearchBindAsync.bindDn = searchResultEntry.getName().toString();
+                        return SearchBindAsync.connection.bindAsync(
+                                Requests.newSimpleBindRequest(bindDn, password));
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        if (result.getResultCode() == ResultCode.SUCCESS) {
+                            System.out.println("Authenticated as " + SearchBindAsync.bindDn + ".");
+                        }
+                        resultCode = result.getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        }  catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    /**
+     * Returns an input string from System.in, after prompting on System.out.
+     * <br>
+     * Note: The input is echoed to the display.
+     *
+     * @param prompt    The prompt asking for input.
+     * @return An input string from System.in.
+     * @throws IOException Failed to read from System.in.
+     */
+    private static String getInputLine(final String prompt) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+        System.out.print(prompt + " ");
+        return reader.readLine();
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private SearchBindAsync() {
+        // Not used
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
new file mode 100644
index 0000000..a422e84
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
@@ -0,0 +1,139 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.LDAPListener.CONNECT_MAX_BACKLOG;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.KeyManagers;
+import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.ServerConnection;
+import org.forgerock.opendj.ldap.ServerConnectionFactory;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.util.Options;
+
+/**
+ * An LDAP directory server which exposes data contained in an LDIF file. This
+ * is implementation is very simple and is only intended as an example:
+ * <ul>
+ * <li>It does not support StartTLS
+ * <li>It does not support Abandon or Cancel requests
+ * <li>Very basic authentication and authorization support.
+ * </ul>
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <listenAddress> <listenPort> <ldifFile> [<keyStoreFile> <keyStorePassword> <certNickname>]}
+ * </pre>
+ */
+public final class Server {
+
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: listen address, listen port, ldifFile,
+     *            and optionally: key store, key store password and certificate nick name
+     */
+    public static void main(final String[] args) {
+        if (args.length != 3 && args.length != 6) {
+            System.err.println("Usage: listenAddress listenPort ldifFile "
+                    + "[keyStoreFile keyStorePassword certNickname]");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String localAddress = args[0];
+        final int localPort = Integer.parseInt(args[1]);
+        final String ldifFileName = args[2];
+        final String keyStoreFileName = (args.length == 6) ? args[3] : null;
+        final String keyStorePassword = (args.length == 6) ? args[4] : null;
+        final String certNickname = (args.length == 6) ? args[5] : null;
+
+        // Create the memory backend.
+        final MemoryBackend backend;
+        try {
+            backend = new MemoryBackend(new LDIFEntryReader(new FileInputStream(ldifFileName)));
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+            return; // Keep compiler quiet.
+        }
+
+        // Create a server connection adapter.
+        final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
+                Connections.newServerConnectionFactory(backend);
+
+        // Create listener.
+        LDAPListener listener = null;
+        try {
+            final Options options = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
+
+            if (keyStoreFileName != null) {
+                // Configure SSL/TLS and enable it when connections are
+                // accepted.
+                final SSLContext sslContext =
+                        new SSLContextBuilder().setKeyManager(
+                                KeyManagers.useSingleCertificate(certNickname, KeyManagers
+                                        .useKeyStoreFile(keyStoreFileName, keyStorePassword
+                                                .toCharArray(), null))).setTrustManager(
+                                TrustManagers.trustAll()).getSSLContext();
+
+                final ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
+                        new ServerConnectionFactory<LDAPClientContext, Integer>() {
+
+                            @Override
+                            public ServerConnection<Integer> handleAccept(final LDAPClientContext clientContext)
+                                    throws LdapException {
+                                clientContext.enableTLS(sslContext, null, null, false, false);
+                                return connectionHandler.handleAccept(clientContext);
+                            }
+                        };
+
+                listener = new LDAPListener(localAddress, localPort, sslWrapper, options);
+            } else {
+                // No SSL.
+                listener = new LDAPListener(localAddress, localPort, connectionHandler, options);
+            }
+            System.out.println("Press any key to stop the server...");
+            System.in.read();
+        } catch (final Exception e) {
+            System.out.println("Error listening on " + localAddress + ":" + localPort);
+            e.printStackTrace();
+        } finally {
+            if (listener != null) {
+                listener.close();
+            }
+        }
+    }
+
+    private Server() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLife.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLife.java
new file mode 100644
index 0000000..095fb71
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLife.java
@@ -0,0 +1,163 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.io.IOException;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.TreeMapEntry;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/**
+ * A command-line client that creates, updates, renames, and deletes a
+ * short-lived entry in order to demonstrate LDAP write operations using the
+ * synchronous API. The client takes as arguments the host and port for the
+ * directory server, and expects to find the entries and access control
+ * instructions as defined in <a
+ * href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>.
+ *
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server</li>
+ * </ul>
+ * All arguments are required.
+ */
+public final class ShortLife {
+
+    /**
+     * Connect to directory server as a user with rights to add, modify, and
+     * delete entries, and then proceed to carry out the operations.
+     *
+     * @param args
+     *            The command line arguments: host, port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: host port");
+            System.err.println("For example: localhost 1389");
+            System.exit(1);
+        }
+        String host = args[0];
+        int port = Integer.parseInt(args[1]);
+
+        // User credentials of a "Directory Administrators" group member.
+        // Kirsten Vaughan is authorized to create, update, and delete
+        // entries.
+        //
+        // You might prompt an administrator user for information needed to
+        // authenticate, or your application might have its own account with
+        // rights to perform all necessary operations.
+        String adminDN = "uid=kvaughan,ou=people,dc=example,dc=com";
+        char[] adminPwd = "bribery".toCharArray();
+
+        // --- JCite add ---
+        // An entry to add to the directory
+        String entryDN = "cn=Bob,ou=People,dc=example,dc=com";
+        Entry entry = new LinkedHashMapEntry(entryDN)
+            .addAttribute("cn", "Bob")
+            .addAttribute("objectclass", "top")
+            .addAttribute("objectclass", "person")
+            .addAttribute("objectclass", "organizationalPerson")
+            .addAttribute("objectclass", "inetOrgPerson")
+            .addAttribute("mail", "subgenius@example.com")
+            .addAttribute("sn", "Dobbs");
+
+        LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+        try {
+            connection = factory.getConnection();
+            connection.bind(adminDN, adminPwd);
+
+            System.out.println("Creating an entry...");
+            writeToConsole(writer, entry);
+            connection.add(entry);
+            System.out.println("...done.");
+            // --- JCite add ---
+
+            // --- JCite modify ---
+            System.out.println("Updating mail address, adding description...");
+            Entry old = TreeMapEntry.deepCopyOfEntry(entry);
+            entry = entry.replaceAttribute("mail", "spammer@example.com")
+                    .addAttribute("description", "Good user gone bad");
+            writeToConsole(writer, entry);
+            ModifyRequest request = Entries.diffEntries(old, entry);
+            connection.modify(request);
+            System.out.println("...done.");
+            // --- JCite modify ---
+
+            // --- JCite rename ---
+            System.out.println("Renaming the entry...");
+            String newDN = "cn=Ted,ou=People,dc=example,dc=com";
+            entry = entry.setName(newDN);
+            writeToConsole(writer, entry);
+            connection.modifyDN(entryDN, "cn=Ted");
+            System.out.println("...done.");
+            // --- JCite rename ---
+
+            // --- JCite delete ---
+            System.out.println("Deleting the entry...");
+            writeToConsole(writer, entry);
+            connection.delete(newDN);
+            System.out.println("...done.");
+            // --- JCite delete ---
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+            try {
+                writer.close();
+            } catch (final IOException ignored) {
+                // Ignore.
+            }
+        }
+    }
+
+    /**
+     * Write the entry in LDIF form to System.out.
+     *
+     * @param entry
+     *            The entry to write to the console.
+     */
+    private static void writeToConsole(LDIFEntryWriter writer, Entry entry) throws IOException {
+        writer.writeEntry(entry);
+        writer.flush();
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private ShortLife() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLifeAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLifeAsync.java
new file mode 100644
index 0000000..9a6f257
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/ShortLifeAsync.java
@@ -0,0 +1,217 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.util.Utils.closeSilently;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.Entries;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.TreeMapEntry;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A command-line client that creates, updates, renames, and deletes a
+ * short-lived entry in order to demonstrate LDAP write operations
+ * using the asynchronous APIs.
+ * <br>
+ * The client takes as arguments the host and port for the directory server,
+ * and expects to find the entries and access control instructions as defined in
+ * <a href="http://opendj.forgerock.org/Example.ldif">Example.ldif</a>.
+ *
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server</li>
+ * </ul>
+ *
+ * All arguments are required.
+ */
+public final class ShortLifeAsync {
+    /** The short-lived entry. */
+    private static Entry entry;
+    /** Writer for displaying LDIF to System.out. */
+    private static LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Result for the operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Adds, modifies, renames, and deletes an entry.
+     *
+     * @param args
+     *            The command line arguments: host, port
+     */
+    public static void main(final String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: host port");
+            System.err.println("For example: localhost 1389");
+            System.exit(1);
+        }
+        final String host = args[0];
+        final int    port = Integer.parseInt(args[1]);
+
+        // User credentials of a "Directory Administrators" group member.
+        // Kirsten Vaughan is authorized to create, update, and delete entries.
+        //
+        // Alternatively, prompt an administrator user for credentials,
+        // or get the application its own account with access to update data.
+        final String adminDn  = "uid=kvaughan,ou=people,dc=example,dc=com";
+        final char[] adminPwd = "bribery".toCharArray();
+
+        // Prepare an entry to add to the directory.
+        final String entryDn = "cn=Bob,ou=People,dc=example,dc=com";
+        entry = new LinkedHashMapEntry(entryDn)
+            .addAttribute("cn", "Bob")
+            .addAttribute("objectclass", "top")
+            .addAttribute("objectclass", "person")
+            .addAttribute("objectclass", "organizationalPerson")
+            .addAttribute("objectclass", "inetOrgPerson")
+            .addAttribute("mail", "subgenius@example.com")
+            .addAttribute("sn", "Dobbs");
+
+        new LDAPConnectionFactory(host, port)
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        ShortLifeAsync.connection = connection;
+                        return connection.bindAsync(
+                                Requests.newSimpleBindRequest(adminDn, adminPwd));
+                    }
+                })
+                .thenAsync(new AsyncFunction<BindResult, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(BindResult bindResult)
+                            throws LdapException {
+                        log("Adding the entry...");
+                        log(entry);
+                        return connection.addAsync(Requests.newAddRequest(entry));
+                    }
+                })
+                .thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(Result result)
+                            throws LdapException {
+                        Entry old = TreeMapEntry.deepCopyOfEntry(entry);
+                        entry = entry
+                                .replaceAttribute("mail", "spammer@example.com")
+                                .addAttribute("description", "Good user gone bad");
+                        log("Updating mail address, adding description...");
+                        log(entry);
+                        ModifyRequest request = Entries.diffEntries(old, entry);
+                        return connection.modifyAsync(request);
+                    }
+                })
+                .thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(Result result)
+                            throws LdapException {
+                        entry = entry.setName("cn=Renamed,ou=People,dc=example,dc=com");
+                        log("Renaming the entry...");
+                        log(entry);
+                        return connection.modifyDNAsync(
+                                Requests.newModifyDNRequest(entryDn, "cn=Renamed"));
+                    }
+                })
+                .thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(Result result)
+                            throws LdapException {
+                        final String newDn = entryDn.replace("Bob", "Renamed");
+                        log("Deleting " + newDn + "...");
+                        return connection.deleteAsync(
+                                Requests.newDeleteRequest(newDn));
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        resultCode = result.getResultCode().intValue();
+                        log("... done.");
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        }  catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    /**
+     * Log a message to System.out.
+     *
+     * @param message   The message to write to the console.
+     */
+    private static void log(final String message) {
+        System.out.println(message);
+    }
+
+    /**
+     * Log an entry in LDIF form.
+     *
+     * @param entry     The entry to log.
+     */
+    private static void log(final Entry entry) {
+        try {
+            writer.writeEntry(entry);
+            writer.flush();
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        }
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private ShortLifeAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java
new file mode 100644
index 0000000..df8eb2b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java
@@ -0,0 +1,314 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.util.Options;
+
+/**
+ * An example client application which performs simple authentication to a
+ * directory server. This example takes the following command line parameters:
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server, e.g. 1389, 1636</li>
+ * <li>bind-dn - DN of the user to authenticate</li>
+ * <li>bind-password - Password of the user to authenticate</li>
+ * <li>use-starttls - (Optional) connect with StartTLS</li>
+ * <li>use-ssl - (Optional) connect over SSL</li>
+ * </ul>
+ * The host, port, bind-dn, and bind-password arguments are required.
+ * The use-starttls and use-ssl arguments are optional and mutually exclusive.
+ * <p>
+ * If the server certificate is self-signed,
+ * or otherwise not trusted out-of-the-box,
+ * then set the trust store by using the JSSE system property
+ * {@code -Djavax.net.ssl.trustStore=/path/to/opendj/config/keystore}
+ * and the trust store password if necessary by using the JSSE system property
+ * {@code -Djavax.net.ssl.trustStorePassword=`cat /path/to/opendj/config/keystore.pin`}.
+ */
+public final class SimpleAuth {
+
+    /**
+     * Authenticate to the directory either over LDAP, over LDAPS, or using
+     * StartTLS.
+     *
+     * @param args
+     *            The command line arguments
+     */
+    public static void main(final String[] args) {
+        parseArgs(args);
+        // Connect and bind to the server, then close the connection.
+        if (useStartTLS) {
+            connectStartTLS();
+        } else if (useSSL) {
+            connectSSL();
+        } else {
+            connect();
+        }
+    }
+
+    // --- JCite basic auth ---
+    /**
+     * Authenticate over LDAP.
+     */
+    private static void connect() {
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(bindDN, bindPassword.toCharArray());
+            System.out.println("Authenticated as " + bindDN + ".");
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+    // --- JCite basic auth ---
+
+    // --- JCite trust options ---
+    /**
+     * For StartTLS and SSL the connection factory needs SSL context options.
+     * In the general case, a trust manager in the SSL context serves
+     * to check server certificates, and a key manager handles client keys
+     * when the server checks certificates from our client.
+     * <p>
+     * This sample expects a directory server
+     * that allows use of Start TLS on the LDAP port.
+     * This sample checks the server certificate,
+     * verifying that the certificate is currently valid,
+     * and that the host name of the server matches that of the certificate,
+     * based on a Java Key Store-format trust store.
+     * This sample does not present a client certificate.
+     *
+     * @param hostname Host name expected in the server certificate
+     * @param truststore Path to trust store file for the trust manager
+     * @param storepass Password for the trust store
+     * @return SSL context options
+     * @throws GeneralSecurityException Could not load the trust store
+     */
+    private static Options getTrustOptions(final String hostname,
+                                           final String truststore,
+                                           final String storepass)
+            throws GeneralSecurityException {
+        Options options = Options.defaultOptions();
+
+        TrustManager trustManager = null;
+        try {
+            trustManager = TrustManagers.checkValidityDates(
+                    TrustManagers.checkHostName(hostname,
+                            TrustManagers.checkUsingTrustStore(
+                                    truststore, storepass.toCharArray(), null)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            System.exit(1);
+        }
+
+        if (trustManager != null) {
+            SSLContext sslContext = new SSLContextBuilder()
+                    .setTrustManager(trustManager).getSSLContext();
+            options.set(SSL_CONTEXT, sslContext);
+        }
+
+        options.set(SSL_USE_STARTTLS, useStartTLS);
+
+        return options;
+    }
+    // --- JCite trust options ---
+
+    // --- JCite secure connect ---
+    /**
+     * Perform authentication over a secure connection.
+     */
+    private static void secureConnect() {
+        Connection connection = null;
+
+        try {
+
+            final LDAPConnectionFactory factory =
+                    new LDAPConnectionFactory(host, port,
+                            getTrustOptions(host, keystore, storepass));
+            connection = factory.getConnection();
+            connection.bind(bindDN, bindPassword.toCharArray());
+
+            System.out.println("Authenticated as " + bindDN + ".");
+
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final GeneralSecurityException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+    // --- JCite secure connect ---
+
+    // --- JCite trust all ---
+    /**
+     * For StartTLS and SSL the connection factory needs SSL context options. In
+     * the general case, a trust manager in the SSL context serves to check
+     * server certificates, and a key manager handles client keys when the
+     * server checks certificates from our client.
+     *
+     * OpenDJ directory server lets you install by default with a self-signed
+     * certificate that is not in the system trust store. To simplify this
+     * implementation trusts all server certificates.
+     */
+    private static Options getTrustAllOptions() throws GeneralSecurityException {
+        Options options = Options.defaultOptions();
+        SSLContext sslContext =
+                new SSLContextBuilder().setTrustManager(TrustManagers.trustAll())
+                        .getSSLContext();
+        options.set(SSL_CONTEXT, sslContext);
+        options.set(SSL_USE_STARTTLS, useStartTLS);
+        return options;
+    }
+    // --- JCite trust all ---
+
+    // --- JCite trust all connect ---
+    /**
+     * Perform authentication over a secure connection, trusting all server
+     * certificates.
+     */
+    private static void trustAllConnect() {
+        Connection connection = null;
+
+        try {
+            final LDAPConnectionFactory factory =
+                    new LDAPConnectionFactory(host, port, getTrustAllOptions());
+            connection = factory.getConnection();
+            connection.bind(bindDN, bindPassword.toCharArray());
+            System.out.println("Authenticated as " + bindDN + ".");
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } catch (final GeneralSecurityException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+    // --- JCite trust all connect ---
+
+    /**
+     * Authenticate using StartTLS.
+     */
+    private static void connectStartTLS() {
+        secureConnect();
+        // trustAllConnect();
+    }
+
+    /**
+     * Authenticate over LDAPS.
+     */
+    private static void connectSSL() {
+        secureConnect();
+        // trustAllConnect();
+    }
+
+    private static String host;
+    private static int port;
+    private static String bindDN;
+    private static String bindPassword;
+    private static boolean useStartTLS;
+    private static boolean useSSL;
+    private static String keystore;
+    private static String storepass;
+
+    /**
+     * Parse command line arguments.
+     *
+     * @param args
+     *            host port bind-dn bind-password [ use-starttls | use-ssl ]
+     */
+    private static void parseArgs(String[] args) {
+        if (args.length < 4 || args.length > 5) {
+            giveUp();
+        }
+
+        host = args[0];
+        port = Integer.parseInt(args[1]);
+        bindDN = args[2];
+        bindPassword = args[3];
+
+        if (args.length == 5) {
+            if ("use-starttls".equals(args[4].toLowerCase())) {
+                useStartTLS = true;
+                useSSL = false;
+            } else if ("use-ssl".equals(args[4].toLowerCase())) {
+                useStartTLS = false;
+                useSSL = true;
+            } else {
+                giveUp();
+            }
+        }
+
+        keystore = System.getProperty("javax.net.ssl.trustStore");
+        storepass = System.getProperty("javax.net.ssl.trustStorePassword");
+        if (keystore == null) { // Try to use Java's cacerts trust store.
+            keystore = System.getProperty("java.home") + File.separator
+                    + "lib" + File.separator
+                    + "security" + File.separator
+                    + "cacerts";
+            storepass = "changeit"; // Default password
+        }
+    }
+
+    private static void giveUp() {
+        printUsage();
+        System.exit(1);
+    }
+
+    private static void printUsage() {
+        System.err.println("Usage: host port bind-dn bind-password [ use-starttls | use-ssl ]");
+        System.err.println("\thost, port, bind-dn, and bind-password arguments are required.");
+        System.err.println("\tuse-starttls and use-ssl are optional and mutually exclusive.");
+        System.err.println("\tOptionally set javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword.");
+    }
+
+    private SimpleAuth() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java
new file mode 100644
index 0000000..c9e7963
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java
@@ -0,0 +1,263 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.opendj.ldap.TrustManagers.checkHostName;
+import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Options;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.ResultHandler;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import java.io.File;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * An example client application which performs simple authentication to a
+ * directory server using the asynchronous APIs.
+ * <br>
+ * This example takes the following command line parameters:
+ * <ul>
+ * <li>host - host name of the directory server</li>
+ * <li>port - port number of the directory server</li>
+ * <li>bind-dn - DN of the user to authenticate</li>
+ * <li>bind-password - Password of the user to authenticate</li>
+ * <li>use-starttls - (Optional) connect with StartTLS</li>
+ * <li>use-ssl - (Optional) connect over SSL</li>
+ * </ul>
+ * The host, port, bind-dn, and bind-password arguments are required.
+ * The use-starttls and use-ssl arguments are optional and mutually exclusive.
+ * <br>
+ * If the server certificate is self-signed,
+ * or otherwise not trusted out-of-the-box,
+ * then set the trust store by using the JSSE system property
+ * {@code -Djavax.net.ssl.trustStore=/path/to/opendj/config/keystore}
+ * and the trust store password if necessary by using the JSSE system property
+ * {@code -Djavax.net.ssl.trustStorePassword=`cat /path/to/opendj/config/keystore.pin`}.
+ */
+public final class SimpleAuthAsync {
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Result for the modify operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Authenticate to the directory either over LDAP, over LDAPS, or using
+     * StartTLS.
+     *
+     * @param args
+     *            The command line arguments
+     */
+    public static void main(final String[] args) {
+        parseArgs(args);
+
+        // Connect and bind.
+        // Pass getTrustAllOptions() instead of getTrustOptions()
+        // to the connection factory constructor
+        // if you want to trust all certificates blindly.
+        new LDAPConnectionFactory(host, port, getTrustOptions(host, keystore, storepass))
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        SimpleAuthAsync.connection = connection;
+                        return connection.bindAsync(
+                                Requests.newSimpleBindRequest(bindDN, bindPassword.toCharArray()));
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        resultCode = result.getResultCode().intValue();
+                        System.out.println("Authenticated as " + bindDN + ".");
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        }  catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    /**
+     * For StartTLS and SSL the connection factory needs SSL context options.
+     * In the general case, a trust manager in the SSL context serves
+     * to check server certificates, and a key manager handles client keys
+     * when the server checks certificates from our client.
+     * <br>
+     * This sample checks the server certificate,
+     * verifying that the certificate is currently valid,
+     * and that the host name of the server matches that of the certificate,
+     * based on a Java Key Store-format trust store.
+     * This sample does not present a client certificate.
+     *
+     * @param hostname      Host name expected in the server certificate
+     * @param truststore    Path to trust store file for the trust manager
+     * @param storepass     Password for the trust store
+     * @return SSL context options if SSL or StartTLS is used.
+     */
+    private static Options getTrustOptions(final String hostname,
+                                           final String truststore,
+                                           final String storepass) {
+        Options options = Options.defaultOptions();
+        if (useSSL || useStartTLS) {
+            try {
+                TrustManager trustManager = TrustManagers.checkValidityDates(
+                        checkHostName(hostname,
+                                TrustManagers.checkUsingTrustStore(
+                                        truststore, storepass.toCharArray(), null)));
+                if (trustManager != null) {
+                    SSLContext sslContext = new SSLContextBuilder()
+                            .setTrustManager(trustManager).getSSLContext();
+                    options.set(SSL_CONTEXT, sslContext);
+                }
+                options.set(SSL_USE_STARTTLS, useStartTLS);
+            } catch (Exception e) {
+                System.err.println(e.getMessage());
+                System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+                return null;
+            }
+        }
+        return options;
+    }
+
+    /**
+     * For StartTLS and SSL the connection factory needs SSL context options. In
+     * the general case, a trust manager in the SSL context serves to check
+     * server certificates, and a key manager handles client keys when the
+     * server checks certificates from our client.
+     * <br>
+     * OpenDJ directory server lets you install by default with a self-signed
+     * certificate that is not in the system trust store. To simplify this
+     * implementation trusts all server certificates.
+     *
+     * @return SSL context options to trust all certificates without checking.
+     */
+    private static Options getTrustAllOptions() {
+        try {
+            Options options = Options.defaultOptions();
+            SSLContext sslContext =
+                    new SSLContextBuilder().setTrustManager(TrustManagers.trustAll())
+                            .getSSLContext();
+            options.set(SSL_CONTEXT, sslContext);
+            options.set(SSL_USE_STARTTLS, useStartTLS);
+            return options;
+        } catch (GeneralSecurityException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+            return null;
+        }
+    }
+
+    private static String  host;
+    private static int     port;
+    private static String  bindDN;
+    private static String  bindPassword;
+    private static boolean useStartTLS;
+    private static boolean useSSL;
+    private static String  keystore;
+    private static String  storepass;
+
+    /**
+     * Parse command line arguments.
+     *
+     * @param args
+     *            host port bind-dn bind-password [ use-starttls | use-ssl ]
+     */
+    private static void parseArgs(String[] args) {
+        if (args.length < 4 || args.length > 5) {
+            giveUp();
+        }
+
+        host = args[0];
+        port = Integer.parseInt(args[1]);
+        bindDN = args[2];
+        bindPassword = args[3];
+
+        if (args.length == 5) {
+            if ("use-starttls".equals(args[4].toLowerCase())) {
+                useStartTLS = true;
+                useSSL = false;
+            } else if ("use-ssl".equals(args[4].toLowerCase())) {
+                useStartTLS = false;
+                useSSL = true;
+            } else {
+                giveUp();
+            }
+        }
+
+        keystore = System.getProperty("javax.net.ssl.trustStore");
+        storepass = System.getProperty("javax.net.ssl.trustStorePassword");
+        if (keystore == null) { // Try to use Java's cacerts trust store.
+            keystore = System.getProperty("java.home") + File.separator
+                    + "lib" + File.separator
+                    + "security" + File.separator
+                    + "cacerts";
+            storepass = "changeit"; // Default password
+        }
+    }
+
+    private static void giveUp() {
+        printUsage();
+        System.exit(1);
+    }
+
+    private static void printUsage() {
+        System.err.println("Usage: host port bind-dn bind-password [ use-starttls | use-ssl ]");
+        System.err.println("\thost, port, bind-dn, and bind-password arguments are required.");
+        System.err.println("\tuse-starttls and use-ssl are optional and mutually exclusive.");
+        System.err.println("\tOptionally set javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword.");
+    }
+
+    private SimpleAuthAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroup.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroup.java
new file mode 100644
index 0000000..7ec596f
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroup.java
@@ -0,0 +1,178 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import java.util.Collection;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.RootDSE;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+
+/**
+ * This command-line client demonstrates adding and removing a member from a
+ * (potentially large) static group.
+ *
+ * The client takes as arguments the host and port of the directory server, the
+ * group DN, the member DN, and whether to "add" or "del" the specified member
+ * from the group. The client uses the Permissive Modify control if it is
+ * available to avoid having to check whether the member belongs to the group or
+ * not.
+ *
+ * This client expects a group that is a <code>groupOfNames</code> such as:
+ *
+ * <pre>
+ * dn: cn=My Static Group,ou=Groups,dc=example,dc=com
+ * cn: My Static Group
+ * objectClass: groupOfNames
+ * objectClass: top
+ * ou: Groups
+ * member: uid=ahunter,ou=People,dc=example,dc=com
+ * member: uid=bjensen,ou=People,dc=example,dc=com
+ * member: uid=tmorris,ou=People,dc=example,dc=com
+ * </pre>
+ *
+ * This client connects as <code>cn=Directory Manager</code> with password
+ * <code>password</code>. Not a best practice; in real code use application
+ * specific credentials to connect, and ensure that your application has access
+ * to use the Permissive Modify control if your directory server supports it.
+ */
+public final class UpdateGroup {
+
+    /**
+     * Connect to the directory server to update the group.
+     *
+     * @param args
+     *            The command line arguments: host, port, group-dn, member-dn,
+     *            {add|del}
+     */
+    public static void main(String[] args) {
+        if (args.length != 5) {
+            printUsage();
+        }
+        final String host = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String groupDN = args[2];
+        final String memberDN = args[3];
+        final ModificationType modType = getModificationType(args[4]);
+
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+        try {
+            connection = factory.getConnection();
+
+            Collection<String> controls =
+                    RootDSE.readRootDSE(connection).getSupportedControls();
+
+            final String user = "cn=Directory Manager";
+            final char[] password = "password".toCharArray();
+            connection.bind(user, password);
+
+            // --- JCite permissive ---
+            if (controls.contains(PermissiveModifyRequestControl.OID)) {
+
+                final ModifyRequest request = Requests.newModifyRequest(groupDN)
+                        .addControl(PermissiveModifyRequestControl.newControl(true))
+                        .addModification(modType, "member", memberDN);
+                connection.modify(request);
+
+            } else {
+                // --- JCite permissive ---
+
+                // --- JCite without permissive ---
+                System.out.println("Checking whether the entry with DN "
+                        + memberDN + " belongs to the group with DN " + groupDN
+                        + "...");
+                final CompareRequest request =
+                        Requests.newCompareRequest(groupDN, "member", memberDN);
+                CompareResult result = connection.compare(request);
+
+                if (modType == ModificationType.ADD
+                        && result.getResultCode() == ResultCode.COMPARE_FALSE) {
+                    System.out.println("Member does not yet belong to group."
+                            + " Adding it...");
+                    final ModifyRequest addMember =
+                            Requests.newModifyRequest(groupDN)
+                                .addModification(modType, "member", memberDN);
+                    connection.modify(addMember);
+                }
+
+                if (modType == ModificationType.DELETE
+                        && result.getResultCode() == ResultCode.COMPARE_TRUE) {
+                    System.out.println("Member belongs to group."
+                            + " Removing it...");
+                    final ModifyRequest delMember =
+                            Requests.newModifyRequest(groupDN)
+                                .addModification(modType, "member", memberDN);
+                    connection.modify(delMember);
+                }
+                // --- JCite without permissive ---
+            }
+
+            String op = (modType == ModificationType.ADD) ? "added to" : "deleted from";
+            System.out.println("The entry with DN " + memberDN + " has been "
+                    + op + " the group with DN " + groupDN + ".");
+
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+            return;
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+
+    /**
+     * Return the modification type for the update operation.
+     * @param operation Operation specified as an argument (add or del).
+     */
+    private static ModificationType getModificationType(String operation) {
+        final boolean isAdd = "add".equalsIgnoreCase(operation);
+        if (!isAdd && !"del".equalsIgnoreCase(operation)) {
+            printUsage();
+        }
+        return isAdd ? ModificationType.ADD : ModificationType.DELETE;
+    }
+
+    /**
+     * Print usage then exit.
+     */
+    private static void printUsage() {
+        System.err.println("Usage: host port group-dn member-dn {add|del}");
+        System.err.println("For example: localhost 1389 "
+                + "cn=Static,ou=Groups,dc=example,dc=com "
+                + "uid=user.5150,ou=People,dc=example,dc=com "
+                + "del");
+        System.exit(1);
+    }
+
+    /**
+     * Constructor not used.
+     */
+    private UpdateGroup() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroupAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroupAsync.java
new file mode 100644
index 0000000..d440d01
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UpdateGroupAsync.java
@@ -0,0 +1,218 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.util.Utils.closeSilently;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.RootDSE;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * This command-line client demonstrates adding and removing a member from a
+ * (potentially large) static group using the asynchronous APIs.
+ *
+ * The client takes as arguments the host and port of the directory server, the
+ * group DN, the member DN, and whether to "add" or "del" the specified member
+ * from the group. The client uses the Permissive Modify control if it is
+ * available to avoid having to check whether the member belongs to the group or
+ * not.
+ *
+ * This client expects a group that is a <code>groupOfNames</code> such as:
+ *
+ * <pre>
+ * dn: cn=My Static Group,ou=Groups,dc=example,dc=com
+ * cn: My Static Group
+ * objectClass: groupOfNames
+ * objectClass: top
+ * ou: Groups
+ * member: uid=ahunter,ou=People,dc=example,dc=com
+ * member: uid=bjensen,ou=People,dc=example,dc=com
+ * member: uid=tmorris,ou=People,dc=example,dc=com
+ * </pre>
+ *
+ * This client connects as <code>cn=Directory Manager</code> with password
+ * <code>password</code>. Not a best practice; in real code use application
+ * specific credentials to connect, and ensure that your application has access
+ * to use the Permissive Modify control if your directory server supports it.
+ */
+public final class UpdateGroupAsync {
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Result for the operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Updates the group as necessary.
+     *
+     * @param args
+     *            The command line arguments: host, port, group-dn, member-dn, (add|del)
+     */
+    public static void main(String[] args) {
+        if (args.length != 5) {
+            printUsage();
+        }
+        final String host              = args[0];
+        final int port                 = Integer.parseInt(args[1]);
+        final String groupDn           = args[2];
+        final String memberDn          = args[3];
+        final ModificationType modType = getModificationType(args[4]);
+
+        // Connect, bind, update group.
+        new LDAPConnectionFactory(host, port)
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        UpdateGroupAsync.connection = connection;
+                        return connection.bindAsync(
+                                Requests.newSimpleBindRequest("cn=Directory Manager", "password".toCharArray()));
+                    }
+                })
+                .thenAsync(new AsyncFunction<BindResult, RootDSE, LdapException>() {
+                    @Override
+                    public Promise<RootDSE, LdapException> apply(BindResult bindResult)
+                            throws LdapException {
+                        return RootDSE.readRootDSEAsync(connection);
+                    }
+                })
+                .thenAsync(new AsyncFunction<RootDSE, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(RootDSE rootDSE) throws LdapException {
+                        // If the directory supports the Permissive Modify request control,
+                        // then the modification type does not matter.
+                        if (rootDSE.getSupportedControls().contains(PermissiveModifyRequestControl.OID)) {
+                            log("Updating group membership.");
+                            return connection.modifyAsync(
+                                    Requests.newModifyRequest(groupDn)
+                                            .addControl(PermissiveModifyRequestControl.newControl(true))
+                                            .addModification(modType, "member", memberDn));
+                        } else {
+                            return connection
+                                    // Check whether the member is present.
+                                    .compareAsync(Requests.newCompareRequest(groupDn, "member", memberDn))
+                                    .thenAsync(new AsyncFunction<CompareResult, Result, LdapException>() {
+                                        @Override
+                                        public Promise<Result, LdapException> apply(CompareResult compareResult)
+                                                throws LdapException {
+                                            ResultCode rc = compareResult.getResultCode();
+                                            // Only add the member if missing from the group.
+                                            if (modType.equals(ModificationType.ADD)
+                                                    && rc.equals(ResultCode.COMPARE_FALSE)) {
+                                                log("Adding " + memberDn + " to " + groupDn + ".");
+                                                return connection.modifyAsync(
+                                                        Requests.newModifyRequest(groupDn)
+                                                                .addModification(modType, "member", memberDn));
+                                            // Only delete if present in the group.
+                                            } else if (modType.equals(ModificationType.DELETE)
+                                                    && rc.equals(ResultCode.COMPARE_TRUE)) {
+                                                log("Deleting " + memberDn + " from " + groupDn + ".");
+                                                return connection.modifyAsync(
+                                                        Requests.newModifyRequest(groupDn)
+                                                                .addModification(modType, "member", memberDn));
+                                            } else {
+                                                return Promises.newResultPromise(
+                                                        Responses.newResult(ResultCode.SUCCESS));
+                                            }
+                                        }
+                                    });
+                        }
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        final String op = (modType == ModificationType.ADD) ? "added to" : "deleted from";
+                        log(memberDn + " has been " + op + " the group " + groupDn + ".");
+                        resultCode = result.getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        } catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    /** Print usage then exit. */
+    private static void printUsage() {
+        System.err.println("Usage: host port group-dn member-dn {add|del}");
+        System.err.println("For example: localhost 1389 "
+                + "cn=Static,ou=Groups,dc=example,dc=com "
+                + "uid=user.5150,ou=People,dc=example,dc=com "
+                + "del");
+        System.exit(1);
+    }
+
+    /**
+     * Return the modification type for the update operation.
+     * @param operation Operation specified as an argument (add or del).
+     */
+    private static ModificationType getModificationType(String operation) {
+        final boolean isAdd = "add".equalsIgnoreCase(operation);
+        if (!isAdd && !"del".equalsIgnoreCase(operation)) {
+            printUsage();
+        }
+        return isAdd ? ModificationType.ADD : ModificationType.DELETE;
+    }
+
+    /**
+     * Log a message to System.out.
+     *
+     * @param message   The message to write to the console.
+     */
+    private static void log(final String message) {
+        System.out.println(message);
+    }
+
+    /** Constructor not used. */
+    private UpdateGroupAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseGenericControl.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseGenericControl.java
new file mode 100644
index 0000000..b1a26c2
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseGenericControl.java
@@ -0,0 +1,162 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import org.forgerock.opendj.io.ASN1;
+import org.forgerock.opendj.io.ASN1Writer;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+import java.io.IOException;
+
+/**
+ * An example client application which uses
+ * {@link org.forgerock.opendj.ldap.controls.GenericControl} to pass the
+ * pre-read request control from <a href="http://tools.ietf.org/html/rfc4527"
+ * >RFC 4527 - Lightweight Directory Access Protocol (LDAP) Read Entry Controls</a>.
+ *
+ * <br>This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <username> <password> <userDN>}
+ * </pre>
+ *
+ * <br>This example modifies the description attribute of an entry that
+ * you specify in the {@code <userDN>} command line parameter.
+ */
+public final class UseGenericControl {
+    /**
+     * Main method.
+     *
+     * @param args The command line arguments: host, port, username, password,
+     *             base DN, where the base DN is the root of a naming context.
+     */
+    public static void main(final String[] args) {
+        if (args.length < 5) {
+            System.err.println("Usage: host port username password userDN");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String hostName = args[0];
+        final int port = Integer.parseInt(args[1]);
+        final String userName = args[2];
+        final String password = args[3];
+        final String userDN = args[4];
+
+        // --- JCite ---
+        // Create an LDIF writer to write entries to stdout.
+        final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
+
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory =
+                new LDAPConnectionFactory(hostName, port);
+        Connection connection = null;
+
+        // Prepare the value for the GenericControl.
+
+        // http://tools.ietf.org/html/rfc4527#section-3.1 says:
+        // "The Pre-Read request control is a LDAP Control [RFC4511] whose
+        // controlType is 1.3.6.1.1.13.1 and whose controlValue is a BER-encoded
+        // AttributeSelection [RFC4511], as extended by [RFC3673]."
+
+        ByteStringBuilder builder = new ByteStringBuilder();
+        ASN1Writer asn1Writer = ASN1.getWriter(builder);
+        try {
+            asn1Writer.writeStartSequence();
+            asn1Writer.writeOctetString("description");
+            asn1Writer.writeEndSequence();
+            asn1Writer.flush();
+            asn1Writer.close();
+        } catch (Exception e) {
+            System.out.println("Failed to prepare control value: "
+                    + e.getCause());
+            System.exit(-1);
+        }
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(userName, password.toCharArray());
+
+            // Modify the user description.
+            final ModifyRequest request =
+                    Requests
+                            .newModifyRequest(userDN)
+                            .addModification(ModificationType.REPLACE,
+                                    "description", "A new description")
+                            .addControl(
+                                    GenericControl
+                                            .newControl(
+                                                    "1.3.6.1.1.13.1",
+                                                    true,
+                                                    builder.toByteString()));
+            final Result result = connection.modify(request);
+
+            // Display the description before and after the modification.
+            if (result.isSuccess()) {
+                final PreReadResponseControl control = result.getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()
+                );
+                final Entry unmodifiedEntry = control.getEntry();
+                writer.writeComment("Before modification");
+                writer.writeEntry(unmodifiedEntry);
+                writer.flush();
+
+                final SearchResultEntry modifiedEntry =
+                        connection.searchSingleEntry(
+                                userDN,
+                                SearchScope.BASE_OBJECT,
+                                "(objectclass=*)",
+                                "description");
+                writer.writeComment("After modification");
+                writer.writeEntry(modifiedEntry);
+                writer.flush();
+            }
+
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private UseGenericControl() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchema.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchema.java
new file mode 100644
index 0000000..40d4052
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchema.java
@@ -0,0 +1,140 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * This example command-line client application validates an entry
+ * against the directory server schema before adding it.
+ *
+ * <br>
+ *
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <bindDN> <bindPassword>}
+ * </pre>
+ *
+ * Then it reads an entry to add from System.in.
+ * If the entry is valid according to the directory schema,
+ * it tries to add the entry to the directory.
+ */
+public final class UseSchema {
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, bindDN, bindPassword.
+     */
+    public static void main(final String[] args) {
+        if (args.length != 4) {
+            System.err.println("Usage: host port bindDN bindPassword");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String host         = args[0];
+        final int    port         = Integer.parseInt(args[1]);
+        final String bindDn       = args[2];
+        final String bindPassword = args[3];
+
+        // --- JCite ---
+        // Connect and bind to the server.
+        final LDAPConnectionFactory factory = new LDAPConnectionFactory(host, port);
+        Connection connection = null;
+
+        try {
+            connection = factory.getConnection();
+            connection.bind(bindDn, bindPassword.toCharArray());
+
+            // Read the schema from the directory server.
+            // If that fails, use the default schema from the LDAP SDK.
+            Schema schema = null;
+            try {
+                schema = Schema.readSchema(connection, DN.valueOf("cn=schema"));
+            } catch (EntryNotFoundException e) {
+                System.err.println(e.getMessage());
+                schema = Schema.getDefaultSchema();
+            } finally {
+                if (schema == null) {
+                    System.err.println("Failed to get schema.");
+                    System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+                }
+            }
+
+            // Read an entry from System.in.
+            final LDIFEntryReader reader = new LDIFEntryReader(System.in);
+            final Entry entry = reader.readEntry();
+
+            // If the entry is valid, try to add it. Otherwise display errors.
+            final List<LocalizableMessage> schemaErrors = new LinkedList<>();
+            boolean conformsToSchema = schema.validateEntry(
+                    entry, SchemaValidationPolicy.defaultPolicy(), schemaErrors);
+            final String entryDn = entry.getName().toString();
+            Result result = null;
+            if (conformsToSchema) {
+                System.out.println("Processing ADD request for " + entryDn);
+                result = connection.add(entry);
+            } else {
+                for (LocalizableMessage error : schemaErrors) {
+                    System.err.println(error);
+                }
+                System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            }
+
+            // Display the result. (A failed add results in an LdapException.)
+            if (result != null) {
+                System.out.println("ADD operation successful for DN " + entryDn);
+            }
+        } catch (final LdapException e) {
+            System.err.println(e.getMessage());
+            System.exit(e.getResult().getResultCode().intValue());
+        } catch (DecodeException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_DECODING_ERROR.intValue());
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // --- JCite ---
+    }
+
+    private UseSchema() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchemaAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchemaAsync.java
new file mode 100644
index 0000000..c175ba7
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/UseSchemaAsync.java
@@ -0,0 +1,173 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.examples;
+
+import static org.forgerock.util.Utils.closeSilently;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * This example command-line client application validates an entry
+ * against the directory server schema before adding it
+ * using the asynchronous APIs.
+ *
+ * <br>
+ *
+ * This example takes the following command line parameters:
+ *
+ * <pre>
+ *  {@code <host> <port> <bindDN> <bindPassword>}
+ * </pre>
+ *
+ * Then it reads an entry to add from System.in.
+ * If the entry is valid according to the directory schema,
+ * it tries to add the entry to the directory.
+ */
+public final class UseSchemaAsync {
+    /** Connection to the LDAP server. */
+    private static Connection connection;
+    /** Result for the operation. */
+    private static int resultCode;
+    /** Count down latch to wait for modify operation to complete. */
+    private static final CountDownLatch COMPLETION_LATCH = new CountDownLatch(1);
+
+    /**
+     * Main method.
+     *
+     * @param args
+     *            The command line arguments: host, port, bindDN, bindPassword.
+     */
+    public static void main(final String[] args) {
+        if (args.length != 4) {
+            System.err.println("Usage: host port bindDN bindPassword");
+            System.exit(1);
+        }
+
+        // Parse command line arguments.
+        final String host         = args[0];
+        final int    port         = Integer.parseInt(args[1]);
+        final String bindDn       = args[2];
+        final char[] bindPassword = args[3].toCharArray();
+
+        // Read an entry from System.in.
+        final Entry entry;
+        try {
+            System.out.println("Enter entry to add in LDIF format:");
+            entry = new LDIFEntryReader(System.in).readEntry();
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
+            return;
+        }
+        final String entryDn = entry.getName().toString();
+
+        // Connect, bind, read schema, and add entry if valid according to schema.
+        new LDAPConnectionFactory(host, port)
+                .getConnectionAsync()
+                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                    @Override
+                    public Promise<BindResult, LdapException> apply(Connection connection)
+                            throws LdapException {
+                        UseSchemaAsync.connection = connection;
+                        return connection.bindAsync(
+                                Requests.newSimpleBindRequest(bindDn, bindPassword));
+                    }
+                })
+                .thenAsync(new AsyncFunction<BindResult, Schema, LdapException>() {
+                    @Override
+                    public Promise<Schema, LdapException> apply(BindResult bindResult)
+                            throws LdapException {
+                        return Schema.readSchemaForEntryAsync(connection, DN.rootDN());
+                    }
+                })
+                .thenAsync(new AsyncFunction<Schema, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(Schema schema)
+                            throws LdapException {
+                        final List<LocalizableMessage> schemaErrors = new LinkedList<>();
+                        boolean isValid = schema.validateEntry(
+                                entry,
+                                SchemaValidationPolicy.defaultPolicy(),
+                                schemaErrors);
+                        if (isValid) {
+                            System.out.println("Processing ADD request for " + entryDn);
+                            return connection.addAsync(Requests.newAddRequest(entry));
+                        } else {
+                            for (LocalizableMessage error : schemaErrors) {
+                                System.err.println(error);
+                            }
+                            return Promises.newExceptionPromise(
+                                    LdapException.newLdapException(
+                                            ResultCode.CLIENT_SIDE_PARAM_ERROR,
+                                            "Entry does not conform to schema."));
+                        }
+                    }
+                })
+                .thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        System.out.println("ADD operation successful for DN " + entryDn);
+                        resultCode = result.getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                })
+                .thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException e) {
+                        System.err.println(e.getMessage());
+                        resultCode = e.getResult().getResultCode().intValue();
+                        COMPLETION_LATCH.countDown();
+                    }
+                });
+
+        try {
+            COMPLETION_LATCH.await();
+        }  catch (InterruptedException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_USER_CANCELLED.intValue());
+            return;
+        }
+
+        closeSilently(connection);
+        System.exit(resultCode);
+    }
+
+    private UseSchemaAsync() {
+        // Not used.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/package-info.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/package-info.java
new file mode 100644
index 0000000..d0ae1e1
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012 ForgeRock AS.
+ */
+
+/**
+ * This package includes examples illustrating various use cases of the
+ * OpenDJ LDAP SDK.
+ */
+package org.forgerock.opendj.examples;
+
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/javadoc/overview.html b/opendj-sdk/opendj-ldap-sdk-examples/src/main/javadoc/overview.html
new file mode 100644
index 0000000..c26df28
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/javadoc/overview.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+    The OpenDJ SDK Examples contains various example applications illustrating
+    usage of the OpenDJ LDAP SDK.
+</body>
+</html>
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/site/xdoc/index.xml.vm b/opendj-sdk/opendj-ldap-sdk-examples/src/site/xdoc/index.xml.vm
new file mode 100644
index 0000000..bdda1a7
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/site/xdoc/index.xml.vm
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2015 ForgeRock AS.
+-->
+<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+  <properties>
+    <title>About ${project.name}</title>
+    <author email="opendj-dev@forgerock.org">${project.organization.name}</author>
+  </properties>
+  <body>
+    <section name="About ${project.name}">
+      <p>This module contains example LDAP applications implemented using the
+        OpenDJ LDAP SDK.</p>
+
+      <p>
+        The following examples use the synchronous APIs:
+      </p>
+
+      <ul>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/Search.html">LDAP search</a>
+          - illustrates how to perform an LDAP search operation using the
+          synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/Modify.html">LDAP modify</a>
+          - illustrates how to perform an LDAP modify operation using the
+          synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/Server.html">LDAP server</a>
+          - illustrates how to implement a very simple LDAP server
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SimpleAuth.html">LDAP bind</a>
+          - illustrates how to bind to an LDAP server using the synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SASLAuth.html">LDAP SASL bind</a>
+          - illustrates how to implement a SASL PLAIN bind to an LDAP server
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ParseAttributes.html">Parse
+          attributes</a> - illustrates how to get an entry's attribute values as objects
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ReadSchema.html">Read LDAP schema</a>
+          - illustrates how to read and verify an LDAP server's schema
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/GetInfo.html">Read Root DSE</a>
+          - illustrates how to read an LDAP server's capabilities and schema
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SearchBind.html">Search &amp; bind</a>
+          - illustrates how to authenticate given a mail address and a password
+          using the synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ShortLife.html">Short life</a>
+          - illustrates how to create, update, rename, and delete an entry
+          using the synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/UseSchema.html">Use LDAP Schema</a>
+          - illustrates how to validate an entry using the directory server LDAP schema
+          using the synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/Controls.html">Use LDAP Controls</a>
+          - illustrates how to use supported LDAP controls
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ExtendedOperations.html">Use
+          LDAP Extended Operations</a> - illustrates how to use supported LDAP extended
+          operations
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/UpdateGroup.html">Update group</a>
+          - illustrates how to add or remove a member from a static group
+          using the synchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/UseGenericControl.html">Use <code>GenericControl</code></a>
+          - illustrates how to use <code>GenericControl</code> to add a pre-read request control
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/GetADChangeNotifications.html">Get AD Change Notifications</a>
+          - illustrates how to use <code>GetADChangeNotifications</code> to get change notifications from Active Directory
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/PasswordResetForAD.html">Reset AD user password</a>
+          - illustrates how to reset a user password in Active Directory as Administrator,
+            or change the password as the user
+        </li>
+      </ul>
+
+      <p>
+        The following examples use the asynchronous APIs:
+      </p>
+
+      <ul>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SearchAsync.html">LDAP search (async)</a>
+          - illustrates how to perform an LDAP search operation using the
+          asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ModifyAsync.html">LDAP modify (async)</a>
+          - illustrates how to perform an LDAP modify operation using the
+          asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/Proxy.html">LDAP proxy</a>
+          - illustrates how to implement a very simple LDAP proxy
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SimpleAuthAsync.html">LDAP bind (async)</a>
+          - illustrates how to bind to an LDAP server using the asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/SearchBindAsync.html">Search &amp; bind (async)</a>
+          - illustrates how to authenticate given a mail address and a password
+          using the asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/ShortLifeAsync.html">Short life (async)</a>
+          - illustrates how to create, update, rename, and delete an entry
+          using the asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/UseSchemaAsync.html">Use LDAP Schema (async)</a>
+          - illustrates how to validate an entry using the directory server LDAP schema
+          using the asynchronous APIs
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/RewriterProxy.html">Rewrite proxy</a>
+          - illustrates how to rewrite DNs and attribute names in a proxy layer
+        </li>
+        <li>
+          <a href="xref/org/forgerock/opendj/examples/UpdateGroupAsync.html">Update group (async)</a>
+          - illustrates how to add or remove a member from a static group
+          using the asynchronous APIs
+        </li>
+      </ul>
+    </section>
+    <section name="Documentation for ${project.name}">
+      <p>
+        Javadoc for this module can be found <a href="apidocs/index.html">here</a>.
+      </p>
+    </section>
+    <section name="Get ${project.name}">
+      <p>
+        You can get ${project.name} using any of the following methods:
+      </p>
+      <subsection name="Download">
+        <p>
+          Pre-built binaries can be downloaded directly from the ForgeRock Maven
+          repository:
+        </p>
+        <ul>
+          <li><a href="${mavenRepoReleases}/org/forgerock/opendj/${project.artifactId}">Stable releases</a></li>
+          <li><a href="${mavenRepoSnapshots}/org/forgerock/opendj/${project.artifactId}/${project.version}">Latest development snapshot</a></li>
+        </ul>
+      </subsection>
+      <subsection name="Build">
+        <p>
+          For the DIY enthusiasts you can build it yourself by checking out the
+          latest code using <a href="source-repository.html">Subversion</a> and
+          building it with Maven 3.
+        </p>
+      </subsection>
+    </section>
+  </body>
+</document>
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/test/bin/checkRewriterProxy.sh b/opendj-sdk/opendj-ldap-sdk-examples/src/test/bin/checkRewriterProxy.sh
new file mode 100644
index 0000000..2ba2715
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/test/bin/checkRewriterProxy.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+
+# Smoke test RewriterProxy.java using OpenDJ tools.
+# Depends on http://opendj.forgerock.org/Example.ldif being in OpenDJ.
+
+OPENDJ_TOOLS_DIR="/path/to/OpenDJ/bin"  # ldapcompare, ldapdelete, ldapmodify, ldapsearch
+HOST=localhost                          # Host where proxy listens
+PORT=8389                               # Port where proxy listens
+
+BINDDN="uid=kvaughan,ou=People,dc=example,dc=com"
+BINDPWD=bribery
+
+CURRDIR=`pwd`
+
+if [ -e $OPENDJ_TOOLS_DIR ]
+then
+	cd $OPENDJ_TOOLS_DIR
+else
+	exit 1
+fi
+
+#set -x
+
+echo Deleting uid=fdupont,ou=People,o=example...
+./ldapdelete -h $HOST -p $PORT -D $BINDDN -w $BINDPWD uid=fdupont,ou=People,o=example
+echo
+
+add() {
+  echo Adding uid=fdupont,ou=People,o=example...
+  ./ldapmodify -h $HOST -p $PORT -D $BINDDN -w $BINDPWD -a <<EOF
+
+dn: uid=fdupont,ou=People,o=example
+uid: fdupont
+fullname: Frederique Dupont
+fullname;lang-fr: Fredérique Dupont
+givenName: Fredérique
+sn: Dupont
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: posixAccount
+objectClass: top
+ou: People
+ou: Product Development
+telephoneNumber: +33 1 23 45 67 89
+facsimileTelephoneNumber: +33 1 23 45 67 88
+mail: fdupont@example.fr
+roomNumber: 0042
+l: Paris
+gidNumber: 1000
+uidNumber: 1110
+homeDirectory: /home/fdupont
+userPassword: password
+
+EOF
+  echo
+}
+
+add
+
+echo Looking for fullname=Frederique Dupont...
+./ldapsearch -h $HOST -p $PORT -D $BINDDN -w $BINDPWD -b o=example "(fullname=Frederique Dupont)" fullname
+echo
+
+echo Comparing fullname:Frederique Dupont...
+./ldapcompare -h $HOST -p $PORT -D $BINDDN -w $BINDPWD "fullname:Frederique Dupont" uid=fdupont,ou=People,o=example
+echo
+
+echo Changing fullname...
+./ldapmodify -h $HOST -p $PORT -D $BINDDN -w $BINDPWD <<EOM
+
+dn: uid=fdupont,ou=People,o=example
+changetype: modify
+replace: fullname
+fullname: Fred Dupont
+
+EOM
+echo
+
+echo Changing uid=fdupont to uid=qdupont...
+./ldapmodify -h $HOST -p $PORT -D $BINDDN -w $BINDPWD <<EOR
+
+dn: uid=fdupont,ou=People,o=example
+changetype: modrdn
+newrdn: uid=qdupont
+deleteoldrdn: 1
+
+EOR
+echo
+
+echo Deleting uid=qdupont,ou=People,o=example
+./ldapdelete -h $HOST -p $PORT -D $BINDDN -w $BINDPWD uid=qdupont,ou=People,o=example
+echo
+
+add
+
+cd $CURRDIR
+
+echo Done.
+exit 0
diff --git a/opendj-sdk/opendj-ldap-toolkit/README b/opendj-sdk/opendj-ldap-toolkit/README
new file mode 100644
index 0000000..97dd3ce
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/README
@@ -0,0 +1,11 @@
+OpenDJ LDAP client tools and SDK.
+
+This Maven project contains the OpenDJ client tools, and LDAP SDK. It is 100%
+Java based and requires Java 1.7.
+
+Complete documentation for this product is available online
+from http://www.forgerock.com/
+
+This product is made available under the Common Development and Distribution
+License (CDDL).  The complete text for this license, and for alternate licenses
+of included components, may be found in the legal-notices directory.
diff --git a/opendj-sdk/opendj-ldap-toolkit/legal-notices/THIRDPARTYREADME.txt b/opendj-sdk/opendj-ldap-toolkit/legal-notices/THIRDPARTYREADME.txt
new file mode 100644
index 0000000..d136bab
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/legal-notices/THIRDPARTYREADME.txt
@@ -0,0 +1,338 @@
+DO NOT TRANSLATE OR LOCALIZE
+
+***************************************************************************
+COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
+***************************************************************************
+
+Version: forgerock-util.jar (3.0.2)
+Copyright: Copyright 2011-2015 ForgeRock AS.
+           Copyright (c) 2010-2011 ApexIdentity Inc. All rights reserved.
+           Portions Copyright 2011-2015 ForgeRock AS.
+
+Version: grizzly-framework.jar (2.3.23)
+Copyright: Copyright (c) 2007-2015 Oracle and/or its affiliates. All rights reserved.
+
+Version: i18n-core.jar (1.4.2)
+Copyright: Copyright 2011 ForgeRock AS
+           Copyright 2007-2009 Sun Microsystems, Inc.
+
+Version: i18n-slf4j.jar (1.4.2)
+Copyright: Copyright 2011-2014 ForgeRock AS
+
+Version: opendj-cli.jar (3.0.0)
+Copyright: Copyright 2014-2015 ForgeRock AS.
+           Copyright 2006-2010 Sun Microsystems, Inc.
+           Portions Copyright 2011-2015 ForgeRock AS.
+           Portions Copyright 2009 Parametric Technology Corporation (PTC)
+
+Version: opendj-core.jar (3.0.0)
+Copyright: Copyright 2011-2015 ForgeRock AS.
+           Copyright 2006-2011 Sun Microsystems, Inc.
+           Copyright 2013-2014 Manuel Gaupp
+           Portions Copyright 2011-2015 ForgeRock AS.
+           Portions Copyright 2012-2014 Manuel Gaupp
+
+Version: opendj-grizzly.jar (3.0.0)
+Copyright: Copyright 2013-2015 ForgeRock AS.
+           Copyright 2009-2010 Sun Microsystems, Inc.
+           Portions Copyright 2011-2015 ForgeRock AS.
+
+Version: opendj-ldap-toolkit.jar (3.0.0)
+Copyright: Copyright 2011-2016 ForgeRock AS.
+           Copyright 2006-2010 Sun Microsystems, Inc.
+           Portions Copyright 2011-2015 ForgeRock AS.
+==================
+Full license text:
+==================
+
+COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 (CDDL-1.0)
+
+1. Definitions.
+
+1.1. Contributor means each individual or entity that creates or contributes to the creation of Modifications.
+
+1.2. Contributor Version means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.
+
+1.3. Covered Software means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.
+
+1.4. Executable means the Covered Software in any form other than Source Code.
+
+1.5. Initial Developer means the individual or entity that first makes Original Software available under this License.
+
+1.6. Larger Work means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.
+
+1.7. License means this document.
+
+1.8. Licensable means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.
+
+1.9. Modifications means the Source Code and Executable form of any of the following:
+
+A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;
+
+B. Any new file that contains any part of the Original Software or previous Modification; or
+
+C. Any new file that is contributed or otherwise made available under the terms of this License.
+
+1.10. Original Software means the Source Code and Executable form of computer software code that is originally released under this License.
+
+1.11. Patent Claims means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.
+
+1.12. Source Code means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.
+
+1.13. You (or Your) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, You includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, control means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
+
+2. License Grants.
+
+2.1. The Initial Developer Grant.
+
+Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and
+
+(b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).
+
+(c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.
+
+(d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.
+
+2.2. Contributor Grant.
+
+Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and
+
+(b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).
+
+(c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.
+
+(d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.
+
+3. Distribution Obligations.
+
+3.1. Availability of Source Code.
+
+Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.
+
+3.2. Modifications.
+
+The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.
+
+3.3. Required Notices.
+
+You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.
+
+3.4. Application of Additional Terms.
+
+You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
+
+3.5. Distribution of Executable Versions.
+
+You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipients rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
+
+3.6. Larger Works.
+
+You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.
+
+4. Versions of the License.
+
+4.1. New Versions.
+
+Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.
+
+4.2. Effect of New Versions.
+
+You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.
+
+4.3. Modified Versions.
+
+When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.
+
+5. DISCLAIMER OF WARRANTY.
+
+COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+6. TERMINATION.
+
+6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
+
+6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as Participant) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.
+
+6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.
+
+7. LIMITATION OF LIABILITY.
+
+UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTYS NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+8. U.S. GOVERNMENT END USERS.
+
+The Covered Software is a commercial item, as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of commercial computer software (as that term is defined at 48 C.F.R.  252.227-7014(a)(1)) and commercial computer software documentation as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.
+
+9. MISCELLANEOUS.
+
+This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdictions conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.
+
+10. RESPONSIBILITY FOR CLAIMS.
+
+As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
+
+
+***************************************************************************
+Apache Software License, Version 2.0
+***************************************************************************
+
+Version: jsr305.jar (3.0.0)
+Copyright: Copyright (c) 2005 Brian Goetz
+
+Version: metrics-core.jar (3.1.2)
+Copyright: Copyright (c) 2010-2014 Coda Hale, Yammer.com
+
+Version: hdrhistogram-metrics-reservoir.jar (1.1.0)
+Copyright: Copyright (c) 2014-2015 Marshall Pierce
+
+==================
+Full license text:
+==================
+
+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:
+
+You must give any other recipients of the Work or Derivative Works a copy of this License; and
+You must cause any modified files to carry prominent notices stating that You changed the files; and
+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
+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.
+
+
+***************************************************************************
+The MIT License (MIT)
+***************************************************************************
+
+Version: slf4j-api.jar (1.7.12)
+Copyright: Copyright (c) 2004-2011 QOS.ch. All rights reserved.
+
+Version: slf4j-jdk14.jar (1.7.12)
+Copyright: Copyright (c) 2004-2011 QOS.ch. All rights reserved.
+
+==================
+Full license text:
+==================
+
+The MIT License (MIT)
+
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+***************************************************************************
+The BSD 2-Clause License
+***************************************************************************
+
+Version: HdrHistogram.jar (2.1.4)
+Copyright (c) 2012, 2013, 2014 Gil Tene
+Copyright (c) 2014 Michael Barker
+Copyright (c) 2014 Matt Warren
+
+==================
+Full license text:
+==================
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+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.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS 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 THE COPYRIGHT HOLDER OR 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.
\ No newline at end of file
diff --git a/opendj-sdk/opendj-ldap-toolkit/pom.xml b/opendj-sdk/opendj-ldap-toolkit/pom.xml
new file mode 100644
index 0000000..b364e21
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/pom.xml
@@ -0,0 +1,327 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2016 ForgeRock AS.
+ -->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-ldap-toolkit</artifactId>
+    <name>OpenDJ SDK Toolkit</name>
+    <description>This module includes LDAP command line tools based on the OpenDJ LDAP SDK.</description>
+
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mpierce.metrics.reservoir</groupId>
+            <artifactId>hdrhistogram-metrics-reservoir</artifactId>
+            <version>1.1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-grizzly</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>i18n-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-cli</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock</groupId>
+            <artifactId>forgerock-build-tools</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>generate-messages</goal>
+                        </goals>
+                        <configuration>
+                            <messageFiles>
+                                <messageFile>com/forgerock/opendj/ldap/tools/tools.properties</messageFile>
+                            </messageFiles>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addClasspath>true</addClasspath>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <appendAssemblyId>false</appendAssemblyId>
+                            <descriptors>
+                                <descriptor>src/main/assembly/descriptor.xml</descriptor>
+                            </descriptors>
+                            <formats>
+                                <format>zip</format>
+                            </formats>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <profiles>
+        <profile>
+            <id>docs</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.forgerock.opendj</groupId>
+                        <artifactId>opendj-doc-maven-plugin</artifactId>
+                        <version>${project.version}</version>
+                        <executions>
+                            <execution>
+                                <id>generate-doc</id>
+                                <goals>
+                                    <goal>generate-refentry</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDir>${project.build.directory}/generated-man-pages</outputDir>
+                                    <tools>
+                                        <tool>
+                                            <name>addrate</name>
+                                            <application>com.forgerock.opendj.ldap.tools.AddRate</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-80-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>addrate-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>authrate</name>
+                                            <application>com.forgerock.opendj.ldap.tools.AuthRate</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>authrate-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldapcompare</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDAPCompare</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-ldap-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>files.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldapcompare-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldapmodify</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDAPModify</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-ldap-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>files.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldapmodify-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldappasswordmodify</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDAPPasswordModify
+                                            </application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-ldap-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>files.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldappasswordmodify-examples.xml
+                                                </trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldapsearch</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDAPSearch</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>filters.xml</trailingSectionPath>
+                                                <trailingSectionPath>attributes.xml</trailingSectionPath>
+                                                <trailingSectionPath>exit-codes-0-ldap-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>files.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldapsearch-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldifdiff</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDIFDiff</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-5-6-other.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldifdiff-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldifmodify</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDIFModify</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-gt0.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldifmodify-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>ldifsearch</name>
+                                            <application>com.forgerock.opendj.ldap.tools.LDIFSearch</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-gt0.xml</trailingSectionPath>
+                                                <trailingSectionPath>ldifsearch-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>makeldif</name>
+                                            <application>com.forgerock.opendj.ldap.tools.MakeLDIF</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-1.xml</trailingSectionPath>
+                                                <trailingSectionPath>makeldif-examples.xml</trailingSectionPath>
+                                                <trailingSectionPath>makeldif-see-also.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>modrate</name>
+                                            <application>com.forgerock.opendj.ldap.tools.ModRate</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>modrate-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+
+                                        <tool>
+                                            <name>searchrate</name>
+                                            <application>com.forgerock.opendj.ldap.tools.SearchRate</application>
+                                            <trailingSectionPaths>
+                                                <trailingSectionPath>exit-codes-0-89.xml</trailingSectionPath>
+                                                <trailingSectionPath>searchrate-examples.xml</trailingSectionPath>
+                                            </trailingSectionPaths>
+                                        </tool>
+                                    </tools>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>man-pages</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <configuration>
+                                    <descriptors>
+                                        <descriptor>src/main/assembly/man-pages.xml</descriptor>
+                                    </descriptors>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate.bat
new file mode 100644
index 0000000..66be9fd
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2014 ForgeRock AS.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AddRate"
+set SCRIPT_NAME=addrate
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/authrate.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/authrate.bat
new file mode 100644
index 0000000..ee463c3
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/authrate.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2010 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AuthRate"
+set SCRIPT_NAME=authrate
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapcompare.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapcompare.bat
new file mode 100644
index 0000000..96083bd
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapcompare.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2008 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPCompare"
+set SCRIPT_NAME=ldapcompare
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapmodify.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapmodify.bat
new file mode 100644
index 0000000..e69a74b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapmodify.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2008 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPModify"
+set SCRIPT_NAME=ldapmodify
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldappasswordmodify.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldappasswordmodify.bat
new file mode 100644
index 0000000..6fd4ec8
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldappasswordmodify.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2008 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPPasswordModify"
+set SCRIPT_NAME=ldappasswordmodify
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapsearch.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapsearch.bat
new file mode 100644
index 0000000..b91e203
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldapsearch.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2008 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPSearch"
+set SCRIPT_NAME=ldapsearch
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifdiff.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifdiff.bat
new file mode 100644
index 0000000..57d155a
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifdiff.bat
@@ -0,0 +1,21 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2012 ForgeRock AS.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFDiff"
+set SCRIPT_NAME=ldifdiff
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifmodify.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifmodify.bat
new file mode 100644
index 0000000..a2642ca
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifmodify.bat
@@ -0,0 +1,21 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2012 ForgeRock AS.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFModify"
+set SCRIPT_NAME=ldifmodify
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifsearch.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifsearch.bat
new file mode 100644
index 0000000..0f1c440
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/ldifsearch.bat
@@ -0,0 +1,21 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2012 ForgeRock AS.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFSearch"
+set SCRIPT_NAME=ldifsearch
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/makeldif.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/makeldif.bat
new file mode 100644
index 0000000..fe7a50d
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/makeldif.bat
@@ -0,0 +1,21 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2013 ForgeRock AS.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.MakeLDIF"
+set SCRIPT_NAME=makeldif
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/modrate.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/modrate.bat
new file mode 100644
index 0000000..d2aad8a
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/modrate.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2009 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.ModRate"
+set SCRIPT_NAME=modrate
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/searchrate.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/searchrate.bat
new file mode 100644
index 0000000..9bcd1ac
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/searchrate.bat
@@ -0,0 +1,22 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2009 Sun Microsystems, Inc.
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.SearchRate"
+set SCRIPT_NAME=searchrate
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate
new file mode 100644
index 0000000..d435cd6
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2014-2015 ForgeRock AS.
+
+
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AddRate"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="addrate"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/authrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/authrate
new file mode 100644
index 0000000..b2552b5
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/authrate
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2010 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to measure auth performance.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AuthRate"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="authrate"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapcompare b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapcompare
new file mode 100644
index 0000000..7c28568
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapcompare
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2006-2008 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP compare operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPCompare"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldapcompare"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapmodify b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapmodify
new file mode 100644
index 0000000..063d637
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapmodify
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2006-2008 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP add, delete, modify, and modify DN
+# operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPModify"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldapmodify"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldappasswordmodify b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldappasswordmodify
new file mode 100644
index 0000000..ac8cbee
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldappasswordmodify
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2006-2008 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP password modify operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPPasswordModify"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldappasswordmodify"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapsearch b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapsearch
new file mode 100644
index 0000000..1e221c3
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldapsearch
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2006-2008 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP search operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDAPSearch"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldapsearch"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifdiff b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifdiff
new file mode 100644
index 0000000..ede3ee6
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifdiff
@@ -0,0 +1,26 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2012-2015 ForgeRock AS.
+
+
+# This script may be used to compare LDIF files.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFDiff"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldifdiff"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifmodify b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifmodify
new file mode 100644
index 0000000..a4a9878
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifmodify
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2012-2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP add, delete, modify, and modify DN
+# operations against an LDIF file.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFModify"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldifmodify"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifsearch b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifsearch
new file mode 100644
index 0000000..f2bc7fc
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/ldifsearch
@@ -0,0 +1,26 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2012-2015 ForgeRock AS.
+
+
+# This script may be used to perform search operations against an LDIF file.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.LDIFSearch"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="ldifsearch"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/makeldif b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/makeldif
new file mode 100644
index 0000000..86273b4
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/makeldif
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2013-2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP add, delete, modify, and modify DN
+# operations against an LDIF file.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.MakeLDIF"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="makeldif"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/modrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/modrate
new file mode 100644
index 0000000..b3a32f1
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/modrate
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP search operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.ModRate"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="modrate"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/searchrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/searchrate
new file mode 100644
index 0000000..1096381
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/searchrate
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2015 ForgeRock AS.
+
+
+# This script may be used to perform LDAP search operations.
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.SearchRate"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="searchrate"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/descriptor.xml b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/descriptor.xml
new file mode 100644
index 0000000..ab4fdff
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/descriptor.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2016 ForgeRock AS.
+ -->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
+                              http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <id>opendj-ldap-toolkit</id>
+
+    <baseDirectory>${project.artifactId}</baseDirectory>
+
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>lib</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+        </dependencySet>
+    </dependencySets>
+
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}</directory>
+            <outputDirectory></outputDirectory>
+            <directoryMode>755</directoryMode>
+            <fileMode>644</fileMode>
+            <includes>
+                <include>README</include>
+                <include>LICENSE</include>
+                <include>NOTICE</include>
+            </includes>
+        </fileSet>
+
+        <!-- Include CDDLv1_0.txt -->
+        <fileSet>
+            <directory>${basedir}/../legal-notices</directory>
+            <outputDirectory>legal-notices</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+            <includes>
+                <include>CDDLv1_0.txt</include>
+            </includes>
+        </fileSet>
+
+        <!-- Include THIRDPARTYREADME.txt -->
+        <fileSet>
+            <directory>${basedir}/legal-notices</directory>
+            <outputDirectory>legal-notices</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+            <includes>
+                <include>THIRDPARTYREADME.txt</include>
+            </includes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${project.parent.parent.basedir}</directory>
+            <outputDirectory></outputDirectory>
+            <directoryMode>755</directoryMode>
+            <fileMode>644</fileMode>
+            <includes>
+                <include>*.png</include>
+            </includes>
+        </fileSet>
+
+        <fileSet>
+            <directory>src/main/assembly/bin</directory>
+            <outputDirectory>bin</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0755</fileMode>
+            <lineEnding>unix</lineEnding>
+        </fileSet>
+
+        <fileSet>
+            <directory>src/main/assembly/bat</directory>
+            <outputDirectory>bat</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+            <lineEnding>dos</lineEnding>
+        </fileSet>
+
+        <fileSet>
+            <directory>src/main/assembly/libbin</directory>
+            <outputDirectory>lib</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0755</fileMode>
+            <lineEnding>unix</lineEnding>
+        </fileSet>
+
+        <fileSet>
+            <directory>src/main/assembly/libbat</directory>
+            <outputDirectory>lib</outputDirectory>
+            <directoryMode>0755</directoryMode>
+            <fileMode>0644</fileMode>
+            <lineEnding>dos</lineEnding>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_client-script.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_client-script.bat
new file mode 100644
index 0000000..eaed204
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_client-script.bat
@@ -0,0 +1,42 @@
+
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2009 Sun Microsystems, Inc.
+
+rem This script is used to invoke various client-side processes.  It should not
+rem be invoked directly by end users.
+
+setlocal
+for %%i in (%~sf0) do set DIR_HOME=%%~dPsi..
+set INSTALL_ROOT=%DIR_HOME%
+
+if "%NO_CHECK%" == "" set NO_CHECK=true
+
+if "%OPENDJ_INVOKE_CLASS%" == "" goto noInvokeClass
+goto launchCommand
+
+:noInvokeClass
+echo Error:  OPENDJ_INVOKE_CLASS environment variable is not set.
+pause
+goto end
+
+:launchCommand
+set SCRIPT_UTIL_CMD=set-full-environment
+call "%INSTALL_ROOT%\lib\_script-util.bat" $*
+if NOT %errorlevel% == 0 exit /B %errorlevel%
+
+"%OPENDJ_JAVA_BIN%" %OPENDJ_JAVA_ARGS% %SCRIPT_NAME_ARG% %OPENDJ_INVOKE_CLASS% %*
+
+:end
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_script-util.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_script-util.bat
new file mode 100644
index 0000000..1a612a9
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/_script-util.bat
@@ -0,0 +1,145 @@
+@echo off
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2008-2009 Sun Microsystems, Inc.
+rem Portions copyright 2013-2016 ForgeRock AS.
+
+set SET_JAVA_HOME_AND_ARGS_DONE=false
+set SET_ENVIRONMENT_VARS_DONE=false
+set SET_CLASSPATH_DONE=false
+
+if "%INSTALL_ROOT%" == "" goto setInstanceRoot
+
+:scriptBegin
+if "%SCRIPT_UTIL_CMD%" == "set-full-environment-and-test-java" goto setFullEnvironmentAndTestJava
+if "%SCRIPT_UTIL_CMD%" == "set-full-environment" goto setFullEnvironment
+if "%SCRIPT_UTIL_CMD%" == "set-java-home-and-args" goto setJavaHomeAndArgs
+if "%SCRIPT_UTIL_CMD%" == "set_environment_vars" goto setEnvironmentVars
+if "%SCRIPT_UTIL_CMD%" == "test-java" goto testJava
+if "%SCRIPT_UTIL_CMD%" == "set-classpath" goto setClassPath
+goto prepareCheck
+
+:setInstanceRoot
+setlocal
+for %%i in (%~sf0) do set DIR_HOME=%%~dPsi..
+set INSTALL_ROOT=%DIR_HOME%
+set CUR_DIR=%~dp0
+cd /d %INSTALL_ROOT%
+cd /d %INSTANCE_DIR%
+set INSTANCE_ROOT=%CD%
+cd /d %CUR_DIR%
+goto scriptBegin
+
+
+:setClassPath
+if "%SET_CLASSPATH_DONE%" == "true" goto prepareCheck
+FOR %%x in ("%INSTALL_ROOT%\lib\*.jar") DO call "%INSTALL_ROOT%\lib\setcp.bat" %%x
+if "%INSTALL_ROOT%" == "%INSTANCE_ROOT%"goto setClassPathDone
+FOR %%x in ("%INSTANCE_ROOT%\lib\*.jar") DO call "%INSTANCE_ROOT%\lib\setcp.bat" %%x
+FOR %%x in ("%INSTALL_ROOT%\resources\*.jar") DO call "%INSTALL_ROOT%\lib\setcp.bat" %%x
+set CLASSPATH=%INSTANCE_ROOT%\classes;%CLASSPATH%
+:setClassPathDone
+set SET_CLASSPATH_DONE=true
+goto scriptBegin
+
+:setFullEnvironment
+if "%SET_JAVA_HOME_AND_ARGS_DONE%" == "false" goto setJavaHomeAndArgs
+if "%SET_CLASSPATH_DONE%" == "false" goto setClassPath
+if "%SET_ENVIRONMENT_VARS_DONE%" == "false" goto setEnvironmentVars
+goto prepareCheck
+
+:setFullEnvironmentAndTestJava
+if "%SET_JAVA_HOME_AND_ARGS_DONE%" == "false" goto setJavaHomeAndArgs
+if "%SET_CLASSPATH_DONE%" == "false" goto setClassPath
+if "%SET_ENVIRONMENT_VARS_DONE%" == "false" goto setEnvironmentVars
+goto testJava
+
+:setJavaHomeAndArgs
+if "%SET_JAVA_HOME_AND_ARGS_DONE%" == "true" goto prepareCheck
+if not exist "%INSTANCE_ROOT%\lib\set-java-home.bat" goto checkEnvJavaArgs
+call "%INSTANCE_ROOT%\lib\set-java-home.bat"
+if "%OPENDJ_JAVA_BIN%" == "" goto checkEnvJavaArgs
+:endJavaHomeAndArgs
+set SET_JAVA_HOME_AND_ARGS_DONE=true
+goto scriptBegin
+
+:checkEnvJavaArgs
+if "%OPENDJ_JAVA_BIN%" == "" goto checkOpenDJJavaHome
+if not exist "%OPENDJ_JAVA_BIN%" goto checkOpenDJJavaHome
+goto endJavaHomeAndArgs
+
+:checkOpenDJJavaHome
+if "%OPENDJ_JAVA_HOME%" == "" goto checkJavaBin
+if not exist "%OPENDJ_JAVA_HOME%\bin\java.exe" goto checkJavaBin
+set OPENDJ_JAVA_BIN=%OPENDJ_JAVA_HOME%\bin\java.exe
+goto endJavaHomeAndArgs
+
+:checkJavaBin
+if "%JAVA_BIN%" == "" goto checkJavaHome
+if not exist "%JAVA_BIN%" goto checkJavaHome
+set OPENDJ_JAVA_BIN=%JAVA_BIN%
+goto endJavaHomeAndArgs
+
+:checkJavaHome
+if "%JAVA_HOME%" == "" goto checkJavaPath
+if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaFound
+set OPENDJ_JAVA_BIN=%JAVA_HOME%\bin\java.exe
+goto endJavaHomeAndArgs
+
+:checkJavaPath
+java.exe -version > NUL 2>&1
+if not %errorlevel% == 0 goto noJavaFound
+set OPENDJ_JAVA_BIN=java.exe
+goto endJavaHomeAndArgs
+
+:noJavaFound
+echo ERROR:  Could not find a valid Java binary to be used.
+echo You must specify the path to a valid Java 5.0 or higher version.
+echo The procedure to follow is:
+echo 1. Delete the file %INSTANCE_ROOT%\lib\set-java-home.bat if it exists.
+echo 2. Set the environment variable OPENDJ_JAVA_HOME to the root of a valid
+echo Java 5.0 installation.
+echo If you want to have specific Java settings for each command line you must
+echo follow the steps 3 and 4.
+echo 3. Edit the properties file specifying the Java binary and the Java arguments
+echo for each command line.  The Java properties file is located in:
+echo %INSTANCE_ROOT%\config\java.properties.
+echo 4. Run the command-line %INSTALL_ROOT%\bat\dsjavaproperties.bat
+pause
+exit /B 1
+
+:setEnvironmentVars
+if %SET_ENVIRONMENT_VARS_DONE% == "true" goto prepareCheck
+set PATH=%SystemRoot%;%PATH%
+set SCRIPT_NAME_ARG=-Dcom.forgerock.opendj.ldap.tools.scriptName=%SCRIPT_NAME%
+set SET_ENVIRONMENT_VARS_DONE=true
+goto scriptBegin
+
+:isVersionOrHelp
+if [%1] == [] goto end
+if [%1] == [--help] goto end
+if [%1] == [-H] goto end
+if [%1] == [--version] goto end
+if [%1] == [-V] goto end
+if [%1] == [--fullversion] goto end
+if [%1] == [-F] goto end
+shift
+goto isVersionOrHelp
+
+:prepareCheck
+rem Perform check unless it is specified not to do it
+if "%NO_CHECK%" == ""  set NO_CHECK=false
+goto isVersionOrHelp
+
+:end
+exit /B 0
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/setcp.bat b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/setcp.bat
new file mode 100644
index 0000000..a47d4e6
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbat/setcp.bat
@@ -0,0 +1,29 @@
+
+rem The contents of this file are subject to the terms of the Common Development and
+rem Distribution License (the License). You may not use this file except in compliance with the
+rem License.
+rem
+rem You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+rem specific language governing permission and limitations under the License.
+rem
+rem When distributing Covered Software, include this CDDL Header Notice in each file and include
+rem the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+rem Header, with the fields enclosed by brackets [] replaced by your own identifying
+rem information: "Portions Copyright [year] [name of copyright owner]".
+rem
+rem Copyright 2006-2008 Sun Microsystems, Inc.
+
+set CLASSPATHCOMPONENT=%1
+if ""%1""=="""" goto gotAllArgs
+shift
+
+:argCheck
+if ""%1""=="""" goto gotAllArgs
+set CLASSPATHCOMPONENT=%CLASSPATHCOMPONENT% %1
+shift
+goto argCheck
+
+:gotAllArgs
+set CLASSPATH=%CLASSPATHCOMPONENT%;%CLASSPATH%
+
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_client-script.sh b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_client-script.sh
new file mode 100644
index 0000000..f65eb6c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_client-script.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2006-2008 Sun Microsystems, Inc.
+
+
+# This script is used to invoke various client-side processes.  It should not
+# be invoked directly by end users.
+if test -z "${OPENDJ_INVOKE_CLASS}"
+then
+  echo "ERROR:  OPENDJ_INVOKE_CLASS environment variable is not set."
+  exit 1
+fi
+
+
+# Capture the current working directory so that we can change to it later.
+# Then capture the location of this script and the Directory Server install
+# root so that we can use them to create appropriate paths.
+WORKING_DIR=`pwd`
+
+cd "`dirname "${0}"`"
+SCRIPT_DIR=`pwd`
+
+cd ..
+INSTALL_ROOT=`pwd`
+export INSTALL_ROOT
+
+cd "${WORKING_DIR}"
+
+
+# Set environment variables
+SCRIPT_UTIL_CMD=set-full-environment
+export SCRIPT_UTIL_CMD
+
+.  "${INSTALL_ROOT}/lib/_script-util.sh"
+RETURN_CODE=$?
+if test ${RETURN_CODE} -ne 0
+then
+  exit ${RETURN_CODE}
+fi
+
+# Launch the appropriate client utility.
+"${OPENDJ_JAVA_BIN}" ${OPENDJ_JAVA_ARGS} ${SCRIPT_NAME_ARG} "${OPENDJ_INVOKE_CLASS}" "${@}"
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_script-util.sh b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_script-util.sh
new file mode 100644
index 0000000..4d19383
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/libbin/_script-util.sh
@@ -0,0 +1,119 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2008-2009 Sun Microsystems, Inc.
+
+#
+# function that sets the java home
+#
+set_java_home_and_args() {
+  if test -f "${INSTANCE_ROOT}/lib/set-java-home"
+  then
+    . "${INSTANCE_ROOT}/lib/set-java-home"
+  fi
+  if test -z "${OPENDJ_JAVA_BIN}"
+  then
+    if test -z "${OPENDJ_JAVA_HOME}"
+    then
+      if test -z "${JAVA_BIN}"
+      then
+        if test -z "${JAVA_HOME}"
+        then
+          OPENDJ_JAVA_BIN=`which java 2> /dev/null`
+          if test ${?} -eq 0
+          then
+            export OPENDJ_JAVA_BIN
+          else
+            echo "Please set OPENDJ_JAVA_HOME to the root of a Java 5 (or later) installation"
+            echo "or edit the java.properties file and then run the dsjavaproperties script to"
+            echo "specify the Java version to be used"
+            exit 1
+          fi
+        else
+          OPENDJ_JAVA_BIN="${JAVA_HOME}/bin/java"
+          export OPENDJ_JAVA_BIN
+        fi
+      else
+        OPENDJ_JAVA_BIN="${JAVA_BIN}"
+        export OPENDJ_JAVA_BIN
+      fi
+    else
+      OPENDJ_JAVA_BIN="${OPENDJ_JAVA_HOME}/bin/java"
+      export OPENDJ_JAVA_BIN
+    fi
+  fi
+}
+
+# Explicitly set the PATH, LD_LIBRARY_PATH, LD_PRELOAD, and other important
+# system environment variables for security and compatibility reasons.
+set_environment_vars() {
+  PATH=/bin:/usr/bin
+  LD_LIBRARY_PATH=
+  LD_LIBRARY_PATH_32=
+  LD_LIBRARY_PATH_64=
+  LD_PRELOAD=
+  LD_PRELOAD_32=
+  LD_PRELOAD_64=
+  export PATH LD_LIBRARY_PATH LD_LIBRARY_PATH_32 LD_LIBRARY_PATH_64 \
+       LD_PRELOAD LD_PRELOAD_32 LD_PRELOAD_64
+  SCRIPT_NAME_ARG=-Dcom.forgerock.opendj.ldap.tools.scriptName=${SCRIPT_NAME}
+	export SCRIPT_NAME_ARG
+}
+
+# Configure the appropriate CLASSPATH.
+set_classpath() {
+  CLASSPATH=${INSTANCE_ROOT}/classes
+  for JAR in "${INSTALL_ROOT}/lib/"*.jar
+  do
+    CLASSPATH=${CLASSPATH}:${JAR}
+  done
+  if [ "${INSTALL_ROOT}" != "${INSTANCE_ROOT}" ]
+  then
+    for JAR in "${INSTANCE_ROOT}/lib/"*.jar
+    do
+      CLASSPATH=${CLASSPATH}:${JAR}
+    done
+  fi
+  export CLASSPATH
+}
+
+if test "${INSTALL_ROOT}" = ""
+then
+  # Capture the current working directory so that we can change to it later.
+  # Then capture the location of this script and the Directory Server instance
+  # root so that we can use them to create appropriate paths.
+  WORKING_DIR=`pwd`
+
+  cd "`dirname "${0}"`"
+  cd ..
+  INSTALL_ROOT=`pwd`
+  cd "${WORKING_DIR}"
+fi
+
+if test "${SCRIPT_UTIL_CMD}" = "set-full-environment"
+then
+  set_java_home_and_args
+  set_environment_vars
+  set_classpath
+elif test "${SCRIPT_UTIL_CMD}" = "set-java-home-and-args"
+then
+  set_java_home_and_args
+elif test "${SCRIPT_UTIL_CMD}" = "set-environment-vars"
+then
+  set_environment_vars
+elif test "${SCRIPT_UTIL_CMD}" = "set-classpath"
+then
+  set_classpath
+fi
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/man-pages.xml b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/man-pages.xml
new file mode 100644
index 0000000..b5fb958
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/man-pages.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2015 ForgeRock AS.
+ -->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
+                              http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+  <id>man-pages</id>
+
+  <includeBaseDirectory>false</includeBaseDirectory>
+
+  <!--
+    Using <files> instead of <fileset> to avoid getting the full path.
+    The .jar to has this file at /man-pages/man-addrate.xml for example.
+  -->
+  <files>
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-addrate.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-authrate.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldapcompare.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldapmodify.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldappasswordmodify.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldapsearch.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldifdiff.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldifmodify.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-ldifsearch.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-makeldif.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-modrate.xml</source>
+    </file>
+
+    <file>
+      <outputDirectory>man-pages</outputDirectory>
+      <source>target/generated-man-pages/man-searchrate.xml</source>
+    </file>
+  </files>
+
+  <formats>
+    <format>jar</format>
+  </formats>
+</assembly>
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
new file mode 100644
index 0000000..47dcec1
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
@@ -0,0 +1,542 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.MultiColumnPrinter.column;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static java.util.concurrent.TimeUnit.*;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import static org.forgerock.opendj.ldap.LdapException.*;
+import static org.forgerock.opendj.ldap.ResultCode.*;
+import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
+import static org.forgerock.util.promise.Promises.*;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.RatioGauge;
+import com.forgerock.opendj.cli.MultiColumnPrinter;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.responses.Responses;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.EntryGenerator;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A load generation tool that can be used to load a Directory Server with Add
+ * and Delete requests using one or more LDAP connections.
+ */
+public class AddRate extends ConsoleApplication {
+
+    @SuppressWarnings("serial")
+    private static final class AddRateExecutionEndedException extends LdapException {
+        private AddRateExecutionEndedException() {
+            super(Responses.newResult(OTHER));
+        }
+    }
+
+    private final class AddPerformanceRunner extends PerformanceRunner {
+        private final class AddStatsHandler extends UpdateStatsResultHandler<Result> {
+            private final String entryDN;
+
+            private AddStatsHandler(final long currentTime, final String entryDN) {
+                super(currentTime);
+                this.entryDN = entryDN;
+            }
+
+            @Override
+            void updateAdditionalStatsOnResult() {
+                switch (delStrategy) {
+                case RANDOM:
+                    long newKey;
+                    do {
+                        newKey = randomSeq.get().nextInt();
+                    } while (dnEntriesAdded.putIfAbsent(newKey, entryDN) != null);
+                    break;
+                case FIFO:
+                    long uniqueTime = operationStartTimeNs;
+                    while (dnEntriesAdded.putIfAbsent(uniqueTime, entryDN) != null) {
+                        uniqueTime++;
+                    }
+                    break;
+                default:
+                    break;
+                }
+
+                addCounter.inc();
+                entryCount.inc();
+            }
+        }
+
+        private final class DeleteStatsHandler extends UpdateStatsResultHandler<Result> {
+            private DeleteStatsHandler(final long startTime) {
+                super(startTime);
+            }
+
+            @Override
+            void updateAdditionalStatsOnResult() {
+                deleteCounter.inc();
+                entryCount.dec();
+            }
+        }
+
+        private final class AddRateStatsThread extends StatsThread {
+            private static final int PERCENTAGE_ADD_COLUMN_WIDTH = 6;
+            private static final String PERCENTAGE_ADD = STAT_ID_PREFIX + "add_percentage";
+
+            private AddRateStatsThread(final PerformanceRunner perfRunner, final ConsoleApplication app) {
+                super(perfRunner, app);
+            }
+
+            @Override
+            void resetAdditionalStats() {
+                addCounter = newIntervalCounter();
+                deleteCounter = newIntervalCounter();
+            }
+
+            @Override
+            List<MultiColumnPrinter.Column> registerAdditionalColumns() {
+                registry.register(PERCENTAGE_ADD, new RatioGauge() {
+                    @Override
+                    protected Ratio getRatio() {
+                        final long addIntervalCount = addCounter.refreshIntervalCount();
+                        final long deleteIntervalCount = deleteCounter.refreshIntervalCount();
+                        return Ratio.of(addIntervalCount * 100, addIntervalCount + deleteIntervalCount);
+                    }
+                });
+                return Collections.singletonList(column(PERCENTAGE_ADD, "Add%", PERCENTAGE_ADD_COLUMN_WIDTH, 2));
+            }
+        }
+
+        private final class AddDeleteWorkerThread extends WorkerThread {
+            private AddDeleteWorkerThread(final Connection connection, final ConnectionFactory connectionFactory) {
+                super(connection, connectionFactory);
+            }
+
+            @Override
+            public Promise<?, LdapException> performOperation(
+                    final Connection connection, final DataSource[] dataSources, final long currentTimeNs) {
+                startPurgeIfMaxNumberAddReached();
+                startToggleDeleteIfAgeThresholdReached(currentTimeNs);
+                try {
+                    String entryToRemove = getEntryToRemove();
+                    if (entryToRemove != null) {
+                        return doDelete(connection, currentTimeNs, entryToRemove);
+                    }
+
+                    return doAdd(connection, currentTimeNs);
+                } catch (final AddRateExecutionEndedException a) {
+                    return newResultPromise(OTHER);
+                } catch (final IOException e) {
+                    return newExceptionPromise(newLdapException(OTHER, e));
+                }
+            }
+
+            private void startToggleDeleteIfAgeThresholdReached(long currentTime) {
+                if (!toggleDelete
+                        && delThreshold == DeleteThreshold.AGE_THRESHOLD
+                        && !dnEntriesAdded.isEmpty()
+                        && dnEntriesAdded.firstKey() + timeToWait < currentTime) {
+                    setSizeThreshold(entryCount.getCount());
+                }
+            }
+
+            private void startPurgeIfMaxNumberAddReached() {
+                AtomicBoolean purgeLatch = new AtomicBoolean();
+                if (!isPurgeBranchRunning.get()
+                            && 0 < maxNbAddIterations && maxNbAddIterations < addCounter.getCount()
+                            && purgeLatch.compareAndSet(false, true)) {
+                    newPurgerThread().start();
+                }
+            }
+
+            // FIXME Followings @Checkstyle:ignore tags are related to the maven-checkstyle-plugin
+            // issue related here: https://github.com/checkstyle/checkstyle/issues/5
+            // @Checkstyle:ignore
+            private String getEntryToRemove() throws AddRateExecutionEndedException {
+                if (isPurgeBranchRunning.get()) {
+                    return purgeEntry();
+                } else if (toggleDelete && entryCount.getCount() > sizeThreshold) {
+                    return removeFirstAddedEntry();
+                }
+                return null;
+            }
+
+            // @Checkstyle:ignore
+            private String purgeEntry() throws AddRateExecutionEndedException {
+                if (!dnEntriesAdded.isEmpty()) {
+                    return removeFirstAddedEntry();
+                }
+                localStopRequested = true;
+                throw new AddRateExecutionEndedException();
+            }
+
+            private String removeFirstAddedEntry() {
+                final Map.Entry<Long, String> entry = dnEntriesAdded.pollFirstEntry();
+                return entry != null ? entry.getValue() : null;
+            }
+
+            private Promise<Result, LdapException> doAdd(
+                    final Connection connection, final long currentTime) throws IOException {
+                final Entry entry;
+                synchronized (generator) {
+                    entry = generator.readEntry();
+                }
+
+                final LdapResultHandler<Result> addHandler = new AddStatsHandler(
+                        currentTime, entry.getName().toString());
+                return connection.addAsync(newAddRequest(entry))
+                                 .thenOnResultOrException(addHandler, addHandler);
+            }
+
+            private Promise<?, LdapException> doDelete(
+                    final Connection connection, final long currentTime, final String entryToRemove) {
+                final LdapResultHandler<Result> deleteHandler = new DeleteStatsHandler(currentTime);
+                return connection.deleteAsync(newDeleteRequest(entryToRemove))
+                                 .thenOnResultOrException(deleteHandler, deleteHandler);
+            }
+        }
+
+        private final class AddRateTimerThread extends TimerThread {
+            private AddRateTimerThread(final long timeToWait) {
+                super(timeToWait);
+            }
+
+            @Override
+            void performStopOperations() {
+                if (purgeEnabled && isPurgeBranchRunning.compareAndSet(false, true)) {
+                    if (!isScriptFriendly()) {
+                        println(LocalizableMessage.raw("Purge phase..."));
+                    }
+                    try {
+                        joinAllWorkerThreads();
+                    } catch (final InterruptedException e) {
+                        throw new IllegalStateException();
+                    }
+                } else if (!purgeEnabled) {
+                    stopTool();
+                }
+            }
+        }
+
+        private final ConcurrentSkipListMap<Long, String> dnEntriesAdded = new ConcurrentSkipListMap<>();
+        private final ThreadLocal<Random> randomSeq = new ThreadLocal<Random>() {
+            @Override
+            protected Random initialValue() {
+                return new Random();
+            }
+        };
+
+        private EntryGenerator generator;
+        private DeleteStrategy delStrategy;
+        private DeleteThreshold delThreshold;
+        private long sizeThreshold;
+        private volatile boolean toggleDelete;
+        private long timeToWait;
+        private int maxNbAddIterations;
+        private boolean purgeEnabled;
+        private StatsThread.IntervalCounter addCounter = StatsThread.newIntervalCounter();
+        private StatsThread.IntervalCounter deleteCounter = StatsThread.newIntervalCounter();
+        private final Counter entryCount = new Counter();
+        private final AtomicBoolean isPurgeBranchRunning = new AtomicBoolean();
+
+        private AddPerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException {
+            super(options);
+        }
+
+        @Override
+        WorkerThread newWorkerThread(final Connection connection, final ConnectionFactory connectionFactory) {
+            return new AddDeleteWorkerThread(connection, connectionFactory);
+        }
+
+        @Override
+        StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) {
+            return new AddRateStatsThread(performanceRunner, app);
+        }
+
+        @Override
+        TimerThread newEndTimerThread(final long timeToWait) {
+            return new AddRateTimerThread(timeToWait);
+        }
+
+        TimerThread newPurgerThread() {
+            return newEndTimerThread(0);
+        }
+
+        public void validate(final MultiChoiceArgument<DeleteStrategy> delModeArg,
+                final IntegerArgument delSizeThresholdArg, final IntegerArgument delAgeThresholdArg,
+                final BooleanArgument noPurgeArgument) throws ArgumentException {
+            super.validate();
+            delStrategy = delModeArg.getTypedValue();
+            maxNbAddIterations = maxIterationsArgument.getIntValue();
+            purgeEnabled = !noPurgeArgument.isPresent();
+
+            // Check for inconsistent use cases
+            if (delSizeThresholdArg.isPresent() && delAgeThresholdArg.isPresent()) {
+                throw new ArgumentException(ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE.get());
+            } else if (delStrategy == DeleteStrategy.OFF
+                && (delSizeThresholdArg.isPresent() || delAgeThresholdArg.isPresent())) {
+                throw new ArgumentException(ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get());
+            } else if (delStrategy == DeleteStrategy.RANDOM && delAgeThresholdArg.isPresent()) {
+                throw new ArgumentException(ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE.get());
+            }
+
+            if (delStrategy != DeleteStrategy.OFF) {
+                delThreshold =
+                    delAgeThresholdArg.isPresent() ? DeleteThreshold.AGE_THRESHOLD : DeleteThreshold.SIZE_THRESHOLD;
+                if (delThreshold == DeleteThreshold.SIZE_THRESHOLD) {
+                    setSizeThreshold(delSizeThresholdArg.getIntValue());
+                    if (0 < maxNbAddIterations && maxNbAddIterations < sizeThreshold) {
+                        throw new ArgumentException(ERR_ADDRATE_SIZE_THRESHOLD_LOWER_THAN_ITERATIONS.get());
+                    }
+                } else {
+                    timeToWait = NANOSECONDS.convert(delAgeThresholdArg.getIntValue(), SECONDS);
+                }
+            }
+        }
+
+        private void setSizeThreshold(final long entriesSizeThreshold) {
+            sizeThreshold = entriesSizeThreshold;
+            toggleDelete = true;
+        }
+    }
+
+    private enum DeleteStrategy {
+        OFF, RANDOM, FIFO;
+    }
+
+    private enum DeleteThreshold {
+        SIZE_THRESHOLD, AGE_THRESHOLD, OFF;
+    }
+
+    private static final int EXIT_CODE_SUCCESS = 0;
+    private static final int DEFAULT_SIZE_THRESHOLD = 10000;
+    /** The minimum time to wait before starting add/delete phase (in seconds). */
+    private static final int AGE_THRESHOLD_LOWERBOUND = 1;
+    /** The minimum number of entries to add before starting add/delete phase. */
+    private static final int SIZE_THRESHOLD_LOWERBOUND = 1;
+
+    /**
+     * The main method for AddRate tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new AddRate().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+    private BooleanArgument scriptFriendly;
+
+    private AddRate() {
+        // Nothing to do
+    }
+
+    AddRate(final PrintStream out, final PrintStream err) {
+        super(out, err);
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isScriptFriendly() {
+        return scriptFriendly.isPresent();
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_ADDRATE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+            new ArgumentParser(AddRate.class.getName(), toolDescription, false, true, 1, 1, "template-file-path");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_ADDRATE.get());
+        argParser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RATE_TOOLS.get());
+
+        final ConnectionFactoryProvider connectionFactoryProvider;
+        final ConnectionFactory connectionFactory;
+        final AddPerformanceRunner runner;
+
+        /* Entries generation parameters */
+        final IntegerArgument randomSeedArg;
+        final StringArgument resourcePathArg;
+        final StringArgument constantsArg;
+
+        /* addrate specifics arguments */
+        final MultiChoiceArgument<DeleteStrategy> deleteMode;
+        final IntegerArgument deleteSizeThreshold;
+        final IntegerArgument deleteAgeThreshold;
+        final BooleanArgument noPurgeArgument;
+
+        try {
+            Utils.setDefaultPerfToolProperties();
+            final PerformanceRunnerOptions options = new PerformanceRunnerOptions(argParser, this);
+            options.setSupportsGeneratorArgument(false);
+
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            runner = new AddPerformanceRunner(options);
+
+            addCommonArguments(argParser);
+
+            /* Entries generation parameters */
+            resourcePathArg =
+                    StringArgument.builder(MakeLDIF.OPTION_LONG_RESOURCE_PATH)
+                            .shortIdentifier('r')
+                            .description(INFO_ADDRATE_DESCRIPTION_RESOURCE_PATH.get())
+                            .docDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RESOURCE_PATH.get())
+                            .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            randomSeedArg =
+                    IntegerArgument.builder(OPTION_LONG_RANDOM_SEED)
+                            .shortIdentifier('R')
+                            .description(INFO_ADDRATE_DESCRIPTION_SEED.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_SEED_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            constantsArg =
+                    StringArgument.builder(MakeLDIF.OPTION_LONG_CONSTANT)
+                            .shortIdentifier('g')
+                            .description(INFO_ADDRATE_DESCRIPTION_CONSTANT.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_CONSTANT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            /* addrate specifics arguments */
+            deleteMode =
+                    MultiChoiceArgument.<DeleteStrategy>builder("deleteMode")
+                            .shortIdentifier('C')
+                            .description(INFO_ADDRATE_DESCRIPTION_DELETEMODE.get())
+                            .allowedValues(DeleteStrategy.values())
+                            .defaultValue(DeleteStrategy.FIFO)
+                            .valuePlaceholder(INFO_DELETEMODE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            deleteSizeThreshold =
+                    IntegerArgument.builder("deleteSizeThreshold")
+                            .shortIdentifier('s')
+                            .description(INFO_ADDRATE_DESCRIPTION_DELETESIZETHRESHOLD.get())
+                            .lowerBound(SIZE_THRESHOLD_LOWERBOUND)
+                            .defaultValue(DEFAULT_SIZE_THRESHOLD)
+                            .valuePlaceholder(INFO_DELETESIZETHRESHOLD_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            deleteAgeThreshold =
+                    IntegerArgument.builder("deleteAgeThreshold")
+                            .shortIdentifier('a')
+                            .description(INFO_ADDRATE_DESCRIPTION_DELETEAGETHRESHOLD.get())
+                            .lowerBound(AGE_THRESHOLD_LOWERBOUND)
+                            .valuePlaceholder(INFO_DELETEAGETHRESHOLD_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            noPurgeArgument =
+                    BooleanArgument.builder("noPurge")
+                        .shortIdentifier('n')
+                        .description(INFO_ADDRATE_DESCRIPTION_NOPURGE.get())
+                        .buildAndAddToParser(argParser);
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            if (argParser.usageOrVersionDisplayed()) {
+                return EXIT_CODE_SUCCESS;
+            }
+
+            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
+            runner.validate(deleteMode, deleteSizeThreshold, deleteAgeThreshold, noPurgeArgument);
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final String templatePath = argParser.getTrailingArguments().get(0);
+        runner.generator = MakeLDIF.createGenerator(
+                templatePath, resourcePathArg, randomSeedArg, constantsArg, false, this);
+        if (runner.generator == null) {
+            // Error message has already been logged.
+            return ResultCode.OPERATIONS_ERROR.intValue();
+        }
+        Runtime.getRuntime().addShutdownHook(runner.newPurgerThread());
+
+        return runner.run(connectionFactory);
+    }
+
+    private void addCommonArguments(final ArgumentParser argParser) throws ArgumentException {
+        final StringArgument propertiesFileArgument = propertiesFileArgument();
+        argParser.addArgument(propertiesFileArgument);
+        argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+        final BooleanArgument noPropertiesFileArgument = noPropertiesFileArgument();
+        argParser.addArgument(noPropertiesFileArgument);
+        argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+        final BooleanArgument showUsage = showUsageArgument();
+        argParser.addArgument(showUsage);
+        argParser.setUsageArgument(showUsage, getOutputStream());
+
+        verbose = verboseArgument();
+        argParser.addArgument(verbose);
+
+        scriptFriendly =
+                BooleanArgument.builder("scriptFriendly")
+                        .shortIdentifier('S')
+                        .description(INFO_DESCRIPTION_SCRIPT_FRIENDLY.get())
+                        .buildAndAddToParser(argParser);
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
new file mode 100644
index 0000000..f0f6f8f
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
@@ -0,0 +1,516 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.MultiColumnPrinter.column;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.ldap.tools.Utils.*;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+import com.codahale.metrics.RatioGauge;
+import com.forgerock.opendj.cli.MultiColumnPrinter;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
+import org.forgerock.opendj.ldap.requests.ExternalSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.GSSAPISASLBindRequest;
+import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A load generation tool that can be used to load a Directory Server with Bind
+ * requests using one or more LDAP connections.
+ */
+public final class AuthRate extends ConsoleApplication {
+
+    private final class BindPerformanceRunner extends PerformanceRunner {
+        private final class BindStatsThread extends StatsThread {
+            private static final int BIND_TIME_PERCENTAGE_COLUMN_WIDTH = 5;
+            private static final String BIND_TIME_PERCENTAGE = STAT_ID_PREFIX + "bind_time_percentage";
+
+            private final boolean computeBindTime;
+
+            private BindStatsThread(final PerformanceRunner performanceRunner,
+                                    final ConsoleApplication app,
+                                    final boolean computeBindTime) {
+                super(performanceRunner, app);
+                this.computeBindTime = computeBindTime;
+            }
+
+            @Override
+            List<MultiColumnPrinter.Column> registerAdditionalColumns() {
+                if (!computeBindTime) {
+                    return Collections.emptyList();
+                }
+
+                registry.register(BIND_TIME_PERCENTAGE, new RatioGauge() {
+                    @Override
+                    protected Ratio getRatio() {
+                        final long searchWaitTimeIntervalNs = searchWaitRecentTimeNs.getLastIntervalCount();
+                        final long waitTimeIntervalNs = waitDurationNsCount.getLastIntervalCount();
+                        return Ratio.of(100 * (waitTimeIntervalNs - searchWaitTimeIntervalNs), waitTimeIntervalNs);
+                    }
+                });
+                return Collections.singletonList(
+                        column(BIND_TIME_PERCENTAGE, "bind time %", BIND_TIME_PERCENTAGE_COLUMN_WIDTH, 1));
+            }
+        }
+
+        private final class BindWorkerThread extends WorkerThread {
+            private SearchRequest sr;
+            private BindRequest br;
+            private Object[] data;
+            private final char[] invalidPassword = "invalid-password".toCharArray();
+
+            private final ThreadLocal<Random> rng = new ThreadLocal<Random>() {
+                @Override
+                protected Random initialValue() {
+                    return new Random();
+                }
+            };
+
+            private BindWorkerThread(final Connection connection, final ConnectionFactory connectionFactory) {
+                super(connection, connectionFactory);
+            }
+
+            @Override
+            public Promise<?, LdapException> performOperation(final Connection connection,
+                    final DataSource[] dataSources, final long currentTimeNs) {
+                if (dataSources != null) {
+                    data = DataSource.generateData(dataSources, data);
+                    if (data.length == dataSources.length) {
+                        final Object[] newData = new Object[data.length + 1];
+                        System.arraycopy(data, 0, newData, 0, data.length);
+                        data = newData;
+                    }
+                }
+
+                Promise<BindResult, LdapException> returnedPromise;
+                if (filter != null && baseDN != null) {
+                    if (sr == null) {
+                        if (dataSources != null) {
+                            final String newBaseDN = String.format(baseDN, data);
+                            final String newFilter = String.format(filter, data);
+                            sr = Requests.newSearchRequest(newBaseDN, scope, newFilter, attributes);
+                        } else {
+                            sr = Requests.newSearchRequest(baseDN, scope, filter, attributes);
+                        }
+                        sr.setDereferenceAliasesPolicy(dereferencesAliasesPolicy);
+                    } else if (dataSources != null) {
+                        sr.setFilter(String.format(filter, data));
+                        sr.setName(String.format(baseDN, data));
+                    }
+
+                    returnedPromise = connection.searchSingleEntryAsync(sr).thenAsync(
+                            new AsyncFunction<SearchResultEntry, BindResult, LdapException>() {
+                                @Override
+                                public Promise<BindResult, LdapException> apply(SearchResultEntry result)
+                                        throws LdapException {
+                                    searchWaitRecentTimeNs.inc(System.nanoTime() - currentTimeNs);
+                                    if (data == null) {
+                                        data = new Object[1];
+                                    }
+                                    data[data.length - 1] = result.getName().toString();
+
+                                    return performBind(connection, data);
+                                }
+                            });
+                } else {
+                    returnedPromise = performBind(connection, data);
+                }
+
+                incrementIterationCount();
+                return returnedPromise.thenOnResult(new UpdateStatsResultHandler<BindResult>(currentTimeNs))
+                                      .thenOnException(new UpdateStatsResultHandler<BindResult>(currentTimeNs));
+            }
+
+            private Promise<BindResult, LdapException> performBind(final Connection connection,
+                final Object[] data) {
+                final boolean useInvalidPassword;
+
+                // Avoid rng if possible.
+                switch (invalidCredPercent) {
+                case 0:
+                    useInvalidPassword = false;
+                    break;
+                case 100:
+                    useInvalidPassword = true;
+                    break;
+                default:
+                    final Random r = rng.get();
+                    final int p = r.nextInt(100);
+                    useInvalidPassword = p < invalidCredPercent;
+                    break;
+                }
+
+                final BindRequest bindRequest = bindRequestTemplate;
+                if (bindRequest instanceof SimpleBindRequest) {
+                    final SimpleBindRequest o = (SimpleBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfSimpleBindRequest(o);
+                    }
+
+                    final SimpleBindRequest sbr = (SimpleBindRequest) br;
+                    if (data != null && o.getName() != null) {
+                        sbr.setName(String.format(o.getName(), data));
+                    }
+                    if (useInvalidPassword) {
+                        sbr.setPassword(invalidPassword);
+                    } else {
+                        sbr.setPassword(o.getPassword());
+                    }
+                } else if (bindRequest instanceof DigestMD5SASLBindRequest) {
+                    final DigestMD5SASLBindRequest o = (DigestMD5SASLBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfDigestMD5SASLBindRequest(o);
+                    }
+
+                    final DigestMD5SASLBindRequest sbr = (DigestMD5SASLBindRequest) br;
+                    if (data != null) {
+                        if (o.getAuthenticationID() != null) {
+                            sbr.setAuthenticationID(String.format(o.getAuthenticationID(), data));
+                        }
+                        if (o.getAuthorizationID() != null) {
+                            sbr.setAuthorizationID(String.format(o.getAuthorizationID(), data));
+                        }
+                    }
+                    if (useInvalidPassword) {
+                        sbr.setPassword(invalidPassword);
+                    } else {
+                        sbr.setPassword(o.getPassword());
+                    }
+                } else if (bindRequest instanceof CRAMMD5SASLBindRequest) {
+                    final CRAMMD5SASLBindRequest o = (CRAMMD5SASLBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfCRAMMD5SASLBindRequest(o);
+                    }
+
+                    final CRAMMD5SASLBindRequest sbr = (CRAMMD5SASLBindRequest) br;
+                    if (data != null && o.getAuthenticationID() != null) {
+                        sbr.setAuthenticationID(String.format(o.getAuthenticationID(), data));
+                    }
+                    if (useInvalidPassword) {
+                        sbr.setPassword(invalidPassword);
+                    } else {
+                        sbr.setPassword(o.getPassword());
+                    }
+                } else if (bindRequest instanceof GSSAPISASLBindRequest) {
+                    final GSSAPISASLBindRequest o = (GSSAPISASLBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfGSSAPISASLBindRequest(o);
+                    }
+
+                    final GSSAPISASLBindRequest sbr = (GSSAPISASLBindRequest) br;
+                    if (data != null) {
+                        if (o.getAuthenticationID() != null) {
+                            sbr.setAuthenticationID(String.format(o.getAuthenticationID(), data));
+                        }
+                        if (o.getAuthorizationID() != null) {
+                            sbr.setAuthorizationID(String.format(o.getAuthorizationID(), data));
+                        }
+                    }
+                    if (useInvalidPassword) {
+                        sbr.setPassword(invalidPassword);
+                    } else {
+                        sbr.setPassword(o.getPassword());
+                    }
+                } else if (bindRequest instanceof ExternalSASLBindRequest) {
+                    final ExternalSASLBindRequest o = (ExternalSASLBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfExternalSASLBindRequest(o);
+                    }
+
+                    final ExternalSASLBindRequest sbr = (ExternalSASLBindRequest) br;
+                    if (data != null && o.getAuthorizationID() != null) {
+                        sbr.setAuthorizationID(String.format(o.getAuthorizationID(), data));
+                    }
+                } else if (bindRequest instanceof PlainSASLBindRequest) {
+                    final PlainSASLBindRequest o = (PlainSASLBindRequest) bindRequest;
+                    if (br == null) {
+                        br = Requests.copyOfPlainSASLBindRequest(o);
+                    }
+
+                    final PlainSASLBindRequest sbr = (PlainSASLBindRequest) br;
+                    if (data != null) {
+                        if (o.getAuthenticationID() != null) {
+                            sbr.setAuthenticationID(String.format(o.getAuthenticationID(), data));
+                        }
+                        if (o.getAuthorizationID() != null) {
+                            sbr.setAuthorizationID(String.format(o.getAuthorizationID(), data));
+                        }
+                    }
+                    if (useInvalidPassword) {
+                        sbr.setPassword(invalidPassword);
+                    } else {
+                        sbr.setPassword(o.getPassword());
+                    }
+                }
+
+                return connection.bindAsync(br);
+            }
+        }
+
+        private final StatsThread.IntervalCounter searchWaitRecentTimeNs = StatsThread.newIntervalCounter();
+        private String filter;
+        private String baseDN;
+        private SearchScope scope;
+        private DereferenceAliasesPolicy dereferencesAliasesPolicy;
+        private String[] attributes;
+        private int invalidCredPercent;
+        /** Template of the bind requests which will be send to the remote server. */
+        private BindRequest bindRequestTemplate;
+
+        private BindPerformanceRunner(final PerformanceRunnerOptions options)
+                throws ArgumentException {
+            super(options);
+        }
+
+        private void setBindRequestTemplate(final BindRequest bindRequestTemplate) {
+            this.bindRequestTemplate = bindRequestTemplate;
+        }
+
+        @Override
+        WorkerThread newWorkerThread(final Connection connection,
+                final ConnectionFactory connectionFactory) {
+            return new BindWorkerThread(connection, connectionFactory);
+        }
+
+        @Override
+        StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) {
+            return new BindStatsThread(performanceRunner, app, filter != null && baseDN != null);
+        }
+    }
+
+    /**
+     * The main method for AuthRate tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new AuthRate().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+    private BooleanArgument scriptFriendly;
+
+    private AuthRate() {
+        // Nothing to do.
+    }
+
+    /**
+     * Constructor to allow tests.
+     *
+     * @param out
+     *            output stream of console application
+     * @param err
+     *            error stream of console application
+     */
+    AuthRate(PrintStream out, PrintStream err) {
+        super(out, err);
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isScriptFriendly() {
+        return scriptFriendly.isPresent();
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_AUTHRATE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(AuthRate.class.getName(), toolDescription, false, true, 0, 0,
+                        "[filter format string] [attributes ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_AUTHRATE.get());
+        argParser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RATE_TOOLS.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        BindPerformanceRunner runner;
+
+        StringArgument baseDN;
+        MultiChoiceArgument<SearchScope> searchScope;
+        MultiChoiceArgument<DereferenceAliasesPolicy> dereferencePolicy;
+        BooleanArgument showUsage;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+        IntegerArgument invalidCredPercent;
+        try {
+            setDefaultPerfToolProperties();
+            PerformanceRunnerOptions options = new PerformanceRunnerOptions(argParser, this);
+            options.setSupportsRebind(false);
+            options.setSupportsMultipleThreadsPerConnection(false);
+
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            runner = new BindPerformanceRunner(options);
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+
+            baseDN =
+                    StringArgument.builder(OPTION_LONG_BASEDN)
+                            .shortIdentifier(OPTION_SHORT_BASEDN)
+                            .description(INFO_SEARCHRATE_TOOL_DESCRIPTION_BASEDN.get())
+                            .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            searchScope = searchScopeArgument();
+            argParser.addArgument(searchScope);
+
+            dereferencePolicy =
+                    MultiChoiceArgument.<DereferenceAliasesPolicy>builder("dereferencePolicy")
+                            .shortIdentifier('a')
+                            .description(INFO_SEARCH_DESCRIPTION_DEREFERENCE_POLICY.get())
+                            .allowedValues(DereferenceAliasesPolicy.values())
+                            .defaultValue(DereferenceAliasesPolicy.NEVER)
+                            .valuePlaceholder(INFO_DEREFERENCE_POLICE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            invalidCredPercent =
+                    IntegerArgument.builder("invalidPassword")
+                            .shortIdentifier('I')
+                            .description(LocalizableMessage.raw(
+                                    "Percent of bind operations with simulated invalid password"))
+                            .range(0, 100)
+                            .defaultValue(0)
+                            .valuePlaceholder(LocalizableMessage.raw("{invalidPassword}"))
+                            .buildAndAddToParser(argParser);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            scriptFriendly =
+                    BooleanArgument.builder("scriptFriendly")
+                            .shortIdentifier('S')
+                            .description(INFO_DESCRIPTION_SCRIPT_FRIENDLY.get())
+                            .buildAndAddToParser(argParser);
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            /* If we should just display usage or version information, then print it and exit. */
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            final BindRequest bindRequestTemplate = connectionFactoryProvider.getBindRequest();
+            if (bindRequestTemplate == null) {
+                throw new ArgumentException(ERR_AUTHRATE_NO_BIND_DN_PROVIDED.get());
+            }
+            runner.setBindRequestTemplate(bindRequestTemplate);
+            runner.validate();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final List<String> attributes = new LinkedList<>();
+        final ArrayList<String> filterAndAttributeStrings = argParser.getTrailingArguments();
+        if (!filterAndAttributeStrings.isEmpty()) {
+             /*The list of trailing arguments should be structured as follow:
+             the first trailing argument is considered the filter, the other
+             as attributes.*/
+            runner.filter = filterAndAttributeStrings.remove(0);
+            // The rest are attributes
+            attributes.addAll(filterAndAttributeStrings);
+        }
+        runner.attributes = attributes.toArray(new String[attributes.size()]);
+        runner.baseDN = baseDN.getValue();
+        try {
+            runner.scope = searchScope.getTypedValue();
+            runner.dereferencesAliasesPolicy = dereferencePolicy.getTypedValue();
+            runner.invalidCredPercent = invalidCredPercent.getIntValue();
+        } catch (final ArgumentException ex1) {
+            errPrintln(ex1.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Try it out to make sure the format string and data sources match.
+        final Object[] data = DataSource.generateData(runner.getDataSources(), null);
+        try {
+            if (runner.baseDN != null && runner.filter != null) {
+                String.format(runner.filter, data);
+                String.format(runner.baseDN, data);
+            }
+        } catch (final Exception ex1) {
+            errPrintln(LocalizableMessage.raw("Error formatting filter or base DN: " + ex1));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        return runner.run(connectionFactory);
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/DataSource.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/DataSource.java
new file mode 100644
index 0000000..9532828
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/DataSource.java
@@ -0,0 +1,416 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.tools;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.util.StaticUtils;
+
+import org.forgerock.util.Reject;
+
+/**
+ * A source of data for performance tools.
+ */
+final class DataSource {
+    private static interface IDataSource {
+        IDataSource duplicate();
+
+        Object getData();
+    }
+
+    private static class IncrementLineFileDataSource implements IDataSource {
+        private final List<String> lines;
+        private int next;
+
+        public IncrementLineFileDataSource(final String file) throws IOException {
+            lines = new ArrayList<>();
+            try (final BufferedReader in = new BufferedReader(new FileReader(file))) {
+                String line;
+                while ((line = in.readLine()) != null) {
+                    lines.add(line);
+                }
+            }
+        }
+
+        private IncrementLineFileDataSource(final List<String> lines) {
+            this.lines = lines;
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            return new IncrementLineFileDataSource(lines);
+        }
+
+        @Override
+        public Object getData() {
+            if (next == lines.size()) {
+                next = 0;
+            }
+
+            return lines.get(next++);
+        }
+
+        public static LocalizableMessage getUsage() {
+            return LocalizableMessage
+                    .raw("\"inc({filename})\" Consecutive, incremental line from file");
+        }
+    }
+
+    private static class IncrementNumberDataSource implements IDataSource {
+        private final int low;
+        private int next;
+        private final int high;
+
+        public IncrementNumberDataSource(final int low, final int high) {
+            this.low = this.next = low;
+            this.high = high;
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            return new IncrementNumberDataSource(low, high);
+        }
+
+        @Override
+        public Object getData() {
+            if (next == high) {
+                next = low;
+                return high;
+            }
+
+            return next++;
+        }
+
+        public static LocalizableMessage getUsage() {
+            return LocalizableMessage.raw("\"inc({min},{max})\" Consecutive, incremental number");
+        }
+    }
+
+    private static class RandomLineFileDataSource implements IDataSource {
+        private final List<String> lines;
+        private final Random random;
+
+        public RandomLineFileDataSource(final long seed, final String file) throws IOException {
+            lines = new ArrayList<>();
+            random = new Random(seed);
+            try (final BufferedReader in = new BufferedReader(new FileReader(file))) {
+                String line;
+                while ((line = in.readLine()) != null) {
+                    lines.add(line);
+                }
+            }
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            return this;
+        }
+
+        @Override
+        public Object getData() {
+            return lines.get(random.nextInt(lines.size()));
+        }
+
+        public static LocalizableMessage getUsage() {
+            return LocalizableMessage.raw("\"rand({filename})\" Random line from file");
+        }
+    }
+
+    private static class RandomNumberDataSource implements IDataSource {
+        private final Random random;
+        private final int offset;
+        private final int range;
+
+        public RandomNumberDataSource(final long seed, final int low, final int high) {
+            random = new Random(seed);
+            offset = low;
+            range = high - low;
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            // There is no state info so threads can just share one instance.
+            return this;
+        }
+
+        @Override
+        public Object getData() {
+            return random.nextInt(range) + offset;
+        }
+
+        public static LocalizableMessage getUsage() {
+            return LocalizableMessage.raw("\"rand({min},{max})\" Random number");
+        }
+    }
+
+    private static final class RandomStringDataSource implements IDataSource {
+        private final Random random;
+        private final int length;
+        private final Character[] charSet;
+
+        private RandomStringDataSource(final int seed, final int length, final String charSet) {
+            this.length = length;
+            final Set<Character> chars = new HashSet<>();
+            for (int i = 0; i < charSet.length(); i++) {
+                final char c = charSet.charAt(i);
+                if (c == '[') {
+                    i += 1;
+                    final char start = charSet.charAt(i);
+                    i += 2;
+                    final char end = charSet.charAt(i);
+                    i += 1;
+                    for (int j = start; j <= end; j++) {
+                        chars.add((char) j);
+                    }
+                } else {
+                    chars.add(c);
+                }
+            }
+            this.charSet = chars.toArray(new Character[chars.size()]);
+            this.random = new Random(seed);
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            return this;
+        }
+
+        @Override
+        public Object getData() {
+            final char[] str = new char[length];
+            for (int i = 0; i < length; i++) {
+                str[i] = charSet[random.nextInt(charSet.length)];
+            }
+            return new String(str);
+        }
+
+        public static LocalizableMessage getUsage() {
+            return LocalizableMessage
+                    .raw("\"randstr({length},_charSet_)\" Random string of specified "
+                            + "length and optionally from characters in "
+                            + "the charSet string. A range of character "
+                            + "can be specified with [start-end] charSet notation. "
+                            + "If no charSet is specified, the default charSet of "
+                            + "[A-Z][a-z][0-9] will be used");
+        }
+    }
+
+    private static final class StaticDataSource implements IDataSource {
+        private final Object data;
+
+        private StaticDataSource(final Object data) {
+            this.data = data;
+        }
+
+        @Override
+        public IDataSource duplicate() {
+            // There is no state info so threads can just share one instance.
+            return this;
+        }
+
+        @Override
+        public Object getData() {
+            return data;
+        }
+    }
+
+    /**
+     * Returns Generated data from the specified data sources. Generated data
+     * will be placed in the specified data array. If the data array is null or
+     * smaller than the number of data sources, one will be allocated.
+     *
+     * @param dataSources
+     *            Data sources that will generate arguments referenced by the
+     *            format specifiers in the format string.
+     * @param data
+     *            The array where generated data will be placed to format the
+     *            string.
+     * @return A formatted string
+     */
+    public static Object[] generateData(final DataSource[] dataSources, Object[] data) {
+        if (data == null || data.length < dataSources.length) {
+            data = new Object[dataSources.length];
+        }
+        for (int i = 0; i < dataSources.length; i++) {
+            data[i] = dataSources[i].getData();
+        }
+        return data;
+    }
+
+    /**
+     * Parses a list of source definitions into an array of data source objects.
+     * A data source is defined as follows: - rand({min},{max}) generates a
+     * random integer between the min and max. - rand({filename}) retrieves a
+     * random line from a file. - inc({min},{max}) returns incremental integer
+     * between the min and max. - inc({filename}) retrieves lines in order from
+     * a file. - {number} always return the integer as given. - {string} always
+     * return the string as given.
+     *
+     * @param sources
+     *            The list of source definitions to parse.
+     * @return The array of parsed data sources.
+     * @throws ArgumentException
+     *             If an exception occurs while parsing.
+     */
+    public static DataSource[] parse(final List<String> sources) throws ArgumentException {
+        Reject.ifNull(sources);
+        final DataSource[] dataSources = new DataSource[sources.size()];
+        for (int i = 0; i < sources.size(); i++) {
+            final String dataSourceDef = sources.get(i);
+            if (dataSourceDef.startsWith("rand(") && dataSourceDef.endsWith(")")) {
+                final int lparenPos = dataSourceDef.indexOf("(");
+                final int commaPos = dataSourceDef.indexOf(",");
+                final int rparenPos = dataSourceDef.indexOf(")");
+
+                if (commaPos < 0) {
+                    try {
+                        // This is a file name
+                        dataSources[i] =
+                                new DataSource(new RandomLineFileDataSource(0, dataSourceDef
+                                        .substring(lparenPos + 1, rparenPos)));
+                    } catch (IOException ioe) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error opening file %s: %s", dataSourceDef.substring(lparenPos + 1,
+                                        rparenPos), ioe.getMessage()), ioe);
+                    } catch (Exception e) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error parsing value generator: %s", e.getMessage()), e);
+                    }
+                } else {
+                    try {
+                        // This range of integers
+                        final int low =
+                                Integer.parseInt(dataSourceDef.substring(lparenPos + 1, commaPos));
+                        final int high =
+                                Integer.parseInt(dataSourceDef.substring(commaPos + 1, rparenPos));
+                        dataSources[i] =
+                                new DataSource(new RandomNumberDataSource(Thread.currentThread()
+                                        .getId(), low, high));
+                    } catch (Exception e) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error parsing value generator: %s", e.getMessage()), e);
+                    }
+                }
+
+            } else if (dataSourceDef.startsWith("randstr(") && dataSourceDef.endsWith(")")) {
+                final int lparenPos = dataSourceDef.indexOf("(");
+                final int commaPos = dataSourceDef.indexOf(",");
+                final int rparenPos = dataSourceDef.indexOf(")");
+                int length;
+                String charSet;
+                try {
+                    if (commaPos < 0) {
+                        length =
+                                Integer.parseInt(dataSourceDef.substring(lparenPos + 1, rparenPos));
+                        charSet = "[A-Z][a-z][0-9]";
+                    } else {
+                        // length and charSet
+                        length = Integer.parseInt(dataSourceDef.substring(lparenPos + 1, commaPos));
+                        charSet = dataSourceDef.substring(commaPos + 1, rparenPos);
+                    }
+                    dataSources[i] = new DataSource(new RandomStringDataSource(0, length, charSet));
+                } catch (Exception e) {
+                    throw new ArgumentException(LocalizableMessage.raw(
+                            "Error parsing value generator: %s", e.getMessage()), e);
+                }
+            } else if (dataSourceDef.startsWith("inc(") && dataSourceDef.endsWith(")")) {
+                final int lparenPos = dataSourceDef.indexOf("(");
+                final int commaPos = dataSourceDef.indexOf(",");
+                final int rparenPos = dataSourceDef.indexOf(")");
+                if (commaPos < 0) {
+                    try {
+                        // This is a file name
+                        dataSources[i] =
+                                new DataSource(new IncrementLineFileDataSource(dataSourceDef
+                                        .substring(lparenPos + 1, rparenPos)));
+                    } catch (IOException ioe) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error opening file %s: %s", dataSourceDef.substring(lparenPos + 1,
+                                        rparenPos), ioe.getMessage()), ioe);
+                    } catch (Exception e) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error parsing value generator: %s", e.getMessage()), e);
+                    }
+                } else {
+                    try {
+                        final int low =
+                                Integer.parseInt(dataSourceDef.substring(lparenPos + 1, commaPos));
+                        final int high =
+                                Integer.parseInt(dataSourceDef.substring(commaPos + 1, rparenPos));
+                        dataSources[i] = new DataSource(new IncrementNumberDataSource(low, high));
+                    } catch (Exception e) {
+                        throw new ArgumentException(LocalizableMessage.raw(
+                                "Error parsing value generator: %s", e.getMessage()), e);
+                    }
+                }
+            } else {
+                try {
+                    dataSources[i] =
+                            new DataSource(new StaticDataSource(Integer.parseInt(dataSourceDef)));
+                } catch (final NumberFormatException nfe) {
+                    dataSources[i] = new DataSource(new StaticDataSource(dataSourceDef));
+                }
+            }
+        }
+
+        return dataSources;
+    }
+
+    public static LocalizableMessage getUsage() {
+        StringBuilder builder = new StringBuilder();
+        builder.append(IncrementLineFileDataSource.getUsage());
+        builder.append(StaticUtils.EOL);
+        builder.append(IncrementNumberDataSource.getUsage());
+        builder.append(StaticUtils.EOL);
+        builder.append(RandomLineFileDataSource.getUsage());
+        builder.append(StaticUtils.EOL);
+        builder.append(RandomNumberDataSource.getUsage());
+        builder.append(StaticUtils.EOL);
+        builder.append(RandomStringDataSource.getUsage());
+        return LocalizableMessage.raw(builder.toString());
+    }
+
+    private final IDataSource impl;
+
+    private DataSource(final IDataSource impl) {
+        this.impl = impl;
+    }
+
+    public DataSource duplicate() {
+        final IDataSource dup = impl.duplicate();
+        if (dup == impl) {
+            return this;
+        } else {
+            return new DataSource(dup);
+        }
+    }
+
+    public Object getData() {
+        return impl.getData();
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java
new file mode 100644
index 0000000..b5ac74c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java
@@ -0,0 +1,387 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.cli.Utils.readBytesFromFile;
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.Result;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/** A tool that can be used to issue Compare requests to the Directory Server. */
+public final class LDAPCompare extends ConsoleApplication {
+    /**
+     * The main method for LDAPModify tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDAPCompare().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+
+    private LDAPCompare() {
+        // Nothing to do.
+    }
+
+    /**
+     * Constructor to allow tests.
+     *
+     * @param out output stream of console application
+     * @param err error stream of console application
+     */
+    LDAPCompare(PrintStream out, PrintStream err) {
+      super(out, err);
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    private int executeCompare(final CompareRequest request, final Connection connection) {
+        println(INFO_PROCESSING_COMPARE_OPERATION.get(request.getAttributeDescription().toString(),
+                request.getAssertionValueAsString(), request.getName().toString()));
+        if (connection != null) {
+            try {
+                Result result = connection.compare(request);
+                if (result.getResultCode() == ResultCode.COMPARE_FALSE) {
+                    println(INFO_COMPARE_OPERATION_RESULT_FALSE.get(request.getName().toString()));
+                } else {
+                    println(INFO_COMPARE_OPERATION_RESULT_TRUE.get(request.getName().toString()));
+                }
+            } catch (final LdapException ere) {
+                final LocalizableMessage msg = INFO_OPERATION_FAILED.get("COMPARE");
+                errPrintln(msg);
+                final Result r = ere.getResult();
+                errPrintln(ERR_TOOL_RESULT_CODE.get(r.getResultCode().intValue(), r.getResultCode()
+                        .toString()));
+                if (r.getDiagnosticMessage() != null && r.getDiagnosticMessage().length() > 0) {
+                    errPrintln(LocalizableMessage.raw(r.getDiagnosticMessage()));
+                }
+                if (r.getMatchedDN() != null && r.getMatchedDN().length() > 0) {
+                    errPrintln(ERR_TOOL_MATCHED_DN.get(r.getMatchedDN()));
+                }
+                return r.getResultCode().intValue();
+            }
+        }
+        return ResultCode.SUCCESS.intValue();
+    }
+
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDAPCOMPARE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser = new ArgumentParser(
+            LDAPCompare.class.getName(), toolDescription, false, true, 1, 0, "attribute:value [DN ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDAPCOMPARE.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
+
+        BooleanArgument continueOnError;
+        BooleanArgument noop;
+        BooleanArgument showUsage;
+        IntegerArgument version;
+        StringArgument assertionFilter;
+        StringArgument controlStr;
+        StringArgument encodingStr;
+        StringArgument filename;
+        StringArgument proxyAuthzID;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+        try {
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            filename = filenameArgument(INFO_LDAPMODIFY_DESCRIPTION_FILENAME.get());
+            argParser.addArgument(filename);
+
+            proxyAuthzID = proxyAuthIdArgument();
+            argParser.addArgument(proxyAuthzID);
+
+            assertionFilter =
+                    StringArgument.builder(OPTION_LONG_ASSERTION_FILE)
+                            .description(INFO_DESCRIPTION_ASSERTION_FILTER.get())
+                            .valuePlaceholder(INFO_ASSERTION_FILTER_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            controlStr = controlArgument();
+            argParser.addArgument(controlStr);
+
+            version = ldapVersionArgument();
+            argParser.addArgument(version);
+
+            encodingStr = encodingArgument();
+            argParser.addArgument(encodingStr);
+
+            continueOnError = continueOnErrorArgument();
+            argParser.addArgument(continueOnError);
+
+            noop = noOpArgument();
+            argParser.addArgument(noop);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information, then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        try {
+            final int versionNumber = version.getIntValue();
+            if (versionNumber != 2 && versionNumber != 3) {
+                argParser.displayMessageAndUsageReference(
+                    getErrStream(), ERR_DESCRIPTION_INVALID_VERSION.get(String.valueOf(versionNumber)));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(
+                getErrStream(), ERR_DESCRIPTION_INVALID_VERSION.get(version.getValue()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final ArrayList<String> dnStrings = new ArrayList<>();
+        final ArrayList<String> attrAndDNStrings = argParser.getTrailingArguments();
+
+        if (attrAndDNStrings.isEmpty()) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_LDAPCOMPARE_NO_ATTR.get());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // First element should be an attribute string.
+        final String attributeString = attrAndDNStrings.remove(0);
+        // Rest are DN strings
+        dnStrings.addAll(attrAndDNStrings);
+
+        // If no DNs were provided, then exit with an error.
+        if (dnStrings.isEmpty() && !filename.isPresent()) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_LDAPCOMPARE_NO_DNS.get());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        /* If trailing DNs were provided and the filename argument was also
+         provided, exit with an error.*/
+        if (!dnStrings.isEmpty() && filename.isPresent()) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_LDAPCOMPARE_FILENAME_AND_DNS.get());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // parse the attribute string
+        final int idx = attributeString.indexOf(":");
+        if (idx == -1) {
+            argParser.displayMessageAndUsageReference(
+                getErrStream(), ERR_LDAPCOMPARE_INVALID_ATTR_STRING.get(attributeString));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+        final String attributeType = attributeString.substring(0, idx);
+        ByteString attributeVal;
+        final String remainder = attributeString.substring(idx + 1, attributeString.length());
+        if (remainder.length() > 0) {
+            final char nextChar = remainder.charAt(0);
+            if (nextChar == ':') {
+                final String base64 = remainder.substring(1, remainder.length());
+                try {
+                    attributeVal = ByteString.valueOfBase64(base64);
+                } catch (final LocalizedIllegalArgumentException e) {
+                    errPrintln(INFO_COMPARE_CANNOT_BASE64_DECODE_ASSERTION_VALUE.get());
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            } else if (nextChar == '<') {
+                try {
+                    final String filePath = remainder.substring(1, remainder.length());
+                    attributeVal = ByteString.wrap(readBytesFromFile(filePath));
+                } catch (final Exception e) {
+                    errPrintln(INFO_COMPARE_CANNOT_READ_ASSERTION_VALUE_FROM_FILE.get(String
+                            .valueOf(e)));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            } else {
+                attributeVal = ByteString.valueOfUtf8(remainder);
+            }
+        } else {
+            attributeVal = ByteString.valueOfUtf8(remainder);
+        }
+
+        final CompareRequest compare = Requests.newCompareRequest("", attributeType, attributeVal);
+
+        if (controlStr.isPresent()) {
+            for (final String ctrlString : controlStr.getValues()) {
+                try {
+                    final Control ctrl = Utils.getControl(ctrlString);
+                    compare.addControl(ctrl);
+                } catch (final DecodeException de) {
+                    errPrintln(ERR_TOOL_INVALID_CONTROL_STRING.get(ctrlString));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+        }
+
+        if (proxyAuthzID.isPresent()) {
+            final Control proxyControl =
+                    ProxiedAuthV2RequestControl.newControl(proxyAuthzID.getValue());
+            compare.addControl(proxyControl);
+        }
+
+        if (assertionFilter.isPresent()) {
+            final String filterString = assertionFilter.getValue();
+            Filter filter;
+            try {
+                filter = Filter.valueOf(filterString);
+
+                // FIXME -- Change this to the correct OID when the official one
+                // is assigned.
+                final Control assertionControl = AssertionRequestControl.newControl(true, filter);
+                compare.addControl(assertionControl);
+            } catch (final LocalizedIllegalArgumentException le) {
+                errPrintln(ERR_LDAP_ASSERTION_INVALID_FILTER.get(le.getMessage()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        BufferedReader rdr = null;
+        if (!filename.isPresent() && dnStrings.isEmpty()) {
+            // Read from stdin.
+            rdr = new BufferedReader(new InputStreamReader(System.in));
+        } else if (filename.isPresent()) {
+            try {
+                rdr = new BufferedReader(new FileReader(filename.getValue()));
+            } catch (final FileNotFoundException t) {
+                errPrintln(ERR_LDAPCOMPARE_ERROR_READING_FILE.get(filename.getValue(), t.toString()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        Connection connection = null;
+        try {
+            if (!noop.isPresent()) {
+                try {
+                    connection = connectionFactory.getConnection();
+                    if (bindRequest != null) {
+                        printPasswordPolicyResults(this, connection.bind(bindRequest));
+                    }
+                } catch (final LdapException ere) {
+                    return printErrorMessage(this, ere);
+                }
+            }
+
+            int result;
+            if (rdr == null) {
+                for (final String dn : dnStrings) {
+                    compare.setName(dn);
+                    result = executeCompare(compare, connection);
+                    if (result != 0 && !continueOnError.isPresent()) {
+                        return result;
+                    }
+                }
+            } else {
+                String dn;
+                try {
+                    while ((dn = rdr.readLine()) != null) {
+                        compare.setName(dn);
+                        result = executeCompare(compare, connection);
+                        if (result != 0 && !continueOnError.isPresent()) {
+                            return result;
+                        }
+                    }
+                } catch (final IOException ioe) {
+                    errPrintln(ERR_LDAPCOMPARE_ERROR_READING_FILE.get(filename.getValue(), ioe
+                            .toString()));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+        } finally {
+            closeSilently(connection, rdr);
+        }
+
+        return 0;
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java
new file mode 100644
index 0000000..8ad5c11
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java
@@ -0,0 +1,463 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.opendj.ldif.ChangeRecordReader;
+import org.forgerock.opendj.ldif.ChangeRecordVisitor;
+import org.forgerock.opendj.ldif.EntryWriter;
+import org.forgerock.opendj.ldif.LDIFChangeRecordReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A tool that can be used to issue update (Add/Delete/Modify/ModifyDN) requests
+ * to the Directory Server.
+ */
+public final class LDAPModify extends ConsoleApplication {
+    private class VisitorImpl implements ChangeRecordVisitor<Integer, java.lang.Void> {
+        @Override
+        public Integer visitChangeRecord(final Void aVoid, final AddRequest change) {
+            for (final Control control : controls) {
+                change.addControl(control);
+            }
+            final String opType = "ADD";
+            println(INFO_PROCESSING_OPERATION.get(opType, change.getName().toString()));
+            if (connection != null) {
+                try {
+                    Result r = connection.add(change);
+                    printResult(opType, change.getName().toString(), r);
+                    return r.getResultCode().intValue();
+                } catch (final LdapException ere) {
+                    return printErrorMessage(LDAPModify.this, ere);
+                }
+            }
+            return ResultCode.SUCCESS.intValue();
+        }
+
+        @Override
+        public Integer visitChangeRecord(final Void aVoid, final DeleteRequest change) {
+            for (final Control control : controls) {
+                change.addControl(control);
+            }
+            final String opType = "DELETE";
+            println(INFO_PROCESSING_OPERATION.get(opType, change.getName().toString()));
+            if (connection != null) {
+                try {
+                    Result r = connection.delete(change);
+                    printResult(opType, change.getName().toString(), r);
+                    return r.getResultCode().intValue();
+                } catch (final LdapException ere) {
+                    return printErrorMessage(LDAPModify.this, ere);
+                }
+            }
+            return ResultCode.SUCCESS.intValue();
+        }
+
+        @Override
+        public Integer visitChangeRecord(final Void aVoid, final ModifyDNRequest change) {
+            for (final Control control : controls) {
+                change.addControl(control);
+            }
+            final String opType = "MODIFY DN";
+            println(INFO_PROCESSING_OPERATION.get(opType, change.getName().toString()));
+            if (connection != null) {
+                try {
+                    Result r = connection.modifyDN(change);
+                    printResult(opType, change.getName().toString(), r);
+                    return r.getResultCode().intValue();
+                } catch (final LdapException ere) {
+                    return printErrorMessage(LDAPModify.this, ere);
+                }
+            }
+            return ResultCode.SUCCESS.intValue();
+        }
+
+        @Override
+        public Integer visitChangeRecord(final Void aVoid, final ModifyRequest change) {
+            for (final Control control : controls) {
+                change.addControl(control);
+            }
+            final String opType = "MODIFY";
+            println(INFO_PROCESSING_OPERATION.get(opType, change.getName().toString()));
+            if (connection != null) {
+                try {
+                    Result r = connection.modify(change);
+                    printResult(opType, change.getName().toString(), r);
+                    return r.getResultCode().intValue();
+                } catch (final LdapException ere) {
+                    return printErrorMessage(LDAPModify.this, ere);
+                }
+            }
+            return ResultCode.SUCCESS.intValue();
+        }
+
+        private void printResult(final String operationType, final String name, final Result r) {
+            if (r.getResultCode() != ResultCode.SUCCESS && r.getResultCode() != ResultCode.REFERRAL) {
+                final LocalizableMessage msg = INFO_OPERATION_FAILED.get(operationType);
+                errPrintln(msg);
+                errPrintln(ERR_TOOL_RESULT_CODE.get(r.getResultCode().intValue(), r.getResultCode()));
+                if (r.getDiagnosticMessage() != null && r.getDiagnosticMessage().length() > 0) {
+                    errPrintln(LocalizableMessage.raw(r.getDiagnosticMessage()));
+                }
+                if (r.getMatchedDN() != null && r.getMatchedDN().length() > 0) {
+                    errPrintln(ERR_TOOL_MATCHED_DN.get(r.getMatchedDN()));
+                }
+            } else {
+                println(INFO_OPERATION_SUCCESSFUL.get(operationType, name));
+                if (r.getDiagnosticMessage() != null && r.getDiagnosticMessage().length() > 0) {
+                    errPrintln(LocalizableMessage.raw(r.getDiagnosticMessage()));
+                }
+                if (r.getReferralURIs() != null) {
+                    for (final String uri : r.getReferralURIs()) {
+                        println(LocalizableMessage.raw(uri));
+                    }
+                }
+            }
+
+            try {
+                final PreReadResponseControl control =
+                        r.getControl(PreReadResponseControl.DECODER, new DecodeOptions());
+                if (control != null) {
+                    println(INFO_LDAPMODIFY_PREREAD_ENTRY.get());
+                    writer.writeEntry(control.getEntry());
+                }
+            } catch (final DecodeException de) {
+                errPrintln(ERR_DECODE_CONTROL_FAILURE.get(de.getLocalizedMessage()));
+            } catch (final IOException ioe) {
+                throw new RuntimeException(ioe);
+            }
+
+            try {
+                final PostReadResponseControl control =
+                        r.getControl(PostReadResponseControl.DECODER, new DecodeOptions());
+                if (control != null) {
+                    println(INFO_LDAPMODIFY_POSTREAD_ENTRY.get());
+                    writer.writeEntry(control.getEntry());
+                }
+            } catch (final DecodeException de) {
+                errPrintln(ERR_DECODE_CONTROL_FAILURE.get(de.getLocalizedMessage()));
+            } catch (final IOException ioe) {
+                throw new RuntimeException(ioe);
+            }
+
+            // TODO: CSN control
+        }
+    }
+
+    /**
+     * The main method for LDAPModify tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDAPModify().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private Connection connection;
+
+    private EntryWriter writer;
+
+    private Collection<Control> controls;
+
+    private BooleanArgument verbose;
+
+    private LDAPModify() {
+        // Nothing to do.
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDAPMODIFY_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(LDAPModify.class.getName(), toolDescription, false);
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDAPMODIFY.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
+
+        BooleanArgument continueOnError;
+        BooleanArgument noop;
+        BooleanArgument showUsage;
+        IntegerArgument version;
+        StringArgument assertionFilter;
+        StringArgument controlStr;
+        StringArgument filename;
+        StringArgument postReadAttributes;
+        StringArgument preReadAttributes;
+        StringArgument proxyAuthzID;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+
+        try {
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            filename =
+                    StringArgument.builder(OPTION_LONG_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_FILENAME)
+                            .description(INFO_LDAPMODIFY_DESCRIPTION_FILENAME.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            proxyAuthzID =
+                    StringArgument.builder(OPTION_LONG_PROXYAUTHID)
+                            .shortIdentifier(OPTION_SHORT_PROXYAUTHID)
+                            .description(INFO_DESCRIPTION_PROXY_AUTHZID.get())
+                            .valuePlaceholder(INFO_PROXYAUTHID_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            assertionFilter =
+                    StringArgument.builder(OPTION_LONG_ASSERTION_FILE)
+                            .description(INFO_DESCRIPTION_ASSERTION_FILTER.get())
+                            .valuePlaceholder(INFO_ASSERTION_FILTER_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            preReadAttributes =
+                    StringArgument.builder("preReadAttributes")
+                            .description(INFO_DESCRIPTION_PREREAD_ATTRS.get())
+                            .valuePlaceholder(INFO_ATTRIBUTE_LIST_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            postReadAttributes =
+                    StringArgument.builder("postReadAttributes")
+                            .description(INFO_DESCRIPTION_POSTREAD_ATTRS.get())
+                            .valuePlaceholder(INFO_ATTRIBUTE_LIST_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            controlStr =
+                    StringArgument.builder("control")
+                            .shortIdentifier('J')
+                            .description(INFO_DESCRIPTION_CONTROLS.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_LDAP_CONTROL_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            version = ldapVersionArgument();
+            argParser.addArgument(version);
+
+            continueOnError = continueOnErrorArgument();
+            argParser.addArgument(continueOnError);
+
+            noop = noOpArgument();
+            argParser.addArgument(noop);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information, then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        try {
+            final int versionNumber = version.getIntValue();
+            if (versionNumber != 2 && versionNumber != 3) {
+                errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(String.valueOf(versionNumber)));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(version.getValue()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        controls = new LinkedList<>();
+        if (controlStr.isPresent()) {
+            for (final String ctrlString : controlStr.getValues()) {
+                try {
+                    final Control ctrl = Utils.getControl(ctrlString);
+                    controls.add(ctrl);
+                } catch (final DecodeException de) {
+                    errPrintln(ERR_TOOL_INVALID_CONTROL_STRING.get(ctrlString));
+                    ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+        }
+
+        if (proxyAuthzID.isPresent()) {
+            final Control proxyControl =
+                    ProxiedAuthV2RequestControl.newControl(proxyAuthzID.getValue());
+            controls.add(proxyControl);
+        }
+
+        if (assertionFilter.isPresent()) {
+            final String filterString = assertionFilter.getValue();
+            Filter filter;
+            try {
+                filter = Filter.valueOf(filterString);
+
+                // FIXME -- Change this to the correct OID when the official one
+                // is assigned.
+                final Control assertionControl = AssertionRequestControl.newControl(true, filter);
+                controls.add(assertionControl);
+            } catch (final LocalizedIllegalArgumentException le) {
+                errPrintln(ERR_LDAP_ASSERTION_INVALID_FILTER.get(le.getMessage()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        if (preReadAttributes.isPresent()) {
+            final String valueStr = preReadAttributes.getValue();
+            final StringTokenizer tokenizer = new StringTokenizer(valueStr, ", ");
+            final List<String> attributes = new LinkedList<>();
+            while (tokenizer.hasMoreTokens()) {
+                attributes.add(tokenizer.nextToken());
+            }
+            controls.add(PreReadRequestControl.newControl(true, attributes));
+        }
+
+        if (postReadAttributes.isPresent()) {
+            final String valueStr = postReadAttributes.getValue();
+            final StringTokenizer tokenizer = new StringTokenizer(valueStr, ", ");
+            final List<String> attributes = new LinkedList<>();
+            while (tokenizer.hasMoreTokens()) {
+                attributes.add(tokenizer.nextToken());
+            }
+            final PostReadRequestControl control =
+                    PostReadRequestControl.newControl(true, attributes);
+            controls.add(control);
+        }
+
+        writer = new LDIFEntryWriter(getOutputStream());
+        final VisitorImpl visitor = new VisitorImpl();
+        ChangeRecordReader reader = null;
+        try {
+            if (!noop.isPresent()) {
+                try {
+                    connection = connectionFactory.getConnection();
+                    if (bindRequest != null) {
+                        printPasswordPolicyResults(this, connection.bind(bindRequest));
+                    }
+                } catch (final LdapException ere) {
+                    return printErrorMessage(this, ere);
+                }
+            }
+
+            if (filename.isPresent()) {
+                try {
+                    reader = new LDIFChangeRecordReader(new FileInputStream(filename.getValue()));
+                } catch (final Exception e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(filename.getValue(), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            } else {
+                reader = new LDIFChangeRecordReader(getInputStream());
+            }
+
+            try {
+                while (reader.hasNext()) {
+                    final ChangeRecord cr = reader.readChangeRecord();
+                    final int result = cr.accept(visitor, null);
+                    if (result != 0 && !continueOnError.isPresent()) {
+                        return result;
+                    }
+                }
+            } catch (final IOException ioe) {
+                errPrintln(ERR_LDIF_FILE_READ_ERROR.get(filename.getValue(), ioe.getLocalizedMessage()));
+                return ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+            }
+        } finally {
+            closeSilently(reader, connection);
+        }
+
+        return ResultCode.SUCCESS.intValue();
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPPasswordModify.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPPasswordModify.java
new file mode 100644
index 0000000..1871364
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPPasswordModify.java
@@ -0,0 +1,288 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.CliMessages.INFO_FILE_PLACEHOLDER;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.FileBasedArgument;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A tool that can be used to issue LDAP password modify extended requests to
+ * the Directory Server. It exposes the three primary options available for this
+ * operation, which are:
+ * <UL>
+ * <LI>The user identity whose password should be changed.</LI>
+ * <LI>The current password for the user.</LI>
+ * <LI>The new password for the user.
+ * </UL>
+ * All of these are optional components that may be included or omitted from the
+ * request.
+ */
+public final class LDAPPasswordModify extends ConsoleApplication {
+    /**
+     * Parses the command-line arguments, establishes a connection to the
+     * Directory Server, sends the password modify request, and reads the
+     * response.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDAPPasswordModify().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+
+    private LDAPPasswordModify() {
+        // Nothing to do.
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    private int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDAPPWMOD_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(LDAPPasswordModify.class.getName(), toolDescription, false);
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDAPPASSWORDMODIFY.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+
+        FileBasedArgument currentPWFile;
+        FileBasedArgument newPWFile;
+        BooleanArgument showUsage;
+        IntegerArgument version;
+        StringArgument currentPW;
+        StringArgument controlStr;
+        StringArgument newPW;
+        StringArgument proxyAuthzID;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+
+        try {
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            newPW =
+                    StringArgument.builder("newPassword")
+                            .shortIdentifier('n')
+                            .description(INFO_LDAPPWMOD_DESCRIPTION_NEWPW.get())
+                            .valuePlaceholder(INFO_NEW_PASSWORD_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            newPWFile =
+                    FileBasedArgument.builder("newPasswordFile")
+                            .shortIdentifier('F')
+                            .description(INFO_LDAPPWMOD_DESCRIPTION_NEWPWFILE.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            currentPW =
+                    StringArgument.builder("currentPassword")
+                            .shortIdentifier('c')
+                            .description(INFO_LDAPPWMOD_DESCRIPTION_CURRENTPW.get())
+                            .valuePlaceholder(INFO_CURRENT_PASSWORD_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            currentPWFile =
+                    FileBasedArgument.builder("currentPasswordFile")
+                            .shortIdentifier('C')
+                            .description(INFO_LDAPPWMOD_DESCRIPTION_CURRENTPWFILE.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            proxyAuthzID =
+                    StringArgument.builder("authzID")
+                            .shortIdentifier('a')
+                            .description(INFO_LDAPPWMOD_DESCRIPTION_AUTHZID.get())
+                            .valuePlaceholder(INFO_PROXYAUTHID_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            controlStr =
+                    StringArgument.builder("control")
+                            .shortIdentifier('J')
+                            .description(INFO_DESCRIPTION_CONTROLS.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_LDAP_CONTROL_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            version = ldapVersionArgument();
+            argParser.addArgument(version);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information, then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final PasswordModifyExtendedRequest request = Requests.newPasswordModifyExtendedRequest();
+        try {
+            final int versionNumber = version.getIntValue();
+            if (versionNumber != 2 && versionNumber != 3) {
+                errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(String.valueOf(versionNumber)));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(version.getValue()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        if (controlStr.isPresent()) {
+            for (final String ctrlString : controlStr.getValues()) {
+                try {
+                    final Control ctrl = Utils.getControl(ctrlString);
+                    request.addControl(ctrl);
+                } catch (final DecodeException de) {
+                    errPrintln(ERR_TOOL_INVALID_CONTROL_STRING.get(ctrlString));
+                    ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+        }
+
+        if (newPW.isPresent() && newPWFile.isPresent()) {
+            final LocalizableMessage message =
+                    ERR_LDAPPWMOD_CONFLICTING_ARGS.get(newPW.getLongIdentifier(), newPWFile
+                            .getLongIdentifier());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        if (currentPW.isPresent() && currentPWFile.isPresent()) {
+            final LocalizableMessage message =
+                    ERR_LDAPPWMOD_CONFLICTING_ARGS.get(currentPW.getLongIdentifier(), currentPWFile
+                            .getLongIdentifier());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        Connection connection;
+        try {
+            connection = connectionFactory.getConnection();
+        } catch (final LdapException ere) {
+            return Utils.printErrorMessage(this, ere);
+        }
+
+        if (proxyAuthzID.isPresent()) {
+            request.setUserIdentity(proxyAuthzID.getValue());
+        }
+
+        if (currentPW.isPresent()) {
+            request.setOldPassword(currentPW.getValue().toCharArray());
+        } else if (currentPWFile.isPresent()) {
+            request.setOldPassword(currentPWFile.getValue().toCharArray());
+        }
+
+        if (newPW.isPresent()) {
+            request.setNewPassword(newPW.getValue().toCharArray());
+        } else if (newPWFile.isPresent()) {
+            request.setNewPassword(newPWFile.getValue().toCharArray());
+        }
+
+        PasswordModifyExtendedResult result;
+        try {
+            result = connection.extendedRequest(request);
+        } catch (final LdapException e) {
+            LocalizableMessage message =
+                    ERR_LDAPPWMOD_FAILED.get(e.getResult().getResultCode().intValue(), e
+                            .getResult().getResultCode().toString());
+            errPrintln(message);
+
+            final String errorMessage = e.getResult().getDiagnosticMessage();
+            if (errorMessage != null && errorMessage.length() > 0) {
+                message = ERR_LDAPPWMOD_FAILURE_ERROR_MESSAGE.get(errorMessage);
+                errPrintln(message);
+            }
+
+            final String matchedDN = e.getResult().getMatchedDN();
+            if (matchedDN != null && matchedDN.length() > 0) {
+                message = ERR_LDAPPWMOD_FAILURE_MATCHED_DN.get(matchedDN);
+                errPrintln(message);
+            }
+            return e.getResult().getResultCode().intValue();
+        }
+
+        println(INFO_LDAPPWMOD_SUCCESSFUL.get());
+
+        final String additionalInfo = result.getDiagnosticMessage();
+        if (additionalInfo != null && additionalInfo.length() > 0) {
+            println(INFO_LDAPPWMOD_ADDITIONAL_INFO.get(additionalInfo));
+        }
+
+        if (result.getGeneratedPassword() != null) {
+            println(INFO_LDAPPWMOD_GENERATED_PASSWORD.get(ByteString.valueOfBytes(
+                    result.getGeneratedPassword()).toString()));
+        }
+
+        return 0;
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java
new file mode 100644
index 0000000..0781922
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java
@@ -0,0 +1,842 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.EntryChangeNotificationResponseControl;
+import org.forgerock.opendj.ldap.controls.GetEffectiveRightsRequestControl;
+import org.forgerock.opendj.ldap.controls.MatchedValuesRequestControl;
+import org.forgerock.opendj.ldap.controls.PersistentSearchChangeType;
+import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.controls.ServerSideSortRequestControl;
+import org.forgerock.opendj.ldap.controls.ServerSideSortResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
+import org.forgerock.opendj.ldap.controls.VirtualListViewRequestControl;
+import org.forgerock.opendj.ldap.controls.VirtualListViewResponseControl;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.EntryWriter;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+import com.forgerock.opendj.ldap.controls.AccountUsabilityResponseControl;
+import com.forgerock.opendj.util.StaticUtils;
+
+import static com.forgerock.opendj.cli.CliMessages.INFO_NUM_ENTRIES_PLACEHOLDER;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
+import static org.forgerock.util.Utils.*;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+/** A tool that can be used to issue Search requests to the Directory Server. */
+public final class LDAPSearch extends ConsoleApplication {
+    private class LDAPSearchResultHandler implements SearchResultHandler {
+        private int entryCount;
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            entryCount++;
+
+            try {
+                final EntryChangeNotificationResponseControl control =
+                        entry.getControl(EntryChangeNotificationResponseControl.DECODER,
+                                new DecodeOptions());
+                if (control != null) {
+                    println(INFO_LDAPSEARCH_PSEARCH_CHANGE_TYPE.get(control.getChangeType()
+                            .toString()));
+                    final DN previousDN = control.getPreviousName();
+                    if (previousDN != null) {
+                        println(INFO_LDAPSEARCH_PSEARCH_PREVIOUS_DN.get(previousDN.toString()));
+                    }
+                }
+            } catch (final DecodeException de) {
+                errPrintln(ERR_DECODE_CONTROL_FAILURE.get(de.getLocalizedMessage()));
+            }
+
+            try {
+                final AccountUsabilityResponseControl control =
+                        entry.getControl(AccountUsabilityResponseControl.DECODER,
+                                new DecodeOptions());
+
+                if (control != null) {
+                    println(INFO_LDAPSEARCH_ACCTUSABLE_HEADER.get());
+                    if (control.isUsable()) {
+                        println(INFO_LDAPSEARCH_ACCTUSABLE_IS_USABLE.get());
+                        if (control.getSecondsBeforeExpiration() > 0) {
+                            final int timeToExp = control.getSecondsBeforeExpiration();
+                            final LocalizableMessage timeToExpStr =
+                                    secondsToTimeString(timeToExp);
+
+                            println(INFO_LDAPSEARCH_ACCTUSABLE_TIME_UNTIL_EXPIRATION
+                                    .get(timeToExpStr));
+                        }
+                    } else {
+                        println(INFO_LDAPSEARCH_ACCTUSABLE_NOT_USABLE.get());
+                        if (control.isInactive()) {
+                            println(INFO_LDAPSEARCH_ACCTUSABLE_ACCT_INACTIVE.get());
+                        }
+                        if (control.isReset()) {
+                            println(INFO_LDAPSEARCH_ACCTUSABLE_PW_RESET.get());
+                        }
+                        if (control.isExpired()) {
+                            println(INFO_LDAPSEARCH_ACCTUSABLE_PW_EXPIRED.get());
+
+                            if (control.getRemainingGraceLogins() > 0) {
+                                println(INFO_LDAPSEARCH_ACCTUSABLE_REMAINING_GRACE.get(control
+                                        .getRemainingGraceLogins()));
+                            }
+                        }
+                        if (control.isLocked()) {
+                            println(INFO_LDAPSEARCH_ACCTUSABLE_LOCKED.get());
+                            if (control.getSecondsBeforeUnlock() > 0) {
+                                final int timeToUnlock = control.getSecondsBeforeUnlock();
+                                final LocalizableMessage timeToUnlockStr =
+                                        secondsToTimeString(timeToUnlock);
+
+                                println(INFO_LDAPSEARCH_ACCTUSABLE_TIME_UNTIL_UNLOCK
+                                        .get(timeToUnlockStr));
+                            }
+                        }
+                    }
+                }
+            } catch (final DecodeException de) {
+                errPrintln(ERR_DECODE_CONTROL_FAILURE.get(de.getLocalizedMessage()));
+            }
+
+            try {
+                ldifWriter.writeEntry(entry);
+                ldifWriter.flush();
+            } catch (final IOException ioe) {
+                // Something is seriously wrong
+                throw new RuntimeException(ioe);
+            }
+            return true;
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            println(LocalizableMessage.raw(reference.toString()));
+            return true;
+        }
+    }
+
+    /**
+     * The main method for LDAPSearch tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+
+    public static void main(final String[] args) {
+        final int retCode = new LDAPSearch().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+
+    private EntryWriter ldifWriter;
+
+    private LDAPSearch() {
+        // Nothing to do.
+    }
+
+    /**
+     * Constructor to allow tests.
+     *
+     * @param out output stream of console application
+     * @param err error stream of console application
+     */
+    LDAPSearch(PrintStream out, PrintStream err) {
+        super(out, err);
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    /** Run ldapsearch with provided command-line arguments. */
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDAPSEARCH_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(LDAPSearch.class.getName(), toolDescription, false, true, 0, 0,
+                        "[filter] [attributes ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDAPSEARCH.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
+
+        BooleanArgument countEntries;
+        BooleanArgument dontWrap;
+        BooleanArgument noop;
+        BooleanArgument typesOnly;
+        IntegerArgument simplePageSize;
+        IntegerArgument timeLimit;
+        IntegerArgument version;
+        StringArgument baseDN;
+        StringArgument controlStr;
+        MultiChoiceArgument<DereferenceAliasesPolicy> dereferencePolicy;
+        StringArgument filename;
+        StringArgument matchedValuesFilter;
+        StringArgument pSearchInfo;
+        MultiChoiceArgument<SearchScope> searchScope;
+        StringArgument vlvDescriptor;
+        StringArgument effectiveRightsUser;
+        StringArgument effectiveRightsAttrs;
+        StringArgument sortOrder;
+        StringArgument proxyAuthzID;
+        StringArgument assertionFilter;
+        IntegerArgument sizeLimit;
+        try {
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            final StringArgument propertiesFileArgument =
+                propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            final BooleanArgument noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            baseDN =
+                    StringArgument.builder(OPTION_LONG_BASEDN)
+                            .shortIdentifier(OPTION_SHORT_BASEDN)
+                            .description(INFO_SEARCH_DESCRIPTION_BASEDN.get())
+                            .required()
+                            .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            searchScope = searchScopeArgument();
+            argParser.addArgument(searchScope);
+
+            filename =
+                    StringArgument.builder(OPTION_LONG_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_FILENAME)
+                            .description(INFO_SEARCH_DESCRIPTION_FILENAME.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            proxyAuthzID =
+                    StringArgument.builder(OPTION_LONG_PROXYAUTHID)
+                            .shortIdentifier(OPTION_SHORT_PROXYAUTHID)
+                            .description(INFO_DESCRIPTION_PROXY_AUTHZID.get())
+                            .valuePlaceholder(INFO_PROXYAUTHID_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            pSearchInfo =
+                    StringArgument.builder("persistentSearch")
+                            .shortIdentifier('C')
+                            .description(INFO_DESCRIPTION_PSEARCH_INFO.get())
+                            .docDescriptionSupplement(SUPPLEMENT_DESCRIPTION_PSEARCH_INFO.get())
+                            .valuePlaceholder(INFO_PSEARCH_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            simplePageSize =
+                    IntegerArgument.builder("simplePageSize")
+                            .description(INFO_DESCRIPTION_SIMPLE_PAGE_SIZE.get())
+                            .lowerBound(1)
+                            .defaultValue(1000)
+                            .valuePlaceholder(INFO_NUM_ENTRIES_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            assertionFilter =
+                    StringArgument.builder(OPTION_LONG_ASSERTION_FILE)
+                            .description(INFO_DESCRIPTION_ASSERTION_FILTER.get())
+                            .valuePlaceholder(INFO_ASSERTION_FILTER_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            matchedValuesFilter =
+                    StringArgument.builder("matchedValuesFilter")
+                            .description(INFO_DESCRIPTION_MATCHED_VALUES_FILTER.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_FILTER_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            sortOrder =
+                    StringArgument.builder("sortOrder")
+                            .shortIdentifier('S')
+                            .description(INFO_DESCRIPTION_SORT_ORDER.get())
+                            .valuePlaceholder(INFO_SORT_ORDER_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            vlvDescriptor =
+                    StringArgument.builder("virtualListView")
+                            .shortIdentifier('G')
+                            .description(INFO_DESCRIPTION_VLV.get())
+                            .valuePlaceholder(INFO_VLV_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            controlStr =
+                    StringArgument.builder("control")
+                            .shortIdentifier('J')
+                            .description(INFO_DESCRIPTION_CONTROLS.get())
+                            .docDescriptionSupplement(SUPPLEMENT_DESCRIPTION_CONTROLS.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_LDAP_CONTROL_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            effectiveRightsUser =
+                    StringArgument.builder(OPTION_LONG_EFFECTIVERIGHTSUSER)
+                            .shortIdentifier(OPTION_SHORT_EFFECTIVERIGHTSUSER)
+                            .description(INFO_DESCRIPTION_EFFECTIVERIGHTS_USER.get())
+                            .valuePlaceholder(INFO_PROXYAUTHID_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            effectiveRightsAttrs =
+                    StringArgument.builder(OPTION_LONG_EFFECTIVERIGHTSATTR)
+                            .shortIdentifier(OPTION_SHORT_EFFECTIVERIGHTSATTR)
+                            .description(INFO_DESCRIPTION_EFFECTIVERIGHTS_ATTR.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_ATTRIBUTE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            version = ldapVersionArgument();
+            argParser.addArgument(version);
+
+            dereferencePolicy =
+                    MultiChoiceArgument.<DereferenceAliasesPolicy>builder("dereferencePolicy")
+                            .shortIdentifier('a')
+                            .description(INFO_SEARCH_DESCRIPTION_DEREFERENCE_POLICY.get())
+                            .allowedValues(DereferenceAliasesPolicy.values())
+                            .defaultValue(DereferenceAliasesPolicy.NEVER)
+                            .valuePlaceholder(INFO_DEREFERENCE_POLICE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            typesOnly =
+                    BooleanArgument.builder("typesOnly")
+                            .shortIdentifier('A')
+                            .description(INFO_DESCRIPTION_TYPES_ONLY.get())
+                            .buildAndAddToParser(argParser);
+            sizeLimit =
+                    IntegerArgument.builder("sizeLimit")
+                            .shortIdentifier('z')
+                            .description(INFO_SEARCH_DESCRIPTION_SIZE_LIMIT.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_SIZE_LIMIT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            timeLimit =
+                    IntegerArgument.builder("timeLimit")
+                            .shortIdentifier('l')
+                            .description(INFO_SEARCH_DESCRIPTION_TIME_LIMIT.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_TIME_LIMIT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            dontWrap =
+                    BooleanArgument.builder("dontWrap")
+                            .shortIdentifier('t')
+                            .description(INFO_DESCRIPTION_DONT_WRAP.get())
+                            .buildAndAddToParser(argParser);
+            countEntries =
+                    BooleanArgument.builder("countEntries")
+                            .description(INFO_DESCRIPTION_COUNT_ENTRIES.get())
+                            .buildAndAddToParser(argParser);
+
+            final BooleanArgument continueOnError = continueOnErrorArgument();
+            argParser.addArgument(continueOnError);
+
+            noop = noOpArgument();
+            argParser.addArgument(noop);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            final BooleanArgument showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information,
+            // then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final List<Filter> filters = new LinkedList<>();
+        final List<String> attributes = new LinkedList<>();
+        final ArrayList<String> filterAndAttributeStrings = argParser.getTrailingArguments();
+        if (!filterAndAttributeStrings.isEmpty()) {
+            /* The list of trailing arguments should be structured as follow:
+             - If a filter file is present, trailing arguments are
+             considered as attributes
+             - If filter file is not present, the first trailing argument is
+             considered the filter, the other as attributes.*/
+            if (!filename.isPresent()) {
+                final String filterString = filterAndAttributeStrings.remove(0);
+
+                try {
+                    filters.add(Filter.valueOf(filterString));
+                } catch (final LocalizedIllegalArgumentException e) {
+                    errPrintln(e.getMessageObject());
+                    return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+                }
+            }
+            // The rest are attributes
+            attributes.addAll(filterAndAttributeStrings);
+        }
+
+        if (filename.isPresent()) {
+            // Read the filter strings.
+            try (BufferedReader in = new BufferedReader(new FileReader(filename.getValue()))) {
+                String line = null;
+                while ((line = in.readLine()) != null) {
+                    if ("".equals(line.trim())) {
+                        // ignore empty lines.
+                        continue;
+                    }
+                    filters.add(Filter.valueOf(line));
+                }
+            } catch (final LocalizedIllegalArgumentException e) {
+                errPrintln(e.getMessageObject());
+                return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+            } catch (final IOException e) {
+                errPrintln(LocalizableMessage.raw(e.toString()));
+                return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+            }
+        }
+
+        if (filters.isEmpty()) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_SEARCH_NO_FILTERS.get());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        SearchScope scope;
+        try {
+            scope = searchScope.getTypedValue();
+        } catch (final ArgumentException ex1) {
+            errPrintln(ex1.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        SearchRequest search;
+        try {
+            search =
+                    Requests.newSearchRequest(DN.valueOf(baseDN.getValue()), scope, filters.get(0),
+                            attributes.toArray(new String[attributes.size()]));
+        } catch (final LocalizedIllegalArgumentException e) {
+            errPrintln(e.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Read the LDAP version number.
+        try {
+            final int versionNumber = version.getIntValue();
+            if (versionNumber != 2 && versionNumber != 3) {
+                errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(String.valueOf(versionNumber)));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_DESCRIPTION_INVALID_VERSION.get(version.getValue()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        search.setTypesOnly(typesOnly.isPresent());
+        // searchOptions.setShowOperations(noop.isPresent());
+        // searchOptions.setVerbose(verbose.isPresent());
+        // searchOptions.setContinueOnError(continueOnError.isPresent());
+        // searchOptions.setEncoding(encodingStr.getValue());
+        // searchOptions.setCountMatchingEntries(countEntries.isPresent());
+        try {
+            search.setTimeLimit(timeLimit.getIntValue());
+            search.setSizeLimit(sizeLimit.getIntValue());
+        } catch (final ArgumentException ex1) {
+            errPrintln(ex1.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+        try {
+            search.setDereferenceAliasesPolicy(dereferencePolicy.getTypedValue());
+        } catch (final ArgumentException ex1) {
+            errPrintln(ex1.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        if (controlStr.isPresent()) {
+            for (final String ctrlString : controlStr.getValues()) {
+                try {
+                    final Control ctrl = Utils.getControl(ctrlString);
+                    search.addControl(ctrl);
+                } catch (final DecodeException de) {
+                    errPrintln(ERR_TOOL_INVALID_CONTROL_STRING.get(ctrlString));
+                    ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+        }
+
+        if (effectiveRightsUser.isPresent()) {
+            final String authzID = effectiveRightsUser.getValue();
+            if (!authzID.startsWith("dn:")) {
+                errPrintln(ERR_EFFECTIVERIGHTS_INVALID_AUTHZID.get(authzID));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+            final Control effectiveRightsControl =
+                    GetEffectiveRightsRequestControl.newControl(false, authzID.substring(3),
+                            effectiveRightsAttrs.getValues().toArray(
+                                    new String[effectiveRightsAttrs.getValues().size()]));
+            search.addControl(effectiveRightsControl);
+        }
+
+        if (proxyAuthzID.isPresent()) {
+            final Control proxyControl =
+                    ProxiedAuthV2RequestControl.newControl(proxyAuthzID.getValue());
+            search.addControl(proxyControl);
+        }
+
+        if (pSearchInfo.isPresent()) {
+            final String infoString = StaticUtils.toLowerCase(pSearchInfo.getValue().trim());
+            boolean changesOnly = true;
+            boolean returnECs = true;
+
+            final StringTokenizer tokenizer = new StringTokenizer(infoString, ":");
+
+            if (!tokenizer.hasMoreTokens()) {
+                errPrintln(ERR_PSEARCH_MISSING_DESCRIPTOR.get());
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            } else {
+                final String token = tokenizer.nextToken();
+                if (!"ps".equals(token)) {
+                    errPrintln(ERR_PSEARCH_DOESNT_START_WITH_PS.get(infoString));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            final ArrayList<PersistentSearchChangeType> ct = new ArrayList<>(4);
+            if (tokenizer.hasMoreTokens()) {
+                final StringTokenizer st = new StringTokenizer(tokenizer.nextToken(), ", ");
+                if (!st.hasMoreTokens()) {
+                    ct.add(PersistentSearchChangeType.ADD);
+                    ct.add(PersistentSearchChangeType.DELETE);
+                    ct.add(PersistentSearchChangeType.MODIFY);
+                    ct.add(PersistentSearchChangeType.MODIFY_DN);
+                } else {
+                    do {
+                        final String token = st.nextToken();
+                        if ("add".equals(token)) {
+                            ct.add(PersistentSearchChangeType.ADD);
+                        } else if ("delete".equals(token) || "del".equals(token)) {
+                            ct.add(PersistentSearchChangeType.DELETE);
+                        } else if ("modify".equals(token) || "mod".equals(token)) {
+                            ct.add(PersistentSearchChangeType.MODIFY);
+                        } else if ("modifydn".equals(token) || "moddn".equals(token)
+                                || "modrdn".equals(token)) {
+                            ct.add(PersistentSearchChangeType.MODIFY_DN);
+                        } else if ("any".equals(token) || "all".equals(token)) {
+                            ct.add(PersistentSearchChangeType.ADD);
+                            ct.add(PersistentSearchChangeType.DELETE);
+                            ct.add(PersistentSearchChangeType.MODIFY);
+                            ct.add(PersistentSearchChangeType.MODIFY_DN);
+                        } else {
+                            errPrintln(ERR_PSEARCH_INVALID_CHANGE_TYPE.get(token));
+                            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                        }
+                    } while (st.hasMoreTokens());
+                }
+            }
+
+            if (tokenizer.hasMoreTokens()) {
+                final String token = tokenizer.nextToken();
+                if ("1".equals(token) || "true".equals(token) || "yes".equals(token)) {
+                    changesOnly = true;
+                } else if ("0".equals(token) || "false".equals(token) || "no".equals(token)) {
+                    changesOnly = false;
+                } else {
+                    errPrintln(ERR_PSEARCH_INVALID_CHANGESONLY.get(token));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            if (tokenizer.hasMoreTokens()) {
+                final String token = tokenizer.nextToken();
+                if ("1".equals(token) || "true".equals(token) || "yes".equals(token)) {
+                    returnECs = true;
+                } else if ("0".equals(token) || "false".equals(token) || "no".equals(token)) {
+                    returnECs = false;
+                } else {
+                    errPrintln(ERR_PSEARCH_INVALID_RETURN_ECS.get(token));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            final PersistentSearchRequestControl psearchControl =
+                    PersistentSearchRequestControl.newControl(true, changesOnly, returnECs, ct
+                            .toArray(new PersistentSearchChangeType[ct.size()]));
+            search.addControl(psearchControl);
+        }
+
+        if (assertionFilter.isPresent()) {
+            final String filterString = assertionFilter.getValue();
+            Filter filter;
+            try {
+                filter = Filter.valueOf(filterString);
+
+                // FIXME -- Change this to the correct OID when the official one
+                // is assigned.
+                final Control assertionControl = AssertionRequestControl.newControl(true, filter);
+                search.addControl(assertionControl);
+            } catch (final LocalizedIllegalArgumentException le) {
+                errPrintln(ERR_LDAP_ASSERTION_INVALID_FILTER.get(le.getMessage()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        if (matchedValuesFilter.isPresent()) {
+            final List<String> mvFilterStrings = matchedValuesFilter.getValues();
+            final List<Filter> mvFilters = new ArrayList<>();
+            for (final String s : mvFilterStrings) {
+                try {
+                    final Filter f = Filter.valueOf(s);
+                    mvFilters.add(f);
+                } catch (final LocalizedIllegalArgumentException le) {
+                    errPrintln(ERR_LDAP_MATCHEDVALUES_INVALID_FILTER.get(le.getMessage()));
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            final MatchedValuesRequestControl mvc =
+                    MatchedValuesRequestControl.newControl(true, mvFilters);
+            search.addControl(mvc);
+        }
+
+        if (sortOrder.isPresent()) {
+            try {
+                search.addControl(ServerSideSortRequestControl.newControl(false, sortOrder
+                        .getValue()));
+            } catch (final LocalizedIllegalArgumentException le) {
+                errPrintln(ERR_LDAP_SORTCONTROL_INVALID_ORDER.get(le.getMessageObject()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        if (vlvDescriptor.isPresent()) {
+            if (!sortOrder.isPresent()) {
+                final LocalizableMessage message =
+                        ERR_LDAPSEARCH_VLV_REQUIRES_SORT.get(vlvDescriptor.getLongIdentifier(),
+                                sortOrder.getLongIdentifier());
+                errPrintln(message);
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+
+            final StringTokenizer tokenizer = new StringTokenizer(vlvDescriptor.getValue(), ":");
+            final int numTokens = tokenizer.countTokens();
+            if (numTokens == 3) {
+                try {
+                    final int beforeCount = Integer.parseInt(tokenizer.nextToken());
+                    final int afterCount = Integer.parseInt(tokenizer.nextToken());
+                    final ByteString assertionValue = ByteString.valueOfUtf8(tokenizer.nextToken());
+                    search.addControl(VirtualListViewRequestControl.newAssertionControl(true,
+                            assertionValue, beforeCount, afterCount, null));
+                } catch (final Exception e) {
+                    errPrintln(ERR_LDAPSEARCH_VLV_INVALID_DESCRIPTOR.get());
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            } else if (numTokens == 4) {
+                try {
+                    final int beforeCount = Integer.parseInt(tokenizer.nextToken());
+                    final int afterCount = Integer.parseInt(tokenizer.nextToken());
+                    final int offset = Integer.parseInt(tokenizer.nextToken());
+                    final int contentCount = Integer.parseInt(tokenizer.nextToken());
+                    search.addControl(VirtualListViewRequestControl.newOffsetControl(true, offset,
+                            contentCount, beforeCount, afterCount, null));
+                } catch (final Exception e) {
+                    errPrintln(ERR_LDAPSEARCH_VLV_INVALID_DESCRIPTOR.get());
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            } else {
+                errPrintln(ERR_LDAPSEARCH_VLV_INVALID_DESCRIPTOR.get());
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        int pageSize = 0;
+        if (simplePageSize.isPresent()) {
+            if (filters.size() > 1) {
+                errPrintln(ERR_PAGED_RESULTS_REQUIRES_SINGLE_FILTER.get());
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+
+            try {
+                pageSize = simplePageSize.getIntValue();
+                search.addControl(SimplePagedResultsControl.newControl(true, pageSize, ByteString
+                        .empty()));
+            } catch (final ArgumentException ae) {
+                errPrintln(ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+        }
+
+        int wrapColumn = 80;
+        if (dontWrap.isPresent()) {
+            wrapColumn = 0;
+        }
+
+        if (noop.isPresent()) {
+            /* We don't actually need to open a connection or perform the
+             search, so we're done. We should return 0 to either mean that the
+             processing was successful or that there were no matching entries,
+             based on countEntries.isPresent() (but in either case the return value
+             should be zero).*/
+            return 0;
+        }
+
+        try (Connection connection = connectionFactory.getConnection()) {
+            if (bindRequest != null) {
+                printPasswordPolicyResults(this, connection.bind(bindRequest));
+            }
+
+            int filterIndex = 0;
+            ldifWriter = new LDIFEntryWriter(getOutputStream()).setWrapColumn(wrapColumn);
+            final LDAPSearchResultHandler resultHandler = new LDAPSearchResultHandler();
+            while (true) {
+                Result result = connection.search(search, resultHandler);
+                try {
+                    final ServerSideSortResponseControl control =
+                            result.getControl(ServerSideSortResponseControl.DECODER,
+                                    new DecodeOptions());
+                    if (control != null
+                            && control.getResult() != ResultCode.SUCCESS) {
+                        println(WARN_LDAPSEARCH_SORT_ERROR.get(control.getResult().toString()));
+                    }
+                } catch (final DecodeException e) {
+                    errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+                }
+
+                try {
+                    final VirtualListViewResponseControl control =
+                            result.getControl(VirtualListViewResponseControl.DECODER,
+                                    new DecodeOptions());
+                    if (control != null) {
+                        if (control.getResult() == ResultCode.SUCCESS) {
+                            LocalizableMessage msg =
+                                    INFO_LDAPSEARCH_VLV_TARGET_OFFSET.get(control
+                                            .getTargetPosition());
+                            println(msg);
+
+                            msg = INFO_LDAPSEARCH_VLV_CONTENT_COUNT.get(control.getContentCount());
+                            println(msg);
+                        } else {
+                            final LocalizableMessage msg =
+                                    WARN_LDAPSEARCH_VLV_ERROR.get(control.getResult().toString());
+                            println(msg);
+                        }
+                    }
+                } catch (final DecodeException e) {
+                    errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+                }
+
+                try {
+                    SimplePagedResultsControl control =
+                            result.getControl(SimplePagedResultsControl.DECODER,
+                                    new DecodeOptions());
+                    if (control != null && control.getCookie().length() > 0) {
+                        if (!isQuiet()) {
+                            pressReturnToContinue();
+                        }
+                        final Iterator<Control> iterator = search.getControls().iterator();
+                        while (iterator.hasNext()) {
+                            if (SimplePagedResultsControl.OID.equals(iterator.next().getOID())) {
+                                iterator.remove();
+                            }
+                        }
+                        control = SimplePagedResultsControl.newControl(true, pageSize, control.getCookie());
+                        search.addControl(control);
+                        continue;
+                    }
+                } catch (final DecodeException e) {
+                    errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+                }
+
+                errPrintln();
+                errPrintln(ERR_TOOL_RESULT_CODE.get(result.getResultCode().intValue(), result
+                        .getResultCode().toString()));
+                if (result.getDiagnosticMessage() != null
+                        && result.getDiagnosticMessage().length() > 0) {
+                    errPrintln(LocalizableMessage.raw(result.getDiagnosticMessage()));
+                }
+                if (result.getMatchedDN() != null && result.getMatchedDN().length() > 0) {
+                    errPrintln(ERR_TOOL_MATCHED_DN.get(result.getMatchedDN()));
+                }
+
+                filterIndex++;
+                if (filterIndex < filters.size()) {
+                    search.setFilter(filters.get(filterIndex));
+                } else {
+                    break;
+                }
+            }
+            if (countEntries.isPresent() && !isQuiet()) {
+                println(INFO_LDAPSEARCH_MATCHING_ENTRY_COUNT.get(resultHandler.entryCount));
+                println();
+            }
+            return 0;
+        } catch (final LdapException ere) {
+            return printErrorMessage(this, ere);
+        } finally {
+            closeSilently(ldifWriter);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFDiff.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFDiff.java
new file mode 100644
index 0000000..0a90670
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFDiff.java
@@ -0,0 +1,197 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.OPTION_LONG_OUTPUT_LDIF_FILENAME;
+import static com.forgerock.opendj.cli.ArgumentConstants.OPTION_SHORT_OUTPUT_LDIF_FILENAME;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldif.LDIF;
+import org.forgerock.opendj.ldif.LDIFChangeRecordWriter;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * This utility can be used to compare two LDIF files and report the differences
+ * in LDIF format.
+ */
+public final class LDIFDiff extends ConsoleApplication {
+
+    /**
+     * The main method for LDIFDiff tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDIFDiff().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private LDIFDiff() {
+        // Nothing to do.
+    }
+
+    private int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDIFDIFF_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser = new ArgumentParser(
+            LDIFDiff.class.getName(), toolDescription, false, true, 2, 2, "source target");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDIFDIFF.get());
+
+        final BooleanArgument showUsage;
+        final StringArgument outputFilename;
+        try {
+            outputFilename =
+                    StringArgument.builder(OPTION_LONG_OUTPUT_LDIF_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_OUTPUT_LDIF_FILENAME)
+                            .description(INFO_LDIFDIFF_DESCRIPTION_OUTPUT_FILENAME.get(
+                                    INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get()))
+                            .defaultValue("stdout")
+                            .valuePlaceholder(INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            /* If we should just display usage or version information, then print it and exit. */
+            if (argParser.usageOrVersionDisplayed()) {
+                return ResultCode.SUCCESS.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        InputStream sourceInputStream = null;
+        InputStream targetInputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // First source file.
+            final List<String> trailingArguments = argParser.getTrailingArguments();
+            if (!"-".equals(trailingArguments.get(0))) {
+                try {
+                    sourceInputStream = new FileInputStream(trailingArguments.get(0));
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(trailingArguments.get(0), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Patch file.
+            if (!"-".equals(trailingArguments.get(1))) {
+                try {
+                    targetInputStream = new FileInputStream(trailingArguments.get(1));
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(trailingArguments.get(1), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Output file.
+            if (outputFilename.isPresent() && !"-".equals(outputFilename.getValue())) {
+                try {
+                    outputStream = new FileOutputStream(outputFilename.getValue());
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_WRITE.get(outputFilename.getValue(), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Default to stdin/stdout for all streams if not specified.
+            if (sourceInputStream == null) {
+                // Command line parameter was "-".
+                sourceInputStream = System.in;
+            }
+
+            if (targetInputStream == null) {
+                targetInputStream = System.in;
+            }
+
+            if (outputStream == null) {
+                outputStream = System.out;
+            }
+
+            /* Check that we are not attempting to read both the source and target from stdin. */
+            if (sourceInputStream == targetInputStream) {
+                final LocalizableMessage message = ERR_LDIFDIFF_MULTIPLE_USES_OF_STDIN.get();
+                errPrintln(message);
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+
+            // Perform the diff.
+            try (LDIFEntryReader sourceReader = new LDIFEntryReader(sourceInputStream);
+                LDIFEntryReader targetReader = new LDIFEntryReader(targetInputStream);
+                LDIFChangeRecordWriter outputWriter = new LDIFChangeRecordWriter(outputStream)) {
+                LDIF.copyTo(LDIF.diff(sourceReader, targetReader), outputWriter);
+            }
+        } catch (final IOException e) {
+            if (e instanceof LocalizableException) {
+                errPrintln(ERR_LDIFDIFF_DIFF_FAILED.get(((LocalizableException) e).getMessageObject()));
+            } else {
+                errPrintln(ERR_LDIFDIFF_DIFF_FAILED.get(e.getLocalizedMessage()));
+            }
+            return ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+        } finally {
+            closeSilently(sourceInputStream, targetInputStream, outputStream);
+        }
+
+        return ResultCode.SUCCESS.intValue();
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFModify.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFModify.java
new file mode 100644
index 0000000..5e6a3bc
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFModify.java
@@ -0,0 +1,290 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.OPTION_LONG_OUTPUT_LDIF_FILENAME;
+import static com.forgerock.opendj.cli.ArgumentConstants.OPTION_SHORT_OUTPUT_LDIF_FILENAME;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static org.forgerock.util.Utils.closeSilently;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldif.LDIF;
+import org.forgerock.opendj.ldif.LDIFChangeRecordReader;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+import org.forgerock.opendj.ldif.RejectedChangeRecordListener;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A tool that can be used to issue update (Add/Delete/Modify/ModifyDN) requests
+ * to a set of entries contained in an LDIF file.
+ */
+public final class LDIFModify extends ConsoleApplication {
+    /**
+     * The main method for LDIFModify tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDIFModify().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private LDIFModify() {
+        // Nothing to do.
+    }
+
+    private int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_LDIFMODIFY_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser = new ArgumentParser(
+            LDIFModify.class.getName(), toolDescription, false, true, 1, 2, "source [changes]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDIFMODIFY.get());
+
+        final BooleanArgument continueOnError;
+        final BooleanArgument showUsage;
+        final StringArgument outputFilename;
+        try {
+            outputFilename =
+                    StringArgument.builder(OPTION_LONG_OUTPUT_LDIF_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_OUTPUT_LDIF_FILENAME)
+                            .description(INFO_LDIFMODIFY_DESCRIPTION_OUTPUT_FILENAME.get(
+                                    INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get()))
+                            .defaultValue("stdout")
+                            .valuePlaceholder(INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            continueOnError = continueOnErrorArgument();
+            argParser.addArgument(continueOnError);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information,
+            // then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return ResultCode.SUCCESS.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        InputStream sourceInputStream = null;
+        InputStream changesInputStream = null;
+        OutputStream outputStream = null;
+        LDIFEntryReader sourceReader = null;
+        LDIFChangeRecordReader changesReader = null;
+        LDIFEntryWriter outputWriter = null;
+
+        try {
+            // First source file.
+            final List<String> trailingArguments = argParser.getTrailingArguments();
+            if (!"-".equals(trailingArguments.get(0))) {
+                try {
+                    sourceInputStream = new FileInputStream(trailingArguments.get(0));
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(trailingArguments.get(0), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Patch file.
+            if (trailingArguments.size() > 1 && !"-".equals(trailingArguments.get(1))) {
+                try {
+                    changesInputStream = new FileInputStream(trailingArguments.get(1));
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(trailingArguments.get(1), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Output file.
+            if (outputFilename.isPresent() && !"-".equals(outputFilename.getValue())) {
+                try {
+                    outputStream = new FileOutputStream(outputFilename.getValue());
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_WRITE.get(outputFilename.getValue(), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Default to stdin/stdout for all streams if not specified.
+            if (sourceInputStream == null) {
+                // Command line parameter was "-".
+                sourceInputStream = System.in;
+            }
+
+            if (changesInputStream == null) {
+                changesInputStream = System.in;
+            }
+
+            if (outputStream == null) {
+                outputStream = System.out;
+            }
+
+            /* Check that we are not attempting to read both the source and changes from stdin. */
+            if (sourceInputStream == changesInputStream) {
+                final LocalizableMessage message = ERR_LDIFMODIFY_MULTIPLE_USES_OF_STDIN.get();
+                errPrintln(message);
+                return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+            }
+
+            // Apply the changes.
+            sourceReader = new LDIFEntryReader(sourceInputStream);
+            changesReader = new LDIFChangeRecordReader(changesInputStream);
+            outputWriter = new LDIFEntryWriter(outputStream);
+
+            final RejectedChangeRecordListener listener = new RejectedChangeRecordListener() {
+                @Override
+                public Entry handleDuplicateEntry(final AddRequest change, final Entry existingEntry)
+                        throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleDuplicateEntry(change,
+                                existingEntry);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                    return change;
+                }
+
+                @Override
+                public Entry handleDuplicateEntry(final ModifyDNRequest change,
+                        final Entry existingEntry, final Entry renamedEntry) throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleDuplicateEntry(change,
+                                existingEntry, renamedEntry);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                    return renamedEntry;
+                }
+
+                @Override
+                public void handleRejectedChangeRecord(final AddRequest change,
+                        final LocalizableMessage reason) throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleRejectedChangeRecord(change,
+                                reason);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                }
+
+                @Override
+                public void handleRejectedChangeRecord(final DeleteRequest change,
+                        final LocalizableMessage reason) throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleRejectedChangeRecord(change,
+                                reason);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                }
+
+                @Override
+                public void handleRejectedChangeRecord(final ModifyDNRequest change,
+                        final LocalizableMessage reason) throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleRejectedChangeRecord(change,
+                                reason);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                }
+
+                @Override
+                public void handleRejectedChangeRecord(final ModifyRequest change,
+                        final LocalizableMessage reason) throws DecodeException {
+                    try {
+                        RejectedChangeRecordListener.FAIL_FAST.handleRejectedChangeRecord(change,
+                                reason);
+                    } catch (final DecodeException e) {
+                        logErrorOrFail(e);
+                    }
+                }
+
+                private void logErrorOrFail(final DecodeException e) throws DecodeException {
+                    if (continueOnError.isPresent()) {
+                        errPrintln(e.getMessageObject());
+                    } else {
+                        throw e;
+                    }
+                }
+            };
+
+            LDIF.copyTo(LDIF.patch(sourceReader, changesReader, listener), outputWriter);
+        } catch (final IOException e) {
+            if (e instanceof LocalizableException) {
+                errPrintln(ERR_LDIFMODIFY_PATCH_FAILED.get(((LocalizableException) e)
+                        .getMessageObject()));
+            } else {
+                errPrintln(ERR_LDIFMODIFY_PATCH_FAILED.get(e.getLocalizedMessage()));
+            }
+            return ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+        } finally {
+            closeSilently(sourceReader, changesReader, outputWriter);
+            closeSilently(sourceInputStream, changesInputStream, outputStream);
+        }
+
+        return ResultCode.SUCCESS.intValue();
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFSearch.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFSearch.java
new file mode 100644
index 0000000..a844af6
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDIFSearch.java
@@ -0,0 +1,280 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableException;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldif.LDIF;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/** This utility can be used to perform search operations against data in an LDIF file. */
+public final class LDIFSearch extends ConsoleApplication {
+    /**
+     * The main method for LDIFSearch tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new LDIFSearch().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private LDIFSearch() {
+        // Nothing to do.
+    }
+
+    private int run(final String[] args) {
+        /* Create the command-line argument parser for use with this program. */
+        final LocalizableMessage toolDescription = INFO_LDIFSEARCH_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser = new ArgumentParser(
+            LDIFSearch.class.getName(), toolDescription, false, true, 1, 0, "source [filter] [attributes ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_LDIFSEARCH.get());
+
+        final BooleanArgument showUsage;
+        final StringArgument outputFilename;
+        final BooleanArgument typesOnly;
+        final IntegerArgument timeLimit;
+        final StringArgument filename;
+        final StringArgument baseDN;
+        final MultiChoiceArgument<SearchScope> searchScope;
+        final IntegerArgument sizeLimit;
+        try {
+            outputFilename =
+                    StringArgument.builder(OPTION_LONG_OUTPUT_LDIF_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_OUTPUT_LDIF_FILENAME)
+                            .description(INFO_LDIFSEARCH_DESCRIPTION_OUTPUT_FILENAME.get(
+                                    INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get()))
+                            .defaultValue("stdout")
+                            .valuePlaceholder(INFO_OUTPUT_LDIF_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            baseDN =
+                    StringArgument.builder(OPTION_LONG_BASEDN)
+                            .shortIdentifier(OPTION_SHORT_BASEDN)
+                            .description(INFO_SEARCH_DESCRIPTION_BASEDN.get())
+                            .required()
+                            .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            searchScope = searchScopeArgument();
+            argParser.addArgument(searchScope);
+
+            filename =
+                    StringArgument.builder(OPTION_LONG_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_FILENAME)
+                            .description(INFO_SEARCH_DESCRIPTION_FILENAME.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            typesOnly =
+                    BooleanArgument.builder("typesOnly")
+                            .shortIdentifier('A')
+                            .description(INFO_DESCRIPTION_TYPES_ONLY.get())
+                            .buildAndAddToParser(argParser);
+            sizeLimit =
+                    IntegerArgument.builder("sizeLimit")
+                            .shortIdentifier('z')
+                            .description(INFO_SEARCH_DESCRIPTION_SIZE_LIMIT.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_SIZE_LIMIT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            timeLimit =
+                    IntegerArgument.builder("timeLimit")
+                            .shortIdentifier('l')
+                            .description(INFO_SEARCH_DESCRIPTION_TIME_LIMIT.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_TIME_LIMIT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            /* If we should just display usage or version information, then print it and exit. */
+            if (argParser.usageOrVersionDisplayed()) {
+                return ResultCode.SUCCESS.intValue();
+            }
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final List<Filter> filters = new LinkedList<>();
+        final List<String> attributes = new LinkedList<>();
+        final List<String> trailingArguments = argParser.getTrailingArguments();
+        if (trailingArguments.size() > 1) {
+            final List<String> filterAndAttributeStrings =
+                    trailingArguments.subList(1, trailingArguments.size());
+
+            /* The list of trailing arguments should be structured as follow:
+             - If a filter file is present, trailing arguments are
+             considered as attributes
+             - If filter file is not present, the first trailing argument is
+             considered the filter, the other as attributes.*/
+            if (!filename.isPresent()) {
+                final String filterString = filterAndAttributeStrings.remove(0);
+                try {
+                    filters.add(Filter.valueOf(filterString));
+                } catch (final LocalizedIllegalArgumentException e) {
+                    errPrintln(e.getMessageObject());
+                    return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+                }
+            }
+            // The rest are attributes
+            attributes.addAll(filterAndAttributeStrings);
+        }
+
+        if (filename.isPresent()) {
+            // Read the filter strings.
+            try (BufferedReader in = new BufferedReader(new FileReader(filename.getValue()))) {
+                String line = null;
+                while ((line = in.readLine()) != null) {
+                    if ("".equals(line.trim())) {
+                        // ignore empty lines.
+                        continue;
+                    }
+                    filters.add(Filter.valueOf(line));
+                }
+            } catch (final LocalizedIllegalArgumentException e) {
+                errPrintln(e.getMessageObject());
+                return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+            } catch (final IOException e) {
+                errPrintln(LocalizableMessage.raw(e.toString()));
+                return ResultCode.CLIENT_SIDE_FILTER_ERROR.intValue();
+            }
+        }
+
+        if (filters.isEmpty()) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_SEARCH_NO_FILTERS.get());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final SearchRequest search;
+        try {
+            final SearchScope scope = searchScope.getTypedValue();
+            search =
+                    Requests.newSearchRequest(DN.valueOf(baseDN.getValue()), scope, filters.get(0),
+                            attributes.toArray(new String[attributes.size()])).setTypesOnly(
+                            typesOnly.isPresent()).setTimeLimit(timeLimit.getIntValue())
+                            .setSizeLimit(sizeLimit.getIntValue());
+        } catch (final ArgumentException | LocalizedIllegalArgumentException e) {
+            errPrintln(e.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        InputStream sourceInputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // First source file.
+            if (!"-".equals(trailingArguments.get(0))) {
+                try {
+                    sourceInputStream = new FileInputStream(trailingArguments.get(0));
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ.get(trailingArguments.get(0), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Output file.
+            if (outputFilename.isPresent() && !"-".equals(outputFilename.getValue())) {
+                try {
+                    outputStream = new FileOutputStream(outputFilename.getValue());
+                } catch (final FileNotFoundException e) {
+                    final LocalizableMessage message =
+                            ERR_LDIF_FILE_CANNOT_OPEN_FOR_WRITE.get(outputFilename.getValue(), e
+                                    .getLocalizedMessage());
+                    errPrintln(message);
+                    return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+                }
+            }
+
+            // Default to stdin/stdout for all streams if not specified.
+            if (sourceInputStream == null) {
+                // Command line parameter was "-".
+                sourceInputStream = System.in;
+            }
+
+            if (outputStream == null) {
+                outputStream = System.out;
+            }
+
+            // Perform the search.
+            try (LDIFEntryReader sourceReader = new LDIFEntryReader(sourceInputStream);
+                LDIFEntryWriter outputWriter = new LDIFEntryWriter(outputStream)) {
+                LDIF.copyTo(LDIF.search(sourceReader, search), outputWriter);
+            }
+        } catch (final IOException e) {
+            if (e instanceof LocalizableException) {
+                errPrintln(ERR_LDIFSEARCH_FAILED.get(((LocalizableException) e).getMessageObject()));
+            } else {
+                errPrintln(ERR_LDIFSEARCH_FAILED.get(e.getLocalizedMessage()));
+            }
+            return ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue();
+        } finally {
+            closeSilently(sourceInputStream, outputStream);
+        }
+
+        return ResultCode.SUCCESS.intValue();
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java
new file mode 100644
index 0000000..a150056
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java
@@ -0,0 +1,290 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions Copyright 2013-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_MAKELDIF_WRAP_COLUMN_PLACEHOLDER;
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintStream;
+
+import org.forgerock.i18n.LocalizableMessage;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldif.EntryGenerator;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+
+/** Program that generate LDIF content based on a template. */
+public final class MakeLDIF extends ConsoleApplication {
+    /** The value for the constant option in LDIF generator tools. */
+    public static final String OPTION_LONG_CONSTANT = "constant";
+
+    /** The value for the path to look for LDIF resources (e.g data files). */
+    public static final String OPTION_LONG_RESOURCE_PATH = "resourcePath";
+
+    private static final int EXIT_CODE_SUCCESS = 0;
+    private static final int EXIT_CODE_FAILURE = 1;
+
+    /** The total number of entries that have been written. */
+    private long numberOfEntriesWritten;
+
+    /**
+     * Main method for MakeLDIF tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new MakeLDIF().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    /** Run Make LDIF with provided command-line arguments. */
+    int run(final String[] args) {
+        final LocalizableMessage toolDescription = INFO_MAKELDIF_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser = new ArgumentParser(MakeLDIF.class.getName(), toolDescription,
+                false, true, 1, 1, "template-file-path");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_MAKELDIF.get());
+        argParser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_MAKELDIF.get());
+
+        BooleanArgument showUsage;
+        IntegerArgument randomSeed;
+        StringArgument ldifFile;
+        StringArgument resourcePath;
+        StringArgument constants;
+        IntegerArgument wrapColumn;
+        try {
+            resourcePath =
+                    StringArgument.builder(OPTION_LONG_RESOURCE_PATH)
+                            .shortIdentifier('r')
+                            .description(INFO_MAKELDIF_DESCRIPTION_RESOURCE_PATH.get())
+                            .docDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RESOURCE_PATH.get())
+                            .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            ldifFile =
+                    StringArgument.builder(OPTION_LONG_OUTPUT_LDIF_FILENAME)
+                            .shortIdentifier(OPTION_SHORT_OUTPUT_LDIF_FILENAME)
+                            .description(INFO_MAKELDIF_DESCRIPTION_LDIF.get())
+                            .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            randomSeed =
+                    IntegerArgument.builder(OPTION_LONG_RANDOM_SEED)
+                            .shortIdentifier(OPTION_SHORT_RANDOM_SEED)
+                            .description(INFO_MAKELDIF_DESCRIPTION_SEED.get())
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_SEED_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            constants =
+                    StringArgument.builder(OPTION_LONG_CONSTANT)
+                            .shortIdentifier('c')
+                            .description(INFO_MAKELDIF_DESCRIPTION_CONSTANT.get())
+                            .multiValued()
+                            .valuePlaceholder(INFO_CONSTANT_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+            showUsage =
+                    BooleanArgument.builder(OPTION_LONG_HELP)
+                            .shortIdentifier(OPTION_SHORT_HELP)
+                            .description(INFO_MAKELDIF_DESCRIPTION_HELP.get())
+                            .buildAndAddToParser(argParser);
+
+            wrapColumn =
+                    IntegerArgument.builder("wrapColumn")
+                            .shortIdentifier('w')
+                            .description(INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN.get())
+                            .lowerBound(0)
+                            .defaultValue(0)
+                            .valuePlaceholder(INFO_MAKELDIF_WRAP_COLUMN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            argParser.setUsageArgument(showUsage, getOutputStream());
+        } catch (ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return EXIT_CODE_FAILURE;
+        }
+
+        // Parse the command-line arguments provided to the program.
+        try {
+            argParser.parseArguments(args);
+        } catch (ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return EXIT_CODE_FAILURE;
+        }
+
+        if (argParser.usageOrVersionDisplayed()) {
+            return 0;
+        }
+        final String templatePath = argParser.getTrailingArguments().get(0);
+        return run(templatePath, resourcePath, ldifFile, randomSeed, constants, wrapColumn);
+    }
+
+    /** Run Make LDIF with provided arguments. */
+    private int run(final String templatePath, final StringArgument resourcePath, final StringArgument ldifFile,
+            final IntegerArgument randomSeedArg, final StringArgument constants, final IntegerArgument wrapColumn) {
+        LDIFEntryWriter writer = null;
+        try (EntryGenerator generator = createGenerator(templatePath, resourcePath, randomSeedArg, constants)) {
+            if (generator == null) {
+                return EXIT_CODE_FAILURE;
+            }
+
+            if (generator.hasWarnings()) {
+                for (LocalizableMessage warn : generator.getWarnings()) {
+                    errPrintln(warn);
+                }
+            }
+
+            try {
+                writer = createLdifWriter(ldifFile, wrapColumn);
+            } catch (final IOException e) {
+                errPrintln(ERR_MAKELDIF_UNABLE_TO_CREATE_LDIF.get(ldifFile.getValue(), e.getMessage()));
+                return EXIT_CODE_FAILURE;
+            } catch (final ArgumentException e) {
+                errPrintln(ERR_ERROR_PARSING_ARGS.get(e.getMessageObject()));
+                return EXIT_CODE_FAILURE;
+            }
+
+            if (!generateEntries(generator, writer, ldifFile)) {
+                return EXIT_CODE_FAILURE;
+            }
+
+            errPrintln(INFO_MAKELDIF_PROCESSING_COMPLETE.get(numberOfEntriesWritten));
+
+            return EXIT_CODE_SUCCESS;
+        } finally {
+            closeSilently(writer);
+        }
+    }
+
+    private LDIFEntryWriter createLdifWriter(final StringArgument ldifFile, final IntegerArgument wrapColumn)
+            throws IOException, ArgumentException {
+        final LDIFEntryWriter writer;
+        if (ldifFile.isPresent()) {
+            writer = new LDIFEntryWriter(new BufferedWriter(new FileWriter(ldifFile.getValue())));
+        } else {
+            writer = new LDIFEntryWriter(getOutputStream());
+        }
+        return writer.setWrapColumn(wrapColumn.getIntValue());
+    }
+
+    static EntryGenerator createGenerator(final String templatePath, final StringArgument resourcePath,
+                                            final IntegerArgument randomSeedArg, final StringArgument constants,
+                                            final boolean generateBranches, final ConsoleApplication app) {
+        final EntryGenerator generator = new EntryGenerator(templatePath).setGenerateBranches(generateBranches);
+
+        if (resourcePath.isPresent()) {
+            final File resourceDir = new File(resourcePath.getValue());
+            if (!resourceDir.exists()) {
+                app.errPrintln(ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY.get(resourcePath.getValue()));
+                generator.close();
+                return null;
+            }
+            generator.setResourcePath(resourcePath.getValue());
+        }
+
+        if (randomSeedArg.isPresent()) {
+            try {
+                generator.setRandomSeed(randomSeedArg.getIntValue());
+            } catch (ArgumentException ae) {
+                app.errPrintln(ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+                generator.close();
+                return null;
+            }
+        }
+
+        if (constants.isPresent()
+                && !addConstantsToGenerator(constants, generator, app)) {
+            generator.close();
+            return null;
+        }
+
+        // Force initialization of generator
+        try {
+            generator.hasNext();
+        } catch (IOException e) {
+            app.errPrintln(ERR_LDIF_GEN_TOOL_EXCEPTION_DURING_PARSE.get(e.getMessage()));
+            generator.close();
+            return null;
+        }
+
+        return generator;
+    }
+
+    /** Returns true if all constants are added to generator, false otherwise. */
+    private static boolean addConstantsToGenerator(StringArgument constants, EntryGenerator generator,
+                                                       final ConsoleApplication app) {
+        for (final String constant : constants.getValues()) {
+            final String[] chunks = constant.split("=");
+            if (chunks.length != 2) {
+                app.errPrintln(ERR_CONSTANT_ARG_CANNOT_DECODE.get(constant));
+                return false;
+            }
+            generator.setConstant(chunks[0], chunks[1]);
+        }
+        return true;
+    }
+
+    private EntryGenerator createGenerator(final String templatePath, final StringArgument resourcePath,
+            final IntegerArgument randomSeedArg, final StringArgument constants) {
+        return createGenerator(templatePath, resourcePath, randomSeedArg, constants, true, this);
+    }
+
+    /** Returns true if generation is successful, false otherwise. */
+    private boolean generateEntries(final EntryGenerator generator, final LDIFEntryWriter writer,
+            final StringArgument ldifFile) {
+        try {
+            while (generator.hasNext()) {
+                final Entry entry = generator.readEntry();
+                try {
+                    writer.writeEntry(entry);
+                } catch (IOException e) {
+                    errPrintln(ERR_MAKELDIF_ERROR_WRITING_LDIF.get(ldifFile.getValue(), e.getMessage()));
+                    return false;
+                }
+                if ((++numberOfEntriesWritten % 1000) == 0) {
+                    errPrintln(INFO_MAKELDIF_PROCESSED_N_ENTRIES.get(numberOfEntriesWritten));
+                }
+            }
+        } catch (Exception e) {
+            errPrintln(ERR_MAKELDIF_EXCEPTION_DURING_PROCESSING.get(e.getMessage()));
+            return false;
+        }
+        return true;
+    }
+
+    private MakeLDIF() {
+        // nothing to do
+    }
+
+    /** To allow tests. */
+    MakeLDIF(PrintStream out, PrintStream err) {
+        super(out, err);
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
new file mode 100644
index 0000000..102ea9f
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
@@ -0,0 +1,244 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.StringArgument;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+/**
+ * A load generation tool that can be used to load a Directory Server with
+ * Modify requests using one or more LDAP connections.
+ */
+public final class ModRate extends ConsoleApplication {
+    private static final class ModifyPerformanceRunner extends PerformanceRunner {
+        private final class ModifyWorkerThread extends WorkerThread {
+            private ModifyRequest mr;
+            private Object[] data;
+
+            private ModifyWorkerThread(final Connection connection,
+                    final ConnectionFactory connectionFactory) {
+                super(connection, connectionFactory);
+            }
+
+            @Override
+            public Promise<?, LdapException> performOperation(final Connection connection,
+                    final DataSource[] dataSources, final long currentTimeNs) {
+                if (dataSources != null) {
+                    data = DataSource.generateData(dataSources, data);
+                }
+                mr = newModifyRequest(data);
+                LdapResultHandler<Result> modRes = new UpdateStatsResultHandler<>(currentTimeNs);
+
+                incrementIterationCount();
+                return connection.modifyAsync(mr).thenOnResult(modRes).thenOnException(modRes);
+            }
+
+            private ModifyRequest newModifyRequest(final Object[] data) {
+                String formattedString;
+                int colonPos;
+                ModifyRequest mr;
+                if (data != null) {
+                    mr = Requests.newModifyRequest(String.format(baseDN, data));
+                } else {
+                    mr = Requests.newModifyRequest(baseDN);
+                }
+                for (final String modString : modStrings) {
+                    if (data != null) {
+                        formattedString = String.format(modString, data);
+                    } else {
+                        formattedString = modString;
+                    }
+                    colonPos = formattedString.indexOf(':');
+                    if (colonPos > 0) {
+                        mr.addModification(ModificationType.REPLACE, formattedString.substring(0,
+                                colonPos), formattedString.substring(colonPos + 1));
+                    }
+                }
+                return mr;
+            }
+        }
+
+        private String baseDN;
+        private String[] modStrings;
+
+        private ModifyPerformanceRunner(final PerformanceRunnerOptions options)
+                throws ArgumentException {
+            super(options);
+        }
+
+        @Override
+        WorkerThread newWorkerThread(final Connection connection,
+                final ConnectionFactory connectionFactory) {
+            return new ModifyWorkerThread(connection, connectionFactory);
+        }
+
+        @Override
+        StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) {
+            return new StatsThread(performanceRunner, app);
+        }
+    }
+
+    /**
+     * The main method for ModRate tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new ModRate().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+    private BooleanArgument scriptFriendly;
+
+    private ModRate() {
+        // Nothing to do.
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isScriptFriendly() {
+        return scriptFriendly.isPresent();
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    private int run(final String[] args) {
+        // Creates the command-line argument parser for use with this program
+        final LocalizableMessage toolDescription = INFO_MODRATE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(ModRate.class.getName(), toolDescription, false, true, 1, 0,
+                        "[(attribute:value format string) ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_MODRATE.get());
+        argParser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RATE_TOOLS.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        ModifyPerformanceRunner runner;
+
+        BooleanArgument showUsage;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+        StringArgument baseDN;
+        try {
+            Utils.setDefaultPerfToolProperties();
+
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            runner = new ModifyPerformanceRunner(new PerformanceRunnerOptions(argParser, this));
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            baseDN =
+                    StringArgument.builder(OPTION_LONG_TARGETDN)
+                            .shortIdentifier(OPTION_SHORT_BASEDN)
+                            .description(INFO_MODRATE_TOOL_DESCRIPTION_TARGETDN.get())
+                            .required()
+                            .valuePlaceholder(INFO_TARGETDN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+
+            scriptFriendly =
+                    BooleanArgument.builder("scriptFriendly")
+                            .shortIdentifier('S')
+                            .description(INFO_DESCRIPTION_SCRIPT_FRIENDLY.get())
+                            .buildAndAddToParser(argParser);
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            /* If we should just display usage or version information, then print it and exit. */
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
+            runner.validate();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        runner.modStrings =
+                argParser.getTrailingArguments().toArray(
+                        new String[argParser.getTrailingArguments().size()]);
+        runner.baseDN = baseDN.getValue();
+
+        try {
+            /* Try it out to make sure the format string and data sources match. */
+            final Object[] data = DataSource.generateData(runner.getDataSources(), null);
+            for (final String modString : runner.modStrings) {
+                String.format(modString, data);
+            }
+            String.format(runner.baseDN, data);
+        } catch (final Exception ex1) {
+            errPrintln(LocalizableMessage.raw("Error formatting filter or base DN: " + ex1));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        return runner.run(connectionFactory);
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
new file mode 100644
index 0000000..bd119ef
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
@@ -0,0 +1,503 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static java.util.concurrent.TimeUnit.*;
+
+import static org.forgerock.util.Utils.*;
+
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.StringArgument;
+import com.forgerock.opendj.util.StaticUtils;
+
+/** Benchmark application framework. */
+abstract class PerformanceRunner implements ConnectionEventListener {
+    private static final double[] DEFAULT_PERCENTILES = new double[] { 99.9, 99.99, 99.999 };
+
+    class TimerThread extends Thread {
+        private final long timeToWait;
+
+        TimerThread(long timeToWait) {
+            this.timeToWait = timeToWait;
+        }
+
+        void performStopOperations() {
+            stopTool();
+        }
+
+        @Override
+        public void run() {
+            try {
+                Thread.sleep(timeToWait);
+            } catch (InterruptedException e) {
+                throw new IllegalStateException(e);
+            } finally {
+                performStopOperations();
+            }
+        }
+    }
+
+    /**
+     * Statistics update result handler implementation.
+     *
+     * @param <S>
+     *            The type of expected result.
+     */
+    class UpdateStatsResultHandler<S extends Result> implements LdapResultHandler<S> {
+        protected final long operationStartTimeNs;
+
+        UpdateStatsResultHandler(final long currentTimeNs) {
+            this.operationStartTimeNs = currentTimeNs;
+        }
+
+        @Override
+        public final void handleException(final LdapException exception) {
+            statsThread.incrementFailedCount();
+            updateResponseTime();
+            app.errPrintVerboseMessage(LocalizableMessage.raw(exception.getResult().toString()));
+        }
+
+        @Override
+        public final void handleResult(final S result) {
+            statsThread.incrementSuccessCount();
+            updateResponseTime();
+            updateAdditionalStatsOnResult();
+        }
+
+        /** Do nothing by default, child classes which manage additional stats need to override this method. */
+        void updateAdditionalStatsOnResult() { }
+
+        private void updateResponseTime() {
+            statsThread.addResponseTime(System.nanoTime() - operationStartTimeNs);
+        }
+    }
+
+    /** Worker thread base implementation. */
+    abstract class WorkerThread extends Thread {
+        private int count;
+        private final Connection connection;
+        private final ConnectionFactory connectionFactory;
+        boolean localStopRequested;
+
+        WorkerThread(final Connection connection, final ConnectionFactory connectionFactory) {
+            super("Worker Thread");
+            this.connection = connection;
+            this.connectionFactory = connectionFactory;
+        }
+
+        public abstract Promise<?, LdapException> performOperation(
+                Connection connection, DataSource[] dataSources, long currentTimeNs);
+
+        @Override
+        public void run() {
+            Promise<?, LdapException> promise;
+            Connection connection;
+            final double targetTimeMs = 1000.0 / (targetThroughput / (double) (numThreads * numConnections));
+            double sleepTimeMs = 0;
+
+            while (!stopRequested && !localStopRequested
+                    && (maxIterations <= 0 || count < maxIterations)) {
+                try {
+                    connection = getConnectionToUse();
+                } catch (final InterruptedException e) {
+                    // Ignore and check stop requested
+                    continue;
+                } catch (final LdapException e) {
+                    handleConnectionError(false, e);
+                    break;
+                }
+
+                long startTimeNs = System.nanoTime();
+                promise = performOperation(connection, dataSources.get(), startTimeNs);
+                statsThread.incrementOperationCount();
+                try {
+                    promise.getOrThrow();
+                } catch (final InterruptedException e) {
+                    // Ignore and check stop requested
+                    continue;
+                } catch (final LdapException e) {
+                    if (!stopRequested && e.getCause() instanceof IOException) {
+                        e.getCause().printStackTrace(app.getErrorStream());
+                        stopTool(true);
+                        break;
+                    }
+                    // Ignore. Handled by result handler
+                } finally {
+                    if (this.connection == null) {
+                        connection.close();
+                    }
+                }
+
+                if (targetThroughput > 0) {
+                    try {
+                        if (sleepTimeMs > 1) {
+                            sleep((long) Math.floor(sleepTimeMs));
+                        }
+                    } catch (final InterruptedException e) {
+                        continue;
+                    }
+
+                    sleepTimeMs += targetTimeMs - NANOSECONDS.toMillis(System.nanoTime() - startTimeNs);
+                    final long oneMinuteMs = MINUTES.toMillis(1);
+                    if (sleepTimeMs + oneMinuteMs < 0) {
+                        // If we fall behind by 60 seconds, just forget about catching up
+                        sleepTimeMs = -oneMinuteMs;
+                    }
+                }
+            }
+        }
+
+        private Connection getConnectionToUse() throws InterruptedException, LdapException {
+            if (this.connection == null) {
+                return connectionFactory.getConnectionAsync().getOrThrow();
+            } else {
+                final Connection resultConnection = this.connection;
+                if (!noRebind && bindRequest != null) {
+                    resultConnection.bindAsync(bindRequest).getOrThrow();
+                }
+                return resultConnection;
+            }
+        }
+
+        void incrementIterationCount() {
+            count++;
+        }
+    }
+
+    private final ConsoleApplication app;
+    private DataSource[] dataSourcePrototypes;
+
+    /** Thread local copies of the data sources. */
+    private final ThreadLocal<DataSource[]> dataSources = new ThreadLocal<DataSource[]>() {
+        @Override
+        protected DataSource[] initialValue() {
+            final DataSource[] prototypes = getDataSources();
+            final int sz = prototypes.length;
+            final DataSource[] threadLocalCopy = new DataSource[sz];
+            for (int i = 0; i < sz; i++) {
+                threadLocalCopy[i] = prototypes[i].duplicate();
+            }
+            return threadLocalCopy;
+        }
+
+    };
+
+    int numThreads;
+    int numConnections;
+    private boolean stopRequested;
+
+    private int targetThroughput;
+    private int maxIterations;
+    /** Warm-up duration time in ms. */
+    private long warmUpDurationMs;
+    /** Max duration time in ms, 0 for unlimited. */
+    private long maxDurationTimeMs;
+    private boolean noRebind;
+    private BindRequest bindRequest;
+    private int statsIntervalMs;
+    private final IntegerArgument numThreadsArgument;
+    private final IntegerArgument maxDurationArgument;
+    private final IntegerArgument statsIntervalArgument;
+    private final IntegerArgument targetThroughputArgument;
+    private final IntegerArgument numConnectionsArgument;
+    private final IntegerArgument percentilesArgument;
+    private final BooleanArgument keepConnectionsOpen;
+    private final BooleanArgument noRebindArgument;
+    private final StringArgument arguments;
+    protected final IntegerArgument maxIterationsArgument;
+    protected final IntegerArgument warmUpArgument;
+
+    private final List<Thread> workerThreads = new ArrayList<>();
+    StatsThread statsThread;
+
+    PerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException {
+        ArgumentParser argParser = options.getArgumentParser();
+
+        this.app = options.getConsoleApplication();
+
+        numThreadsArgument =
+                IntegerArgument.builder("numThreads")
+                        .shortIdentifier('t')
+                        .description(LocalizableMessage.raw("Number of worker threads per connection"))
+                        .lowerBound(1)
+                        .defaultValue(1)
+                        .valuePlaceholder(LocalizableMessage.raw("{numThreads}"))
+                        .buildArgument();
+        if (options.supportsMultipleThreadsPerConnection()) {
+            argParser.addArgument(numThreadsArgument);
+        } else {
+            numThreadsArgument.addValue("1");
+        }
+
+        numConnectionsArgument =
+                IntegerArgument.builder("numConnections")
+                        .shortIdentifier('c')
+                        .description(LocalizableMessage.raw("Number of connections"))
+                        .lowerBound(1)
+                        .defaultValue(1)
+                        .valuePlaceholder(LocalizableMessage.raw("{numConnections}"))
+                        .buildAndAddToParser(argParser);
+        maxIterationsArgument =
+                IntegerArgument.builder("maxIterations")
+                        .shortIdentifier('m')
+                        .description(LocalizableMessage.raw("Max iterations, 0 for unlimited"))
+                        .defaultValue(0)
+                        .valuePlaceholder(LocalizableMessage.raw("{maxIterations}"))
+                        .buildAndAddToParser(argParser);
+        maxDurationArgument =
+                IntegerArgument.builder("maxDuration")
+                        .shortIdentifier('d')
+                        .description(LocalizableMessage.raw("Maximum duration in seconds, 0 for unlimited"))
+                        .lowerBound(1)
+                        .defaultValue(0)
+                        .valuePlaceholder(LocalizableMessage.raw("{maxDuration}"))
+                        .buildAndAddToParser(argParser);
+        warmUpArgument =
+                IntegerArgument.builder("warmUpDuration")
+                        .shortIdentifier('B')
+                        .description(LocalizableMessage.raw("Warm up duration in seconds"))
+                        .defaultValue(0)
+                        .valuePlaceholder(LocalizableMessage.raw("{warmUpDuration}"))
+                        .buildAndAddToParser(argParser);
+        statsIntervalArgument =
+                IntegerArgument.builder("statInterval")
+                        .shortIdentifier('i')
+                        .description(LocalizableMessage.raw("Display results each specified number of seconds"))
+                        .lowerBound(1)
+                        .defaultValue(5)
+                        .valuePlaceholder(LocalizableMessage.raw("{statInterval}"))
+                        .buildAndAddToParser(argParser);
+        targetThroughputArgument =
+                IntegerArgument.builder("targetThroughput")
+                        .shortIdentifier('M')
+                        .description(LocalizableMessage.raw("Target average throughput to achieve"))
+                        .defaultValue(0)
+                        .valuePlaceholder(LocalizableMessage.raw("{targetThroughput}"))
+                        .buildAndAddToParser(argParser);
+        percentilesArgument =
+                IntegerArgument.builder("percentile")
+                        .shortIdentifier('e')
+                        .description(
+                                LocalizableMessage.raw("Calculate max response time for a percentile of operations"))
+                        .multiValued()
+                        .range(0, 100)
+                        .valuePlaceholder(LocalizableMessage.raw("{percentile}"))
+                        .buildAndAddToParser(argParser);
+        keepConnectionsOpen =
+                BooleanArgument.builder("keepConnectionsOpen")
+                        .shortIdentifier('f')
+                        .description(LocalizableMessage.raw("Keep connections open"))
+                        .buildAndAddToParser(argParser);
+        noRebindArgument =
+                BooleanArgument.builder("noRebind")
+                        .shortIdentifier('F')
+                        .description(LocalizableMessage.raw("Keep connections open and do not rebind"))
+                        .buildArgument();
+        if (options.supportsRebind()) {
+            argParser.addArgument(noRebindArgument);
+        }
+
+        arguments =
+                StringArgument.builder("argument")
+                        .shortIdentifier('g')
+                        .description(LocalizableMessage
+                                .raw("Argument used to evaluate the Java "
+                                        + "style format strings in program parameters (ie. Base DN, "
+                                        + "Search Filter). The set of all arguments provided form the "
+                                        + "the argument list in order. Besides static string "
+                                        + "arguments, they can be generated per iteration with the "
+                                        + "following functions: " + StaticUtils.EOL
+                                        + DataSource.getUsage()))
+                        .multiValued()
+                        .valuePlaceholder(LocalizableMessage.raw("{generator function or static string}"))
+                        .buildArgument();
+        if (options.supportsGeneratorArgument()) {
+            argParser.addArgument(arguments);
+        }
+    }
+
+    @Override
+    public void handleConnectionClosed() {
+        // Ignore
+    }
+
+    @Override
+    public synchronized void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) {
+        if (!stopRequested) {
+            app.errPrintln(ERROR_RATE_TOOLS_CANNOT_GET_CONNECTION.get(error.getMessage()));
+            if (error.getCause() != null && app.isVerbose()) {
+                error.getCause().printStackTrace(app.getErrorStream());
+            }
+            stopTool(true);
+        }
+    }
+
+    @Override
+    public void handleUnsolicitedNotification(final ExtendedResult notification) {
+        // Ignore
+    }
+
+    public final void validate() throws ArgumentException {
+        numConnections = numConnectionsArgument.getIntValue();
+        numThreads = numThreadsArgument.getIntValue();
+        warmUpDurationMs = warmUpArgument.getIntValue() * 1000L;
+        maxIterations = maxIterationsArgument.getIntValue() / numConnections / numThreads;
+        maxDurationTimeMs = maxDurationArgument.getIntValue() * 1000L;
+        statsIntervalMs = statsIntervalArgument.getIntValue() * 1000;
+        targetThroughput = targetThroughputArgument.getIntValue();
+
+        noRebind = noRebindArgument.isPresent();
+
+        if (!noRebindArgument.isPresent() && this.numThreads > 1) {
+            throw new ArgumentException(ERR_TOOL_ARG_MUST_BE_USED_WHEN_ARG_CONDITION.get(
+                "--" + noRebindArgument.getLongIdentifier(), "--" + numThreadsArgument.getLongIdentifier(), "> 1"));
+        }
+
+        if (maxIterationsArgument.isPresent() && maxIterations <= 0) {
+            throw new ArgumentException(ERR_TOOL_NOT_ENOUGH_ITERATIONS.get(
+                "--" + maxIterationsArgument.getLongIdentifier(), numConnections * numThreads,
+                numConnectionsArgument.getLongIdentifier(), numThreadsArgument.getLongIdentifier()));
+        }
+
+        dataSourcePrototypes = DataSource.parse(arguments.getValues());
+    }
+
+    final DataSource[] getDataSources() {
+        if (dataSourcePrototypes == null) {
+            throw new IllegalStateException("dataSources are null - validate() must be called first");
+        }
+        return dataSourcePrototypes;
+    }
+
+    abstract WorkerThread newWorkerThread(final Connection connection, final ConnectionFactory connectionFactory);
+    abstract StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app);
+
+    TimerThread newEndTimerThread(final long timeToWait) {
+        return new TimerThread(timeToWait);
+    }
+
+    final int run(final ConnectionFactory connectionFactory) {
+        final List<Connection> connections = new ArrayList<>();
+        statsThread = newStatsThread(this, app);
+
+        try {
+            validateCanConnectToServer(connectionFactory);
+            for (int i = 0; i < numConnections; i++) {
+                Connection connection = null;
+                if (keepConnectionsOpen.isPresent() || noRebindArgument.isPresent()) {
+                    connection = connectionFactory.getConnection();
+                    connection.addConnectionEventListener(this);
+                    connections.add(connection);
+                }
+                for (int j = 0; j < numThreads; j++) {
+                    final Thread thread = newWorkerThread(connection, connectionFactory);
+                    workerThreads.add(thread);
+                    thread.start();
+                }
+            }
+
+            if (maxDurationTimeMs > 0) {
+                newEndTimerThread(maxDurationTimeMs).start();
+            }
+
+            statsThread.startReporting();
+            joinAllWorkerThreads();
+            stopTool();
+        } catch (final InterruptedException e) {
+            stopTool(true);
+        } catch (final LdapException e) {
+            stopTool(true);
+            printErrorMessage(app, e);
+            return e.getResult().getResultCode().intValue();
+        } finally {
+            closeSilently(connections);
+        }
+
+        return 0;
+    }
+
+    // detects wrong bind parameters, server unreachable (server down, network problem?), etc.
+    private void validateCanConnectToServer(ConnectionFactory connectionFactory) throws LdapException {
+        connectionFactory.getConnection().close();
+    }
+
+    synchronized void stopTool() {
+        stopTool(false);
+    }
+
+    synchronized void stopTool(final boolean stoppedByError) {
+        if (!stopRequested) {
+            stopRequested = true;
+            statsThread.stopRecording(stoppedByError);
+        }
+    }
+
+    void setBindRequest(final BindRequest request) {
+        this.bindRequest = request;
+    }
+
+    protected void joinAllWorkerThreads() throws InterruptedException {
+        for (final Thread t : workerThreads) {
+            t.join();
+        }
+    }
+
+    double[] getPercentiles() {
+        if (percentilesArgument.isPresent()) {
+            double[] percentiles = new double[percentilesArgument.getValues().size()];
+            int index = 0;
+            for (final String percentile : percentilesArgument.getValues()) {
+                percentiles[index++] = Double.parseDouble(percentile);
+            }
+            Arrays.sort(percentiles);
+            return percentiles;
+        }
+
+        return DEFAULT_PERCENTILES;
+    }
+
+    long getWarmUpDurationMs() {
+        return warmUpDurationMs;
+    }
+
+    long getStatsInterval() {
+        return statsIntervalMs;
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
new file mode 100644
index 0000000..42b4032
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Portions Copyright 2014-2016 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.tools;
+
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.ConsoleApplication;
+
+/**
+ * SDK Performance Runner options wrapper.
+ */
+class PerformanceRunnerOptions {
+    private ArgumentParser argParser;
+    private ConsoleApplication app;
+
+    private boolean supportsRebind = true;
+    private boolean supportsMultipleThreadsPerConnection = true;
+    private boolean supportsGeneratorArgument = true;
+
+    PerformanceRunnerOptions(ArgumentParser argParser, ConsoleApplication app) {
+        this.argParser = argParser;
+        this.app = app;
+    }
+
+    boolean supportsRebind() {
+        return supportsRebind;
+    }
+
+    void setSupportsRebind(boolean supportsRebind) {
+        this.supportsRebind = supportsRebind;
+    }
+
+    boolean supportsMultipleThreadsPerConnection() {
+        return supportsMultipleThreadsPerConnection;
+    }
+
+    void setSupportsMultipleThreadsPerConnection(boolean supportsMultipleThreadsPerConnection) {
+        this.supportsMultipleThreadsPerConnection = supportsMultipleThreadsPerConnection;
+    }
+
+    boolean supportsGeneratorArgument() {
+        return supportsGeneratorArgument;
+    }
+
+    void setSupportsGeneratorArgument(boolean supportsGeneratorArgument) {
+        this.supportsGeneratorArgument = supportsGeneratorArgument;
+    }
+
+    ArgumentParser getArgumentParser() {
+        return argParser;
+    }
+
+    ConsoleApplication getConsoleApplication() {
+        return app;
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
new file mode 100644
index 0000000..b0f839b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
@@ -0,0 +1,317 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2011-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.MultiColumnPrinter.column;
+import static com.forgerock.opendj.cli.ToolVersionHandler.newSdkVersionHandler;
+import static com.forgerock.opendj.cli.Utils.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.CommonArguments.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.codahale.metrics.RatioGauge;
+import com.forgerock.opendj.cli.MultiColumnPrinter;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.promise.Promise;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A load generation tool that can be used to load a Directory Server with
+ * Search requests using one or more LDAP connections.
+ */
+public final class SearchRate extends ConsoleApplication {
+    private final class SearchPerformanceRunner extends PerformanceRunner {
+        private final class SearchStatsHandler extends UpdateStatsResultHandler<Result> implements SearchResultHandler {
+            private SearchStatsHandler(final long startTime) {
+                super(startTime);
+            }
+
+            @Override
+            public boolean handleEntry(final SearchResultEntry entry) {
+                entryCount.inc();
+                return true;
+            }
+
+            @Override
+            public boolean handleReference(final SearchResultReference reference) {
+                return true;
+            }
+        }
+
+        private final class SearchStatsThread extends StatsThread {
+            private static final int ENTRIES_PER_SEARCH_COLUMN_WIDTH = 5;
+            private static final String ENTRIES_PER_SEARCH = STAT_ID_PREFIX + "entries_per_search";
+
+            private SearchStatsThread(final PerformanceRunner perfRunner, final ConsoleApplication app) {
+                super(perfRunner, app);
+            }
+
+            @Override
+            void resetAdditionalStats() {
+                entryCount = newIntervalCounter();
+            }
+
+            @Override
+            List<MultiColumnPrinter.Column> registerAdditionalColumns() {
+                registry.register(ENTRIES_PER_SEARCH, new RatioGauge() {
+                    @Override
+                    protected Ratio getRatio() {
+                        return Ratio.of(entryCount.refreshIntervalCount(), successCount.getLastIntervalCount());
+                    }
+                });
+                return Collections.singletonList(
+                        column(ENTRIES_PER_SEARCH, "Entries/Srch", ENTRIES_PER_SEARCH_COLUMN_WIDTH, 1));
+            }
+        }
+
+        private final class SearchWorkerThread extends WorkerThread {
+            private SearchRequest sr;
+            private Object[] data;
+
+            private SearchWorkerThread(final Connection connection,
+                    final ConnectionFactory connectionFactory) {
+                super(connection, connectionFactory);
+            }
+
+            @Override
+            public Promise<?, LdapException> performOperation(final Connection connection,
+                    final DataSource[] dataSources, final long currentTimeNs) {
+                if (sr == null) {
+                    if (dataSources == null) {
+                        sr = Requests.newSearchRequest(baseDN, scope, filter, attributes);
+                    } else {
+                        data = DataSource.generateData(dataSources, data);
+                        sr =
+                                Requests.newSearchRequest(String.format(baseDN, data), scope,
+                                        String.format(filter, data), attributes);
+                    }
+                    sr.setDereferenceAliasesPolicy(dereferencesAliasesPolicy);
+                } else if (dataSources != null) {
+                    data = DataSource.generateData(dataSources, data);
+                    sr.setFilter(String.format(filter, data));
+                    sr.setName(String.format(baseDN, data));
+                }
+
+                final SearchStatsHandler handler = new SearchStatsHandler(currentTimeNs);
+                incrementIterationCount();
+                return connection.searchAsync(sr, handler).thenOnResult(handler).thenOnException(handler);
+            }
+        }
+
+        private String filter;
+        private String baseDN;
+        private SearchScope scope;
+        private DereferenceAliasesPolicy dereferencesAliasesPolicy;
+        private String[] attributes;
+
+        private SearchPerformanceRunner(final PerformanceRunnerOptions options)
+                throws ArgumentException {
+            super(options);
+        }
+
+        @Override
+        WorkerThread newWorkerThread(final Connection connection,
+                final ConnectionFactory connectionFactory) {
+            return new SearchWorkerThread(connection, connectionFactory);
+        }
+
+        @Override
+        StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) {
+            return new SearchStatsThread(performanceRunner, app);
+        }
+    }
+
+    /**
+     * The main method for SearchRate tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new SearchRate().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+    private BooleanArgument scriptFriendly;
+    private StatsThread.IntervalCounter entryCount = StatsThread.newIntervalCounter();
+
+    private SearchRate() {
+        // Nothing to do.
+    }
+
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    @Override
+    public boolean isScriptFriendly() {
+        return scriptFriendly.isPresent();
+    }
+
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    private int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_SEARCHRATE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+                new ArgumentParser(SearchRate.class.getName(), toolDescription, false, true, 1, 0,
+                        "[filter format string] [attributes ...]");
+        argParser.setVersionHandler(newSdkVersionHandler());
+        argParser.setShortToolDescription(REF_SHORT_DESC_SEARCHRATE.get());
+        argParser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_RATE_TOOLS.get());
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        SearchPerformanceRunner runner;
+
+        StringArgument baseDN;
+        MultiChoiceArgument<SearchScope> searchScope;
+        MultiChoiceArgument<DereferenceAliasesPolicy> dereferencePolicy;
+        BooleanArgument showUsage;
+        StringArgument propertiesFileArgument;
+        BooleanArgument noPropertiesFileArgument;
+        try {
+            Utils.setDefaultPerfToolProperties();
+
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            runner = new SearchPerformanceRunner(new PerformanceRunnerOptions(argParser, this));
+
+            propertiesFileArgument = propertiesFileArgument();
+            argParser.addArgument(propertiesFileArgument);
+            argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+            noPropertiesFileArgument = noPropertiesFileArgument();
+            argParser.addArgument(noPropertiesFileArgument);
+            argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+            showUsage = showUsageArgument();
+            argParser.addArgument(showUsage);
+            argParser.setUsageArgument(showUsage, getOutputStream());
+
+            baseDN =
+                    StringArgument.builder(OPTION_LONG_BASEDN)
+                            .shortIdentifier(OPTION_SHORT_BASEDN)
+                            .description(INFO_SEARCHRATE_TOOL_DESCRIPTION_BASEDN.get())
+                            .required()
+                            .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            searchScope = searchScopeArgument();
+            argParser.addArgument(searchScope);
+
+            dereferencePolicy =
+                    MultiChoiceArgument.<DereferenceAliasesPolicy>builder("dereferencePolicy")
+                            .shortIdentifier('a')
+                            .description(INFO_SEARCH_DESCRIPTION_DEREFERENCE_POLICY.get())
+                            .allowedValues(DereferenceAliasesPolicy.values())
+                            .defaultValue(DereferenceAliasesPolicy.NEVER)
+                            .valuePlaceholder(INFO_DEREFERENCE_POLICE_PLACEHOLDER.get())
+                            .buildAndAddToParser(argParser);
+
+            verbose = verboseArgument();
+            argParser.addArgument(verbose);
+
+            scriptFriendly =
+                    BooleanArgument.builder("scriptFriendly")
+                            .shortIdentifier('S')
+                            .description(INFO_DESCRIPTION_SCRIPT_FRIENDLY.get())
+                            .buildAndAddToParser(argParser);
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            // If we should just display usage or version information,
+            // then print it and exit.
+            if (argParser.usageOrVersionDisplayed()) {
+                return 0;
+            }
+
+            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
+            runner.validate();
+        } catch (final ArgumentException ae) {
+            argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final List<String> attributes = new LinkedList<>();
+        final ArrayList<String> filterAndAttributeStrings = argParser.getTrailingArguments();
+        if (!filterAndAttributeStrings.isEmpty()) {
+            /* The list of trailing arguments should be structured as follow:
+             the first trailing argument is considered the filter, the other as attributes.*/
+            runner.filter = filterAndAttributeStrings.remove(0);
+            // The rest are attributes
+            attributes.addAll(filterAndAttributeStrings);
+        }
+        runner.attributes = attributes.toArray(new String[attributes.size()]);
+        runner.baseDN = baseDN.getValue();
+        try {
+            runner.scope = searchScope.getTypedValue();
+            runner.dereferencesAliasesPolicy = dereferencePolicy.getTypedValue();
+        } catch (final ArgumentException ex1) {
+            errPrintln(ex1.getMessageObject());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        try {
+            /* Try it out to make sure the format string and data sources match. */
+            final Object[] data = DataSource.generateData(runner.getDataSources(), null);
+            String.format(runner.filter, data);
+            String.format(runner.baseDN, data);
+        } catch (final Exception ex1) {
+            errPrintln(LocalizableMessage.raw("Error formatting filter or base DN: " + ex1));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        return runner.run(connectionFactory);
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/StatsThread.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/StatsThread.java
new file mode 100644
index 0000000..573ca87
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/StatsThread.java
@@ -0,0 +1,418 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.MultiColumnPrinter.column;
+import static com.forgerock.opendj.cli.MultiColumnPrinter.separatorColumn;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_TOOL_WARMING_UP;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.RatioGauge;
+import com.codahale.metrics.ScheduledReporter;
+import com.codahale.metrics.Timer;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.MultiColumnPrinter;
+import org.mpierce.metrics.reservoir.hdrhistogram.HdrHistogramReservoir;
+
+/**
+ * Statistics thread base implementation.
+ * <p>
+ * The goal of this class is to compute and print rate tool general statistics.
+ */
+class StatsThread extends Thread {
+
+    static final String STAT_ID_PREFIX = "org.forgerock.opendj.";
+
+    private static final String TIME_NOW = STAT_ID_PREFIX + "current_time";
+    private static final String RECENT_THROUGHPUT = STAT_ID_PREFIX + "recent_throughput";
+    private static final String AVERAGE_THROUGHPUT = STAT_ID_PREFIX + "average_throughput";
+    private static final String RECENT_RESPONSE_TIME_MS = STAT_ID_PREFIX + "recent_response_time";
+    private static final String AVERAGE_RESPONSE_TIME_MS = STAT_ID_PREFIX + "average_response_time";
+    private static final String PERCENTILES = STAT_ID_PREFIX + "percentiles";
+    private static final String ERROR_PER_SECOND = STAT_ID_PREFIX + "error_per_second";
+
+    public static final double MS_IN_S = TimeUnit.SECONDS.toMillis(1);
+    public static final double NS_IN_MS = TimeUnit.MILLISECONDS.toNanos(1);
+
+    private abstract class RateReporter extends ScheduledReporter {
+        final MultiColumnPrinter printer;
+
+        private RateReporter() {
+            super(registry, "", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS);
+            printer = createPrinter();
+        }
+
+        abstract MultiColumnPrinter createPrinter();
+        abstract void printTitle();
+
+        @Override
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        public void report(final SortedMap<String, Gauge> gauges,
+                           final SortedMap<String, Counter> counters,
+                           final SortedMap<String, Histogram> histograms,
+                           final SortedMap<String, Meter> meters,
+                           final SortedMap<String, Timer> timers) {
+            int percentileIndex = 0;
+            for (final MultiColumnPrinter.Column column : printer.getColumns()) {
+                final String statKey = column.getId();
+                if (gauges.containsKey(statKey)) {
+                    printer.printData(((Gauge<Double>) gauges.get(statKey)).getValue());
+                } else if (statKey.startsWith(PERCENTILES)) {
+                    final double quantile = percentiles[percentileIndex++] / 100.0;
+                    printer.printData(
+                            histograms.get(PERCENTILES).getSnapshot().getValue(quantile) / MILLISECONDS.toNanos(1));
+                } else {
+                    printer.printData("-");
+                }
+            }
+        }
+    }
+
+    private final class ConsoleRateReporter extends RateReporter {
+        private static final int STANDARD_WIDTH = 8;
+        private List<MultiColumnPrinter.Column> additionalColumns;
+
+        @Override
+        void printTitle() {
+            final int throughputRawSpan = 2;
+            final int responseTimeRawSpan = 2 + percentiles.length;
+            final int additionalStatsRawSpan = 1 + additionalColumns.size();
+
+            printer.printDashedLine();
+            printer.printTitleSection("Throughput", throughputRawSpan);
+            printer.printTitleSection("Response Time", responseTimeRawSpan);
+            printer.printTitleSection(additionalStatsRawSpan > 1 ? "Additional" : "", additionalStatsRawSpan);
+            printer.printTitleSection("(ops/second)", throughputRawSpan);
+            printer.printTitleSection("(milliseconds)", responseTimeRawSpan);
+            printer.printTitleSection(additionalStatsRawSpan > 1 ? "Statistics" : "", additionalStatsRawSpan);
+            printer.printTitleLine();
+            printer.printDashedLine();
+        }
+
+        @Override
+        MultiColumnPrinter createPrinter() {
+            final List<MultiColumnPrinter.Column> columns = new ArrayList<>();
+            // Throughput (ops/sec)
+            columns.add(separatorColumn());
+            columns.add(column(RECENT_THROUGHPUT, "recent", STANDARD_WIDTH, 1));
+            columns.add(column(AVERAGE_THROUGHPUT, "average", STANDARD_WIDTH, 1));
+            // Response Time (ms)
+            columns.add(separatorColumn());
+            columns.add(column(RECENT_RESPONSE_TIME_MS, "recent", STANDARD_WIDTH, 3));
+            columns.add(column(AVERAGE_RESPONSE_TIME_MS, "average", STANDARD_WIDTH, 3));
+            for (double percentile : percentiles) {
+                columns.add(column(PERCENTILES + percentile, percentile + "%", STANDARD_WIDTH, 2));
+            }
+            // Additional stats
+            columns.add(separatorColumn());
+            columns.add(column(ERROR_PER_SECOND, "err/sec", STANDARD_WIDTH, 1));
+            additionalColumns = registerAdditionalColumns();
+            if (!additionalColumns.isEmpty()) {
+                columns.addAll(additionalColumns);
+            }
+            columns.add(separatorColumn());
+
+
+            return MultiColumnPrinter.builder(app.getOutputStream(), columns)
+                    .format(true)
+                    .titleAlignment(MultiColumnPrinter.Alignment.CENTER)
+                    .build();
+        }
+    }
+
+    private final class CsvRateReporter extends RateReporter {
+        @Override
+        void printTitle() {
+            printer.printTitleLine();
+        }
+
+        @Override
+        MultiColumnPrinter createPrinter() {
+            final List<MultiColumnPrinter.Column> columns = new ArrayList<>();
+            columns.add(column(TIME_NOW, "time", 3));
+            columns.add(column(RECENT_THROUGHPUT, "recent throughput", 1));
+            columns.add(column(AVERAGE_THROUGHPUT, "average throughput", 1));
+            columns.add(column(RECENT_RESPONSE_TIME_MS, "recent response time", 3));
+            columns.add(column(AVERAGE_RESPONSE_TIME_MS, "average response time", 3));
+            for (double percentile : percentiles) {
+                columns.add(column(
+                        PERCENTILES + percentile, percentile + "% response time", 2));
+            }
+            columns.add(column(ERROR_PER_SECOND, "errors/second", 1));
+            columns.addAll(registerAdditionalColumns());
+
+
+            return MultiColumnPrinter.builder(app.getOutputStream(), columns)
+                    .columnSeparator(",")
+                    .build();
+        }
+    }
+
+    /** A timer to prevent adding temporary variables in {@link StatsThread#run()}. **/
+    private static abstract class StatsTimer {
+        private long startTimeMeasure;
+        private long elapsed;
+
+        abstract long getInstantTime();
+
+        private void start() {
+            startTimeMeasure = getInstantTime();
+        }
+
+        private long reset() {
+            final long time = getInstantTime();
+            elapsed = time - this.startTimeMeasure;
+            this.startTimeMeasure = time;
+
+            return elapsed;
+        }
+
+        private long elapsed() {
+            return elapsed;
+        }
+    }
+
+    /** A counter to prevent adding temporary variables in {@link StatsThread#run()}. **/
+    static final class IntervalCounter extends Counter {
+        private long lastIntervalCount;
+        private long lastTotalCount;
+
+        long refreshIntervalCount() {
+            final long totalCount = getCount();
+            lastIntervalCount = totalCount - lastTotalCount;
+            lastTotalCount = totalCount;
+            return lastIntervalCount;
+        }
+
+        long getLastIntervalCount() {
+            return lastIntervalCount;
+        }
+
+        long getLastTotalCount() {
+            return lastTotalCount;
+        }
+    }
+
+    static final IntervalCounter newIntervalCounter() {
+        return new IntervalCounter();
+    }
+
+
+    final MetricRegistry registry = new MetricRegistry();
+    private final Histogram responseTimes = new Histogram(new HdrHistogramReservoir());
+
+    private final StatsTimer gcTimerMs = new StatsTimer() {
+        private final List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
+
+        @Override
+        long getInstantTime() {
+            long gcDurationMs = 0;
+            for (final GarbageCollectorMXBean bean : gcBeans) {
+                gcDurationMs += bean.getCollectionTime();
+            }
+
+            return gcDurationMs;
+        }
+    };
+
+    private final StatsTimer timerMs = new StatsTimer() {
+        @Override
+        long getInstantTime() {
+            return System.currentTimeMillis();
+        }
+    };
+
+    IntervalCounter waitDurationNsCount;
+    IntervalCounter successCount;
+    private IntervalCounter operationCount;
+    private IntervalCounter errorCount;
+    private IntervalCounter durationMsCount;
+
+    private final ConsoleApplication app;
+    private final double[] percentiles;
+    private final PerformanceRunner performanceRunner;
+    private final RateReporter reporter;
+    private long startTimeMs;
+    private volatile boolean warmingUp;
+    private final ScheduledExecutorService statThreadScheduler = Executors.newSingleThreadScheduledExecutor();
+
+    StatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication application) {
+        super("Stats Thread");
+        resetStats();
+        this.performanceRunner = performanceRunner;
+        this.app = application;
+        this.percentiles = performanceRunner.getPercentiles();
+        this.reporter = app.isScriptFriendly() ? new CsvRateReporter()
+                                               : new ConsoleRateReporter();
+        registerStats();
+    }
+
+    /** Resets both general and recent statistic indicators. */
+    final void resetStats() {
+        errorCount = newIntervalCounter();
+        operationCount = newIntervalCounter();
+        successCount = newIntervalCounter();
+        waitDurationNsCount = newIntervalCounter();
+        durationMsCount = newIntervalCounter();
+        resetAdditionalStats();
+    }
+
+    private void registerStats() {
+        if (app.isScriptFriendly()) {
+            registry.register(TIME_NOW, new RatioGauge() {
+                @Override
+                protected Ratio getRatio() {
+                    return Ratio.of(System.currentTimeMillis() - startTimeMs, MS_IN_S);
+                }
+            });
+        }
+
+        registry.register(RECENT_THROUGHPUT, new RatioGauge() {
+            @Override
+            protected Ratio getRatio() {
+                return Ratio.of(successCount.getLastIntervalCount() + errorCount.getLastIntervalCount(),
+                                durationMsCount.getLastIntervalCount() / MS_IN_S);
+            }
+        });
+
+        registry.register(AVERAGE_THROUGHPUT, new RatioGauge() {
+            @Override
+            protected Ratio getRatio() {
+                return Ratio.of(successCount.getLastTotalCount() + errorCount.getLastTotalCount(),
+                                durationMsCount.getLastTotalCount() / MS_IN_S);
+            }
+        });
+
+        registry.register(RECENT_RESPONSE_TIME_MS, new RatioGauge() {
+            @Override
+            protected Ratio getRatio() {
+                return Ratio.of((waitDurationNsCount.getLastIntervalCount() / NS_IN_MS) - gcTimerMs.elapsed(),
+                                successCount.getLastIntervalCount() + errorCount.getLastIntervalCount());
+            }
+        });
+
+        registry.register(AVERAGE_RESPONSE_TIME_MS, new RatioGauge() {
+            @Override
+            protected Ratio getRatio() {
+                return Ratio.of((waitDurationNsCount.getLastTotalCount() / NS_IN_MS) - gcTimerMs.getInstantTime(),
+                                 successCount.getLastTotalCount() + errorCount.getLastTotalCount());
+            }
+        });
+
+        registry.register(ERROR_PER_SECOND, new RatioGauge() {
+            @Override
+            protected Ratio getRatio() {
+                return Ratio.of(errorCount.getLastIntervalCount(), durationMsCount.getLastIntervalCount() / MS_IN_S);
+            }
+        });
+        registry.register(PERCENTILES, responseTimes);
+    }
+
+    void startReporting() throws InterruptedException {
+        warmUp();
+        init();
+        final long statsIntervalMs = performanceRunner.getStatsInterval();
+        statThreadScheduler.scheduleAtFixedRate(this, statsIntervalMs, statsIntervalMs, TimeUnit.MILLISECONDS);
+    }
+
+    private void warmUp() throws InterruptedException {
+        final long warmUpDurationMs = performanceRunner.getWarmUpDurationMs();
+        if (warmUpDurationMs > 0) {
+            if (!app.isScriptFriendly()) {
+                app.println(INFO_TOOL_WARMING_UP.get(warmUpDurationMs / TimeUnit.SECONDS.toMillis(1)));
+            }
+            Thread.sleep(warmUpDurationMs);
+            resetStats();
+        }
+        warmingUp = false;
+    }
+
+    private void init() {
+        reporter.printTitle();
+        timerMs.start();
+        gcTimerMs.start();
+        startTimeMs = System.currentTimeMillis();
+    }
+
+    public void stopRecording(final boolean stoppedByError) {
+        statThreadScheduler.shutdown();
+        if (!stoppedByError) {
+            // If stats thread is printing stats, wait for it to finish and print a last line of stats.
+            try {
+                statThreadScheduler.awaitTermination(50, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException ignored) {
+                // Do nothing.
+            }
+            run();
+        }
+    }
+
+    /** Performs stat snapshots and reports results to application. */
+    @Override
+    public void run() {
+        durationMsCount.inc(timerMs.reset() - gcTimerMs.reset());
+        durationMsCount.refreshIntervalCount();
+        operationCount.refreshIntervalCount();
+        successCount.refreshIntervalCount();
+        errorCount.refreshIntervalCount();
+        waitDurationNsCount.refreshIntervalCount();
+
+        reporter.report();
+    }
+
+    void addResponseTime(final long responseTimeNs) {
+        if (!warmingUp) {
+            waitDurationNsCount.inc(responseTimeNs);
+            responseTimes.update(responseTimeNs);
+        }
+    }
+
+    void incrementFailedCount() {
+        errorCount.inc();
+    }
+
+    void incrementSuccessCount() {
+        successCount.inc();
+    }
+
+    void incrementOperationCount() {
+        operationCount.inc();
+    }
+
+    /** Child classes which manage additional stats need to override this method. */
+    List<MultiColumnPrinter.Column> registerAdditionalColumns() {
+        return Collections.emptyList();
+    }
+
+    /** Do nothing by default, child classes which manage additional stats need to override this method. */
+    void resetAdditionalStats() { }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java
new file mode 100644
index 0000000..8b0a853
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java
@@ -0,0 +1,267 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2006-2010 Sun Microsystems, Inc.
+ * Portions copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.Utils.readBytesFromFile;
+import static com.forgerock.opendj.cli.Utils.secondsToTimeString;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl;
+import org.forgerock.opendj.ldap.controls.AuthorizationIdentityResponseControl;
+import org.forgerock.opendj.ldap.controls.GenericControl;
+import org.forgerock.opendj.ldap.controls.GetEffectiveRightsRequestControl;
+import org.forgerock.opendj.ldap.controls.PasswordExpiredResponseControl;
+import org.forgerock.opendj.ldap.controls.PasswordExpiringResponseControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyErrorType;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyRequestControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyResponseControl;
+import org.forgerock.opendj.ldap.controls.PasswordPolicyWarningType;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.responses.BindResult;
+
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.ldap.controls.AccountUsabilityRequestControl;
+import com.forgerock.opendj.util.StaticUtils;
+
+/**
+ * This class provides utility functions for all the client side tools.
+ */
+final class Utils {
+
+    /**
+     * Parse the specified command line argument to create the appropriate
+     * LDAPControl. The argument string should be in the format
+     * controloid[:criticality[:value|::b64value|:&lt;fileurl]]
+     *
+     * @param argString
+     *            The argument string containing the encoded control
+     *            information.
+     * @return The control decoded from the provided string, or
+     *         <CODE>null</CODE> if an error occurs while parsing the argument
+     *         value.
+     * @throws org.forgerock.opendj.ldap.DecodeException
+     *             If an error occurs.
+     */
+    static GenericControl getControl(final String argString) throws DecodeException {
+        String controlOID = null;
+        boolean controlCriticality = false;
+        ByteString controlValue = null;
+
+        int idx = argString.indexOf(":");
+
+        if (idx < 0) {
+            controlOID = argString;
+        } else {
+            controlOID = argString.substring(0, idx);
+        }
+
+        final String lowerOID = StaticUtils.toLowerCase(controlOID);
+        if ("accountusable".equals(lowerOID) || "accountusability".equals(lowerOID)) {
+            controlOID = AccountUsabilityRequestControl.OID;
+        } else if ("authzid".equals(lowerOID) || "authorizationidentity".equals(lowerOID)) {
+            controlOID = AuthorizationIdentityRequestControl.OID;
+        } else if ("noop".equals(lowerOID) || "no-op".equals(lowerOID)) {
+            // controlOID = OID_LDAP_NOOP_OPENLDAP_ASSIGNED;
+        } else if ("subentries".equals(lowerOID)) {
+            // controlOID = OID_LDAP_SUBENTRIES;
+        } else if ("managedsait".equals(lowerOID)) {
+            // controlOID = OID_MANAGE_DSAIT_CONTROL;
+        } else if ("pwpolicy".equals(lowerOID) || "passwordpolicy".equals(lowerOID)) {
+            controlOID = PasswordPolicyRequestControl.OID;
+        } else if ("subtreedelete".equals(lowerOID) || "treedelete".equals(lowerOID)) {
+            controlOID = SubtreeDeleteRequestControl.OID;
+        } else if ("realattrsonly".equals(lowerOID) || "realattributesonly".equals(lowerOID)) {
+            // controlOID = OID_REAL_ATTRS_ONLY;
+        } else if ("virtualattrsonly".equals(lowerOID) || "virtualattributesonly".equals(lowerOID)) {
+            // controlOID = OID_VIRTUAL_ATTRS_ONLY;
+        } else if ("effectiverights".equals(lowerOID) || "geteffectiverights".equals(lowerOID)) {
+            controlOID = GetEffectiveRightsRequestControl.OID;
+        }
+
+        if (idx < 0) {
+            return GenericControl.newControl(controlOID);
+        }
+
+        final String remainder = argString.substring(idx + 1, argString.length());
+
+        idx = remainder.indexOf(":");
+        if (idx == -1) {
+            if ("true".equalsIgnoreCase(remainder)) {
+                controlCriticality = true;
+            } else if ("false".equalsIgnoreCase(remainder)) {
+                controlCriticality = false;
+            } else {
+                // TODO: I18N
+                throw DecodeException.error(LocalizableMessage
+                        .raw("Invalid format for criticality value:" + remainder));
+            }
+            return GenericControl.newControl(controlOID, controlCriticality);
+
+        }
+
+        final String critical = remainder.substring(0, idx);
+        if ("true".equalsIgnoreCase(critical)) {
+            controlCriticality = true;
+        } else if ("false".equalsIgnoreCase(critical)) {
+            controlCriticality = false;
+        } else {
+            // TODO: I18N
+            throw DecodeException.error(LocalizableMessage
+                    .raw("Invalid format for criticality value:" + critical));
+        }
+
+        final String valString = remainder.substring(idx + 1, remainder.length());
+        if (valString.charAt(0) == ':') {
+            controlValue = ByteString.valueOfBase64(valString.substring(1, valString.length()));
+        } else if (valString.charAt(0) == '<') {
+            // Read data from the file.
+            final String filePath = valString.substring(1, valString.length());
+            try {
+                final byte[] val = readBytesFromFile(filePath);
+                controlValue = ByteString.wrap(val);
+            } catch (final Exception e) {
+                return null;
+            }
+        } else {
+            controlValue = ByteString.valueOfUtf8(valString);
+        }
+
+        return GenericControl.newControl(controlOID, controlCriticality, controlValue);
+    }
+
+    /**
+     * Prints a multi-line error message with the provided information to the
+     * given print stream.
+     *
+     * @param app
+     *            The console app to use to write the error message.
+     * @param ere
+     *            The error result.
+     * @return The error code.
+     */
+    static int printErrorMessage(final ConsoleApplication app, final LdapException ere) {
+         /* if ((ere.getMessage() != null) && (ere.getMessage().length() > 0)) {
+             app.println(LocalizableMessage.raw(ere.getMessage()));
+         }*/
+
+        if (ere.getResult().getResultCode().intValue() >= 0) {
+            app.errPrintln(ERR_TOOL_RESULT_CODE.get(ere.getResult().getResultCode().intValue(), ere
+                    .getResult().getResultCode().toString()));
+        }
+
+        if (ere.getResult().getDiagnosticMessage() != null
+                && ere.getResult().getDiagnosticMessage().length() > 0) {
+            app.errPrintln(ERR_TOOL_ERROR_MESSAGE.get(ere.getResult().getDiagnosticMessage()));
+        }
+
+        if (ere.getResult().getMatchedDN() != null && ere.getResult().getMatchedDN().length() > 0) {
+            app.errPrintln(ERR_TOOL_MATCHED_DN.get(ere.getResult().getMatchedDN()));
+        }
+
+        if (app.isVerbose() && ere.getResult().getCause() != null) {
+            ere.getResult().getCause().printStackTrace(app.getErrorStream());
+        }
+
+        return ere.getResult().getResultCode().intValue();
+    }
+
+    static void printPasswordPolicyResults(final ConsoleApplication app, final BindResult result) {
+        try {
+            final AuthorizationIdentityResponseControl control = result.getControl(
+                    AuthorizationIdentityResponseControl.DECODER, new DecodeOptions());
+            if (control != null) {
+                app.println(INFO_BIND_AUTHZID_RETURNED.get(control.getAuthorizationID()));
+            }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
+
+        try {
+            final PasswordExpiredResponseControl control = result.getControl(PasswordExpiredResponseControl.DECODER,
+                                                                             new DecodeOptions());
+            if (control != null) {
+                app.println(INFO_BIND_PASSWORD_EXPIRED.get());
+            }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
+
+        try {
+            final PasswordExpiringResponseControl control = result.getControl(PasswordExpiringResponseControl.DECODER,
+                                                                              new DecodeOptions());
+            if (control != null) {
+                final LocalizableMessage timeString = secondsToTimeString(control.getSecondsUntilExpiration());
+                app.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString));
+            }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
+
+        try {
+            final PasswordPolicyResponseControl control = result.getControl(PasswordPolicyResponseControl.DECODER,
+                                                                            new DecodeOptions());
+            if (control != null) {
+                final PasswordPolicyErrorType errorType = control.getErrorType();
+                if (errorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) {
+                    app.println(INFO_BIND_PASSWORD_EXPIRED.get());
+                } else if (errorType == PasswordPolicyErrorType.ACCOUNT_LOCKED) {
+                    app.println(INFO_BIND_ACCOUNT_LOCKED.get());
+                } else if (errorType == PasswordPolicyErrorType.CHANGE_AFTER_RESET) {
+
+                    app.println(INFO_BIND_MUST_CHANGE_PASSWORD.get());
+                }
+
+                final PasswordPolicyWarningType warningType = control.getWarningType();
+                if (warningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION) {
+                    final LocalizableMessage timeString = secondsToTimeString(control.getWarningValue());
+                    app.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString));
+                } else if (warningType == PasswordPolicyWarningType.GRACE_LOGINS_REMAINING) {
+                    app.println(INFO_BIND_GRACE_LOGINS_REMAINING.get(control.getWarningValue()));
+                }
+            }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
+    }
+
+    /**
+     * Sets default system property settings for the xxxrate performance tools.
+     */
+    static void setDefaultPerfToolProperties() {
+        // Use SameThreadStrategy by default.
+        if (System.getProperty("org.forgerock.opendj.transport.useWorkerThreads") == null) {
+            System.setProperty("org.forgerock.opendj.transport.useWorkerThreads", "false");
+        }
+
+        /* Configure connections to be terminate immediately after closing (this
+         prevents port exhaustion in xxxrate tools when
+         connecting/disconnecting).*/
+        if (System.getProperty("org.forgerock.opendj.transport.linger") == null) {
+            System.setProperty("org.forgerock.opendj.transport.linger", "0");
+        }
+    }
+
+    /** Prevent instantiation. */
+    private Utils() {
+        // Do nothing.
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/package-info.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/package-info.java
new file mode 100644
index 0000000..1b7848b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2009 Sun Microsystems, Inc.
+ */
+
+/**
+ * Classes implementing the OpenDJ SDK client tools.
+ */
+package com.forgerock.opendj.ldap.tools;
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
new file mode 100644
index 0000000..eb1b3ca
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
@@ -0,0 +1,372 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2010 Sun Microsystems, Inc.
+# Portions copyright 2012-2016 ForgeRock AS.
+
+ERROR_RATE_TOOLS_CANNOT_GET_CONNECTION=%s\nStopping...
+ERR_CANNOT_INITIALIZE_ARGS=An unexpected error occurred while \
+ attempting to initialize the command-line arguments:  %s
+ERR_ERROR_PARSING_ARGS=An error occurred while parsing the \
+ command-line arguments:  %s
+INFO_PROCESSING_OPERATION=Processing %s request for %s
+INFO_OPERATION_FAILED=%s operation failed
+INFO_OPERATION_SUCCESSFUL=%s operation successful for DN %s
+INFO_PROCESSING_COMPARE_OPERATION=Comparing type %s with value %s in \
+ entry %s
+INFO_COMPARE_OPERATION_RESULT_FALSE=Compare operation returned false for \
+ entry %s
+INFO_COMPARE_OPERATION_RESULT_TRUE=Compare operation returned true for \
+ entry %s
+INFO_DESCRIPTION_CONTROLS=Use a request control with the provided \
+ information
+INFO_SEARCH_DESCRIPTION_BASEDN=Search base DN
+INFO_SEARCH_DESCRIPTION_SIZE_LIMIT=Maximum number of entries to return \
+ from the search
+INFO_SEARCH_DESCRIPTION_TIME_LIMIT=Maximum length of time in seconds to \
+ allow for the search
+INFO_SEARCH_DESCRIPTION_DEREFERENCE_POLICY=Alias dereference policy \
+ ('never', 'always', 'search', or 'find')
+ERR_DESCRIPTION_INVALID_VERSION=Invalid LDAP version number '%s'. \
+ Allowed values are 2 and 3
+ERR_SEARCH_NO_FILTERS=No filters specified for the search request
+INFO_DESCRIPTION_DONT_WRAP=Do not wrap long lines
+INFO_DESCRIPTION_TYPES_ONLY=Only retrieve attribute names but not their \
+ values
+INFO_DESCRIPTION_ASSERTION_FILTER=Use the LDAP assertion control with the \
+ provided filter
+ERR_LDAP_ASSERTION_INVALID_FILTER=The search filter provided for the \
+ LDAP assertion control was invalid:  %s
+INFO_DESCRIPTION_PREREAD_ATTRS=Use the LDAP ReadEntry pre-read control
+INFO_DESCRIPTION_POSTREAD_ATTRS=Use the LDAP ReadEntry post-read control
+INFO_LDAPMODIFY_PREREAD_ENTRY=Target entry before the operation:
+INFO_LDAPMODIFY_POSTREAD_ENTRY=Target entry after the operation:
+INFO_DESCRIPTION_PROXY_AUTHZID=Use the proxied authorization control with \
+ the given authorization ID
+INFO_DESCRIPTION_PSEARCH_INFO=Use the persistent search control
+ERR_PSEARCH_MISSING_DESCRIPTOR=The request to use the persistent \
+ search control did not include a descriptor that indicates the options to use \
+ with that control
+ERR_PSEARCH_DOESNT_START_WITH_PS=The persistent search descriptor %s \
+ did not start with the required 'ps' string
+ERR_PSEARCH_INVALID_CHANGE_TYPE=The provided change type value %s is \
+ invalid.  The recognized change types are add, delete, modify, modifydn, and \
+ any
+ERR_PSEARCH_INVALID_CHANGESONLY=The provided changesOnly value %s is \
+ invalid.  Allowed values are 1 to only return matching entries that have \
+ changed since the beginning of the search, or 0 to also include existing \
+ entries that match the search criteria
+ERR_PSEARCH_INVALID_RETURN_ECS=The provided returnECs value %s is \
+ invalid.  Allowed values are 1 to request that the entry change notification \
+ control be included in updated entries, or 0 to exclude the control from \
+ matching entries
+INFO_BIND_AUTHZID_RETURNED=# Bound with authorization ID %s
+INFO_SEARCH_DESCRIPTION_FILENAME=File containing a list of search filter \
+ strings
+INFO_DESCRIPTION_MATCHED_VALUES_FILTER=Use the LDAP matched values \
+ control with the provided filter
+ERR_LDAP_MATCHEDVALUES_INVALID_FILTER=The provided matched values \
+ filter was invalid:  %s
+ERR_LDIF_FILE_CANNOT_OPEN_FOR_READ=An error occurred while \
+ attempting to open the LDIF file %s for reading:  %s
+ERR_LDIF_FILE_CANNOT_OPEN_FOR_WRITE=An error occurred while \
+ attempting to open the LDIF file %s for writing:  %s
+ERR_LDIF_FILE_READ_ERROR=An error occurred while attempting to read \
+ the contents of LDIF file %s:  %s
+INFO_BIND_PASSWORD_EXPIRED=# Your password has expired
+INFO_BIND_PASSWORD_EXPIRING=# Your password will expire in %s
+INFO_BIND_ACCOUNT_LOCKED=# Your account has been locked
+INFO_BIND_MUST_CHANGE_PASSWORD=# You must change your password before any \
+ other operations will be allowed
+INFO_BIND_GRACE_LOGINS_REMAINING=# You have %d grace logins remaining
+INFO_LDAPPWMOD_DESCRIPTION_AUTHZID=Authorization ID for the \
+ user entry whose password should be changed. \
+ The authorization ID is a string having either \
+ the prefix "dn:" followed by the user's distinguished name, or \
+ the prefix "u:" followed by a user identifier \
+ that depends on the identity mapping used \
+ to match the user identifier to an entry in the directory. \
+ Examples include "dn:uid=bjensen,ou=People,dc=example,dc=com", and, \
+ if we assume that "bjensen" is mapped to Barbara Jensen's entry, "u:bjensen"
+INFO_LDAPPWMOD_DESCRIPTION_NEWPW=New password to provide \
+ for the target user
+INFO_LDAPPWMOD_DESCRIPTION_NEWPWFILE=Path to a file \
+ containing the new password to provide for the target user
+INFO_LDAPPWMOD_DESCRIPTION_CURRENTPW=Current password for \
+ the target user
+INFO_LDAPPWMOD_DESCRIPTION_CURRENTPWFILE=Path to a file \
+ containing the current password for the target user
+ERR_LDAPPWMOD_CONFLICTING_ARGS=The %s and %s arguments may not be \
+ provided together
+ERR_LDAPPWMOD_FAILED=The LDAP password modify operation failed: \
+ %d (%s)
+ERR_LDAPPWMOD_FAILURE_ERROR_MESSAGE=Error Message:  %s
+ERR_LDAPPWMOD_FAILURE_MATCHED_DN=Matched DN:  %s
+INFO_LDAPPWMOD_SUCCESSFUL=The LDAP password modify operation was \
+ successful
+INFO_LDAPPWMOD_ADDITIONAL_INFO=Additional Info:  %s
+INFO_LDAPPWMOD_GENERATED_PASSWORD=Generated Password:  %s
+INFO_COMPARE_CANNOT_BASE64_DECODE_ASSERTION_VALUE=The assertion value was \
+ indicated to be base64-encoded, but an error occurred while trying to decode \
+ the value
+INFO_COMPARE_CANNOT_READ_ASSERTION_VALUE_FROM_FILE=Unable to read the \
+ assertion value from the specified file:  %s
+ERR_LDAPCOMPARE_NO_DNS=No entry DNs provided for the compare \
+ operation
+INFO_LDAPCOMPARE_TOOL_DESCRIPTION=This utility can be used to perform \
+ LDAP compare operations in the Directory Server
+INFO_LDAPMODIFY_TOOL_DESCRIPTION=This utility can be used to perform LDAP \
+ modify, add, delete, and modify DN operations in the Directory Server. \
+ When not using a file to specify modifications, end your input with EOF \
+ (Ctrl+D on UNIX, Ctrl+Z on Windows)
+INFO_LDAPPWMOD_TOOL_DESCRIPTION=This utility can be used to perform LDAP \
+ password modify operations in the Directory Server
+INFO_LDAPSEARCH_TOOL_DESCRIPTION=This utility can be used to perform LDAP \
+ search operations in the Directory Server
+ERR_LDAPCOMPARE_NO_ATTR=No attribute was specified to use as the \
+ target for the comparison
+ERR_LDAPCOMPARE_INVALID_ATTR_STRING=Invalid attribute string '%s'. \
+ The attribute string must be in one of the following forms: \
+ 'attribute:value', 'attribute::base64value', or 'attribute:<valueFilePath'
+ERR_TOOL_INVALID_CONTROL_STRING=Invalid control specification '%s'
+INFO_LDAPSEARCH_PSEARCH_CHANGE_TYPE=# Persistent search change type:  %s
+INFO_LDAPSEARCH_PSEARCH_PREVIOUS_DN=# Persistent search previous entry \
+ DN:  %s
+INFO_LDAPSEARCH_ACCTUSABLE_HEADER=# Account Usability Response Control
+INFO_LDAPSEARCH_ACCTUSABLE_IS_USABLE=#   The account is usable
+INFO_LDAPSEARCH_ACCTUSABLE_TIME_UNTIL_EXPIRATION=#   Time until password \
+ expiration:  %s
+INFO_LDAPSEARCH_ACCTUSABLE_NOT_USABLE=#   The account is not usable
+INFO_LDAPSEARCH_ACCTUSABLE_ACCT_INACTIVE=#   The account has been \
+ deactivated
+INFO_LDAPSEARCH_ACCTUSABLE_PW_RESET=#   The password has been reset
+INFO_LDAPSEARCH_ACCTUSABLE_PW_EXPIRED=#   The password has expired
+INFO_LDAPSEARCH_ACCTUSABLE_REMAINING_GRACE=#   Number of grace logins \
+ remaining:  %d
+INFO_LDAPSEARCH_ACCTUSABLE_LOCKED=#   The account is locked
+INFO_LDAPSEARCH_ACCTUSABLE_TIME_UNTIL_UNLOCK=#   Time until the account \
+ is unlocked:  %s
+INFO_DESCRIPTION_COUNT_ENTRIES=Count the number of entries returned by \
+ the server
+INFO_LDAPSEARCH_MATCHING_ENTRY_COUNT=# Total number of matching entries: \
+ %d
+INFO_DESCRIPTION_SIMPLE_PAGE_SIZE=Use the simple paged results control \
+ with the given page size
+ERR_PAGED_RESULTS_REQUIRES_SINGLE_FILTER=The simple paged results \
+ control may only be used with a single search filter
+ERR_TOOL_RESULT_CODE=Result Code:  %d (%s)
+ERR_TOOL_ERROR_MESSAGE=Additional Information:  %s
+ERR_TOOL_MATCHED_DN=Matched DN:  %s
+INFO_LDAPMODIFY_DESCRIPTION_FILENAME=LDIF file containing \
+ the changes to apply
+INFO_DESCRIPTION_SORT_ORDER=Sort the results using the provided sort \
+ order
+ERR_LDAP_SORTCONTROL_INVALID_ORDER=The provided sort order was \
+ invalid:  %s
+INFO_DESCRIPTION_VLV=Use the virtual list view control to retrieve the \
+ specified results page
+ERR_LDAPSEARCH_VLV_REQUIRES_SORT=If the --%s argument is provided, \
+ then the --%s argument must also be given
+ERR_LDAPSEARCH_VLV_INVALID_DESCRIPTOR=The provided virtual list view \
+ descriptor was invalid.  It must be a value in the form \
+ 'beforeCount:afterCount:offset:contentCount' (where offset specifies the \
+ index of the target entry and contentCount specifies the estimated total \
+ number of results or zero if it is not known), or \
+ 'beforeCount:afterCount:assertionValue' (where the entry should be the first \
+ entry whose primary sort value is greater than or equal to the provided \
+ assertionValue).  In either case, beforeCount is the number of entries to \
+ return before the target value and afterCount is the number of entries to \
+ return after the target value
+WARN_LDAPSEARCH_SORT_ERROR=# Server-side sort failed:  %s
+INFO_LDAPSEARCH_VLV_TARGET_OFFSET=# VLV Target Offset:  %d
+INFO_LDAPSEARCH_VLV_CONTENT_COUNT=# VLV Content Count:  %d
+WARN_LDAPSEARCH_VLV_ERROR=# Virtual list view processing failed: \
+ %s
+INFO_DESCRIPTION_EFFECTIVERIGHTS_USER=Use geteffectiverights control with \
+ the provided authzid
+INFO_DESCRIPTION_EFFECTIVERIGHTS_ATTR=Specifies geteffectiverights \
+ control specific attribute list
+ERR_EFFECTIVERIGHTS_INVALID_AUTHZID=The authorization ID "%s" \
+ contained in the geteffectiverights control is invalid because it does not \
+ start with "dn:" to indicate a user DN
+INFO_DESCRIPTION_SCRIPT_FRIENDLY=Use script-friendly mode
+INFO_FILE_PLACEHOLDER={file}
+INFO_BASEDN_PLACEHOLDER={baseDN}
+INFO_ASSERTION_FILTER_PLACEHOLDER={filter}
+INFO_FILTER_PLACEHOLDER={filter}
+INFO_PROXYAUTHID_PLACEHOLDER={authzID}
+INFO_ATTRIBUTE_PLACEHOLDER={attribute}
+INFO_NUM_ENTRIES_PLACEHOLDER={numEntries}
+INFO_LDAP_CONTROL_PLACEHOLDER={controloid[:criticality[:value|::b64value|:<filePath]]}
+INFO_ATTRIBUTE_LIST_PLACEHOLDER={attrList}
+INFO_NEW_PASSWORD_PLACEHOLDER={newPassword}
+INFO_CURRENT_PASSWORD_PLACEHOLDER={currentPassword}
+INFO_SORT_ORDER_PLACEHOLDER={sortOrder}
+INFO_VLV_PLACEHOLDER={before:after:index:count | before:after:value}
+INFO_DEREFERENCE_POLICE_PLACEHOLDER={dereferencePolicy}
+INFO_SIZE_LIMIT_PLACEHOLDER={sizeLimit}
+INFO_TIME_LIMIT_PLACEHOLDER={timeLimit}
+INFO_TARGETDN_PLACEHOLDER={targetDN}
+INFO_PSEARCH_PLACEHOLDER=ps[:changetype[:changesonly[:entrychgcontrols]]]
+ERR_LDAPCOMPARE_ERROR_READING_FILE=An error occurred reading file \
+ '%s'.  Check that the file exists and that you have read access rights to \
+ it.  Details: %s
+ERR_LDAPCOMPARE_FILENAME_AND_DNS=Both entry DNs and a file name \
+ were provided for the compare operation.  These arguments are not compatible
+ERR_DECODE_CONTROL_FAILURE=# %s
+INFO_SEARCHRATE_TOOL_DESCRIPTION=This utility can be used to measure \
+  search throughput and response time of a directory service using \
+  user-defined searches.\n\n\
+  Example:\n\n\ \ searchrate -p 1389 -D "cn=directory manager" -w password \\\n\
+  \ \ \ \ -F -c 4 -t 4 -b "dc=example,dc=com" -g "rand(0,2000)" "(uid=user.%%d)"\n\n\
+  Before trying the example, import 2000 randomly generated users
+INFO_ADDRATE_TOOL_DESCRIPTION=This utility can be used to measure \
+  add and optionally delete throughput and response time of a directory server using \
+  user-defined entries. \
+  The {template-file-path} argument identifies a template file \
+  that has the same form as a template file for the makeldif command.\n\
+  \nExamples:\n \  This example adds entries and randomly deletes them while \
+  the number of entries added is greater than 10,000: \n \
+  addrate -p 1389 -f -c 10 -C random -s 10000 addrate.template \n \
+  This example adds entries and starts to delete them in the same \
+  order if their age is greater than a certain time: \n \
+  addrate -p 1389 -f -c 10 -C fifo -a 2 addrate.template\n\n\
+  For details about the template file, see makeldif.template
+INFO_SEARCHRATE_TOOL_DESCRIPTION_BASEDN=Base DN format string.
+INFO_MODRATE_TOOL_DESCRIPTION=This utility can be used to measure \
+  modify throughput and response time of a directory service using \
+  user-defined modifications.\n\n\
+  Example:\n\n\ \ modrate -p 1389 -D "cn=directory manager" -w password \\\n\
+  \ \ \ \ -F -c 4 -t 4 -b "uid=user.%%d,ou=people,dc=example,dc=com" \\\n\
+  \ \ \ \ -g "rand(0,2000)" -g "randstr(16)" 'description:%%2$s'\n\n\
+  Before trying the example, import 2000 randomly generated users
+INFO_MODRATE_TOOL_DESCRIPTION_TARGETDN=Target entry DN format string
+INFO_AUTHRATE_TOOL_DESCRIPTION=This utility can be used to measure \
+  bind throughput and response time of a directory service using \
+  user-defined bind or search-then-bind operations.\n\nFormat strings may be \
+  used in the bind DN option as well as the authid and authzid SASL bind \
+  options. A search operation may be used to retrieve the bind DN by \
+  specifying the base DN and a filter. The retrieved entry DN will be appended \
+  as the last argument in the argument list when evaluating format strings.\n\n\
+  Example (bind only):\n\n\ \ authrate -p 1389 -D "uid=user.%%d,ou=people,dc=example,dc=com" \\\n\
+  \ \ \ \  -w password -f -c 10 -g "rand(0,2000)"\n\n\
+  Example (search then bind):\n\n\ \ authrate -p 1389 -D '%%2$s' -w password -f -c 10 \\\n\
+  \ \ \ \ -b "ou=people,dc=example,dc=com" -s one -g "rand(0,2000)" "(uid=user.%%d)"\n\n\
+  Before trying the example, import 2000 randomly generated users
+INFO_OUTPUT_LDIF_FILE_PLACEHOLDER={file}
+INFO_LDIFMODIFY_DESCRIPTION_OUTPUT_FILENAME=Write updated entries to %s \
+ instead of stdout
+INFO_LDIFDIFF_DESCRIPTION_OUTPUT_FILENAME=Write differences to %s \
+ instead of stdout
+INFO_LDIFSEARCH_DESCRIPTION_OUTPUT_FILENAME=Write search results to %s \
+ instead of stdout
+ERR_LDIFMODIFY_MULTIPLE_USES_OF_STDIN=Unable to use stdin for both the source \
+ LDIF and changes LDIF
+ERR_LDIFDIFF_MULTIPLE_USES_OF_STDIN=Unable to use stdin for both the source \
+ LDIF and target LDIF
+ERR_LDIFMODIFY_PATCH_FAILED=The changes could not be applied for the following \
+ reason: %s
+ERR_LDIFDIFF_DIFF_FAILED=The differences could not be computed for the following \
+ reason: %s
+ERR_LDIFSEARCH_FAILED=The search could not be performed for the following \
+ reason: %s
+INFO_LDIFMODIFY_TOOL_DESCRIPTION=This utility can be used to apply a set of \
+ modify, add, and delete operations to entries contained in an LDIF file
+INFO_LDIFDIFF_TOOL_DESCRIPTION=This utility can be used to compare two LDIF \
+ files and report the differences in LDIF format
+INFO_LDIFSEARCH_TOOL_DESCRIPTION=This utility can be used to perform search \
+ operations against entries contained in an LDIF file
+ERR_LDIF_GEN_TOOL_EXCEPTION_DURING_PARSE=An error occurred while \
+ parsing template file:  %s
+ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY=The specified resource \
+ directory %s does not exist
+ERR_TOOL_NOT_ENOUGH_ITERATIONS=%s argument must be greater than or equal to %s \
+ (%s per %s)
+ERR_TOOL_ARG_MUST_BE_USED_WHEN_ARG_CONDITION=%s must be used if %s is %s
+INFO_TOOL_WARMING_UP=Warming up for %d seconds...
+ERR_AUTHRATE_NO_BIND_DN_PROVIDED=Authentication information must be provided \
+ to use this tool
+ #
+ # MakeLDIF tool
+ #
+INFO_MAKELDIF_TOOL_DESCRIPTION=This utility can be used to generate LDIF \
+ data based on a definition in a template file
+INFO_CONSTANT_PLACEHOLDER={name=value}
+INFO_SEED_PLACEHOLDER={seed}
+INFO_PATH_PLACEHOLDER={path}
+INFO_MAKELDIF_DESCRIPTION_CONSTANT=A constant that overrides the value \
+ set in the template file
+INFO_MAKELDIF_DESCRIPTION_LDIF=The path to the LDIF file to be written
+INFO_MAKELDIF_DESCRIPTION_SEED=The seed to use to initialize the random \
+ number generator
+INFO_MAKELDIF_DESCRIPTION_HELP=Show this usage information
+INFO_MAKELDIF_DESCRIPTION_RESOURCE_PATH=Path to look for \
+ MakeLDIF resources (e.g., data files)
+INFO_MAKELDIF_PROCESSED_N_ENTRIES=Processed %d entries
+INFO_MAKELDIF_PROCESSING_COMPLETE=LDIF processing complete. %d entries \
+ written
+ERR_MAKELDIF_UNABLE_TO_CREATE_LDIF=An error occurred while \
+ attempting to open LDIF file %s for writing:  %s
+ERR_MAKELDIF_ERROR_WRITING_LDIF=An error occurred while writing data \
+ to LDIF file %s:  %s
+ERR_MAKELDIF_EXCEPTION_DURING_PROCESSING=An error occurred while \
+ processing :  %s
+ERR_CONSTANT_ARG_CANNOT_DECODE=Unable to parse a constant argument \
+ expecting name=value but got %s
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Maximum length of an output line \
+ (0 for no wrapping)
+INFO_MAKELDIF_WRAP_COLUMN_PLACEHOLDER={wrapColumn}
+#
+# AddRate Tool
+#
+INFO_ADDRATE_DESCRIPTION_RESOURCE_PATH=Path to look for template resources (e.g. data files)
+INFO_ADDRATE_DESCRIPTION_SEED=The seed to use for initializing the random number generator
+INFO_ADDRATE_DESCRIPTION_CONSTANT=A constant that overrides the value set in the template file
+INFO_ADDRATE_DESCRIPTION_DELETEMODE=The algorithm used for selecting entries to be deleted which \
+ must be one of "fifo", "random", or "off".
+INFO_ADDRATE_DESCRIPTION_DELETESIZETHRESHOLD=Specifies the number of entries \
+ to be added before deletion begins
+INFO_ADDRATE_DESCRIPTION_DELETEAGETHRESHOLD=Specifies the age at which added entries \
+ will become candidates for deletion
+INFO_DELETEMODE_PLACEHOLDER={fifo | random | off}
+INFO_DELETESIZETHRESHOLD_PLACEHOLDER={count}
+INFO_DELETEAGETHRESHOLD_PLACEHOLDER={seconds}
+ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON=A deletion threshold should not be specified when deletion is disabled
+ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE=Size and age based deletion thresholds were both \
+ specified, but only only one at a time is supported
+ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE=A age based deletion threshold should not be used \
+ with a random deletion mode
+ERR_ADDRATE_SIZE_THRESHOLD_LOWER_THAN_ITERATIONS=The size threshold must be lower than \
+ the maximum number of add operations
+INFO_ADDRATE_DESCRIPTION_NOPURGE=Disable the purge phase when the tool stops.
+
+# Strings for generated reference documentation.
+REF_SHORT_DESC_ADDRATE=measure add and delete throughput and response time
+REF_SHORT_DESC_AUTHRATE=measure bind throughput and response time
+REF_SHORT_DESC_LDAPCOMPARE=perform LDAP compare operations
+REF_SHORT_DESC_LDAPMODIFY=perform LDAP modify, add, delete, mod DN operations
+REF_SHORT_DESC_LDAPPASSWORDMODIFY=perform LDAP password modifications
+REF_SHORT_DESC_LDAPSEARCH=perform LDAP search operations
+REF_SHORT_DESC_LDIFDIFF=compare small LDIF files
+REF_SHORT_DESC_LDIFMODIFY=apply LDIF changes to LDIF
+REF_SHORT_DESC_LDIFSEARCH=search LDIF with LDAP filters
+REF_SHORT_DESC_MAKELDIF=generate test LDIF
+REF_SHORT_DESC_MODRATE=measure modification throughput and response time
+REF_SHORT_DESC_SEARCHRATE=measure search throughput and response time
+
+# Supplements to descriptions for generated reference documentation.
+SUPPLEMENT_DESCRIPTION_RATE_TOOLS=<xinclude:include href="description-rate-tools.xml" />
+SUPPLEMENT_DESCRIPTION_MAKELDIF=<xinclude:include href="description-makeldif.xml" />
+SUPPLEMENT_DESCRIPTION_RESOURCE_PATH=<xinclude:include href="description-resource-path.xml" />
+SUPPLEMENT_DESCRIPTION_PSEARCH_INFO=<xinclude:include href="description-psearch-info.xml" />
+SUPPLEMENT_DESCRIPTION_CONTROLS=<xinclude:include href="variablelist-ldap-controls.xml" />
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ca_ES.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ca_ES.properties
new file mode 100644
index 0000000..906f8c3
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ca_ES.properties
@@ -0,0 +1,20 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Columna in la qual s'envoltaran les l\u00ednies incorrectes (0 per no envoltar)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_de.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_de.properties
new file mode 100644
index 0000000..e95c476
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_de.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Spalte, in der lange Zeile umgebrochen werden sollen (0 f\u00fcr keinen Zeilenumbruch)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_es.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_es.properties
new file mode 100644
index 0000000..0a3db36
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_es.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Columna en la que ajustar l\u00edneas largas (0 para ning\u00fan ajuste)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_fr.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_fr.properties
new file mode 100644
index 0000000..4a8219b
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_fr.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Taille maximum d'une ligne dans le fichier de sortie (0 = pas de limite)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ja.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ja.properties
new file mode 100644
index 0000000..c140f67
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ja.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=\u9577\u3044\u884c\u3092\u6298\u308a\u8fd4\u3059\u6841 (0 \u306f\u6298\u308a\u8fd4\u3057\u306a\u3057)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ko.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ko.properties
new file mode 100644
index 0000000..782e4f2
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_ko.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=\uae34 \ud589\uc744 \ub798\ud551\ud560 \uc5f4(0 = \ub798\ud551 \uc548 \ud568)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_pl.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_pl.properties
new file mode 100644
index 0000000..219edc7
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_pl.properties
@@ -0,0 +1,20 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+#  Copyright 2016 ForgeRock AS.
+#
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=Kolumna na kt\u00f3rej zawijane b\u0119d\u0105 d\u0142ugie linie (0 aby wy\u0142\u0105czy\u0107 zawijanie)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_CN.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_CN.properties
new file mode 100644
index 0000000..cde850c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_CN.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=\u5bf9\u8f83\u957f\u7684\u884c\u8fdb\u884c\u6362\u884c\u7684\u5217\uff080 \u8868\u793a\u4e0d\u6362\u884c\uff09
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_TW.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_TW.properties
new file mode 100644
index 0000000..2a61a7f
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools_zh_TW.properties
@@ -0,0 +1,19 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2009 Sun Microsystems, Inc.
+# Portions Copyright 2016 ForgeRock AS.
+
+#
+# MakeLDIF tool
+#
+INFO_MAKELDIF_DESCRIPTION_WRAP_COLUMN=\u8981\u5c0d\u9577\u53e5\u63db\u884c\u7684\u6b04 (0 \u8868\u793a\u4e0d\u63db\u884c)
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/site/xdoc/index.xml.vm b/opendj-sdk/opendj-ldap-toolkit/src/site/xdoc/index.xml.vm
new file mode 100644
index 0000000..6bc0def
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/site/xdoc/index.xml.vm
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2015 ForgeRock AS.
+-->
+<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+  <properties>
+    <title>About OpenDJ LDAP Toolkit</title>
+    <author email="opendj-dev@forgerock.org">ForgeRock AS</author>
+  </properties>
+  <body>
+    <section name="About OpenDJ LDAP Toolkit">
+      <p>The OpenDJ LDAP Toolkit includes the following command-line tools.</p>
+      <dl>
+       <dt>authrate</dt><dd>measure bind throughput and response time</dd>
+       <dt>ldapcompare</dt><dd>perform LDAP compare operations</dd>
+       <dt>ldapmodify</dt><dd>perform LDAP modify, add, delete, mod DN operations</dd>
+       <dt>ldappasswordmodify</dt><dd>perform LDAP password modifications</dd>
+       <dt>ldapsearch</dt><dd>perform LDAP search operations</dd>
+       <dt>ldifmodify</dt><dd>perform LDAP modify, add, delete, mod DN operations against entries contained in an LDIF file</dd>
+       <dt>ldifsearch</dt><dd>perform search operations against entries contained in an LDIF file</dd>
+       <dt>ldifdiff</dt><dd>compare two LDIF files and report the differences in LDIF format</dd>
+       <dt>makeldif</dt><dd>generate LDIF content from and LDIF template</dd>
+       <dt>modrate</dt><dd>measure modification throughput and response time</dd>
+       <dt>searchrate</dt><dd>measure search throughput and response time</dd>
+      </dl>
+    </section>
+    <section name="Get ${project.name}">
+      <p>
+        You can get ${project.name} using any of the following methods:
+      </p>
+      <subsection name="Download">
+        <p>
+          Pre-built binaries can be downloaded directly from the ForgeRock Maven
+          repository:
+        </p>
+        <ul>
+          <li><a href="${mavenRepoReleases}/org/forgerock/opendj/${project.artifactId}">Stable releases</a></li>
+          <li><a href="${mavenRepoSnapshots}/org/forgerock/opendj/${project.artifactId}/${project.version}">Latest development snapshot</a></li>
+        </ul>
+      </subsection>
+      <subsection name="Build">
+        <p>
+          For the DIY enthusiasts you can build it yourself by checking out the
+          latest code using <a href="source-repository.html">Subversion</a> and
+          building it with Maven 3.
+        </p>
+      </subsection>
+    </section>
+  </body>
+</document>
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java
new file mode 100644
index 0000000..9967807
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java
@@ -0,0 +1,124 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.util.Utils.*;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+
+import java.io.PrintStream;
+
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class AddRateITCase extends ToolsITCase {
+
+    private static final String TEMPLATE_NAME = "addrate.template";
+    private static final String ADD_PERCENT_TEXT = "Add%";
+    private static final int THROUGHPUT_COLUMN = 1;
+    private static final int ERR_PER_SEC_COLUMN_NUMBER = 8;
+
+    private ByteStringBuilder out;
+    private ByteStringBuilder err;
+    private PrintStream outStream;
+    private PrintStream errStream;
+
+    @BeforeMethod
+    private void refreshStreams() {
+        out = new ByteStringBuilder();
+        err = new ByteStringBuilder();
+        outStream = new PrintStream(out.asOutputStream());
+        errStream = new PrintStream(err.asOutputStream());
+    }
+
+    @AfterMethod
+    private void closeStreams() {
+        closeSilently(outStream, errStream);
+    }
+
+    private String[] commonsArgs() {
+        return new String[] {
+            "-h", TestCaseUtils.getServerSocketAddress().getHostName(),
+            "-p", Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()),
+            "-c", "1", "-t", "1", "-i", "1", "-m", "100", "-S"};
+    }
+
+    public String[] args(String[] startLine, String... args) {
+        String[] res = new String[startLine.length + args.length];
+
+        System.arraycopy(startLine, 0, res, 0, startLine.length);
+        System.arraycopy(args, 0, res, startLine.length, args.length);
+
+        return res;
+    }
+
+    @DataProvider
+    public Object[][] invalidAddRateArgs() throws Exception {
+        return new Object[][] {
+            // Should report inconsistent use of options
+            { args("-C", "off", "-a", "3", TEMPLATE_NAME), ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get() },
+            { args("-C", "off", "-s", "30000", TEMPLATE_NAME), ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get() },
+            { args("-C", "fifo", "-a", "3", "-s", "20000", TEMPLATE_NAME), ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE.get() },
+            { args("-C", "random", "-a", "3", TEMPLATE_NAME), ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE.get() },
+            { args("-s", "999", TEMPLATE_NAME), ERR_ADDRATE_SIZE_THRESHOLD_LOWER_THAN_ITERATIONS.get() },
+            { args("-42"), INFO_GLOBAL_HELP_REFERENCE.get("java " + AddRate.class.getCanonicalName()) }
+        };
+    }
+
+    @Test(dataProvider = "invalidAddRateArgs")
+    public void addRateExceptions(String[] arguments, Object expectedErr) throws Exception {
+        AddRate addRate = new AddRate(outStream, errStream);
+        int retCode = addRate.run(args(commonsArgs(), arguments));
+        checkOuputStreams(out, err, "", expectedErr);
+        assertThat(retCode).isNotEqualTo(0);
+    }
+
+    @Test
+    public void addRateDisplaysHelp() throws Exception {
+        AddRate addRate = new AddRate(outStream, errStream);
+        int retCode = addRate.run(args("-H"));
+        checkOuputStreams(out, err, INFO_ADDRATE_TOOL_DESCRIPTION.get(), "");
+        assertThat(retCode).isEqualTo(0);
+    }
+
+    @Test(timeOut = 10000)
+    public void addRateSimpleRun() throws Exception {
+        AddRate addRate = new AddRate(outStream, errStream);
+        int retCode = addRate.run(args(commonsArgs(), "-s", "10", TEMPLATE_NAME));
+        checkOuputStreams(out, err, ADD_PERCENT_TEXT, "");
+        assertThat(retCode).isEqualTo(0);
+        String outContent = out.toString();
+
+        if (outContent.contains(ADD_PERCENT_TEXT)) {
+            // Check that there was no error
+            String[] addRateResLines = outContent.split(System.getProperty("line.separator"));
+            // Skip header line
+            for (int i = 1; i < addRateResLines.length; i++) {
+                String[] lineData = addRateResLines[i].split(",");
+                assertThat(lineData[ERR_PER_SEC_COLUMN_NUMBER].trim()).isEqualTo("0.0");
+                assertThat(lineData[THROUGHPUT_COLUMN].trim()).isNotEqualTo("0.0");
+            }
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AuthRateITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AuthRateITCase.java
new file mode 100644
index 0000000..67a817e
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AuthRateITCase.java
@@ -0,0 +1,82 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_ERROR_PARSING_ARGS;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_TOOL_WARMING_UP;
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.io.PrintStream;
+
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class AuthRateITCase extends ToolsITCase {
+
+    private static final String THROUGHPUT_TEXT = "recent throughput";
+
+    @DataProvider
+    public Object[][] authRateArgs() throws Exception {
+        return new Object[][] {
+            { args(""), "", ERR_ERROR_PARSING_ARGS.get("") },
+            { args("-42"), "", INFO_GLOBAL_HELP_REFERENCE.get("java " + AuthRate.class.getCanonicalName()) },
+            // Warm-up test case
+            {
+                args("-h", TestCaseUtils.getServerSocketAddress().getHostName(),
+                     "-p", Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()),
+                     "-g", "rand(0,1000)", "-D", "uid=%d,ou=people,o=test", "-w", "password",
+                     "-i", "1", "-m", "10", "-f", "-B", "1"),
+                INFO_TOOL_WARMING_UP.get(1), "" },
+
+            // Correct test case
+            {
+                args("-h", TestCaseUtils.getServerSocketAddress().getHostName(),
+                     "-p", Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()),
+                     "-g", "rand(0,1000)", "-D", "uid=%d,ou=people,o=test", "-w", "password",
+                     "-i", "1", "-c", "1", "-m", "10", "-f", "-S", "-B", "0"),
+                THROUGHPUT_TEXT, "" },
+        };
+    }
+
+    @Test(dataProvider = "authRateArgs")
+    public void testITAuthRate(String[] arguments, Object expectedOut, Object expectedErr) throws Exception {
+        ByteStringBuilder out = new ByteStringBuilder();
+        ByteStringBuilder err = new ByteStringBuilder();
+
+        try (PrintStream outStream = new PrintStream(out.asOutputStream());
+            PrintStream errStream = new PrintStream(err.asOutputStream())) {
+            AuthRate authRate = new AuthRate(outStream, errStream);
+
+            authRate.run(arguments);
+            checkOuputStreams(out, err, expectedOut, expectedErr);
+            String outContent = out.toString();
+
+            if (expectedOut.toString().contains(THROUGHPUT_TEXT)) {
+                // Check that there was no error in search
+                String[] authRateResLines = outContent.split(System.getProperty("line.separator"));
+                //Skip header line
+                for (int i = 1; i < authRateResLines.length; i++) {
+                    String[] authRateLineData = authRateResLines[i].split(",");
+                    assertThat(authRateLineData[authRateLineData.length - 1].trim()).isEqualTo("0.0");
+                }
+            }
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java
new file mode 100644
index 0000000..b3050ce
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.io.File;
+
+import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+
+@SuppressWarnings("javadoc")
+public class ConnectionFactoryProviderTest extends ToolsTestCase {
+
+    @Mock
+    private ConsoleApplication app;
+
+    private ArgumentParser argParser;
+
+    private ConnectionFactoryProvider connectionFactoryProvider;
+
+    @BeforeMethod
+    public void init() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        argParser = new ArgumentParser("unused", new LocalizableMessageBuilder().toMessage(), false);
+        connectionFactoryProvider = new ConnectionFactoryProvider(argParser, app);
+    }
+
+    /** Issue OPENDJ-734. */
+    @Test
+    public void getConnectionFactoryShouldAllowNullTrustStorePassword() throws Exception {
+        // provide a trustStorePath but no password
+        String trustStorePath = new File(getClass().getClassLoader().getResource("dummy-truststore").toURI())
+                .getCanonicalPath();
+        argParser.parseArguments(new String[] { "--useStartTLS", "--trustStorePath", trustStorePath });
+
+        ConnectionFactory factory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+
+        assertThat(factory).isNotNull();
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPCompareITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPCompareITCase.java
new file mode 100644
index 0000000..0efba74
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPCompareITCase.java
@@ -0,0 +1,94 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_COMPARE_OPERATION_RESULT_FALSE;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_COMPARE_OPERATION_RESULT_TRUE;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_LDAPCOMPARE_TOOL_DESCRIPTION;
+
+import java.io.PrintStream;
+import java.util.Random;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class LDAPCompareITCase extends ToolsITCase {
+
+    private static final int NB_RAND_SIMPLE_COMPARE = 10;
+    private static final int NB_OTHER_SIMPLE_SEARCH = 2;
+
+    @DataProvider
+    public Object[][] ldapCompareArgs() throws Exception {
+        Object[][] data = new Object[NB_RAND_SIMPLE_COMPARE + NB_OTHER_SIMPLE_SEARCH][];
+        Random rand = new Random();
+        long[] randUIDs = new long[NB_RAND_SIMPLE_COMPARE];
+
+        // Check if the help message is correctly prompted
+        data[0] = new Object[] { args("--help"), INFO_LDAPCOMPARE_TOOL_DESCRIPTION.get(), "" };
+
+        // Check if the help reference message is prompted if arguments failed to be parsed
+        data[1] = new Object[] {
+            args("-42"), "", INFO_GLOBAL_HELP_REFERENCE.get("java " + LDAPCompare.class.getCanonicalName()) };
+
+        // Perform some basic comparison on random user from the test server
+        for (int i = 0; i < NB_RAND_SIMPLE_COMPARE; i++) {
+            randUIDs[i] = Math.round(rand.nextInt(1000));
+        }
+
+        for (int i = 0; i < NB_RAND_SIMPLE_COMPARE; i++) {
+            long firstUID = randUIDs[i];
+            // For first test, ensure that both uids are equals
+            long secondUID = i == 0 ? firstUID : randUIDs[rand.nextInt(randUIDs.length)];
+
+            data[i + NB_OTHER_SIMPLE_SEARCH] = produceLDAPCompareBasicTest(firstUID, secondUID);
+        }
+
+        return data;
+    }
+
+    private Object[] produceLDAPCompareBasicTest(long firstUID, long secondUID) {
+        String uid = String.format("uid:user.%d", firstUID);
+        String dn = String.format("uid=user.%d,ou=people,o=test", secondUID);
+        LocalizableMessage messageToCheck = INFO_COMPARE_OPERATION_RESULT_FALSE.get(dn);
+
+        if (firstUID == secondUID) {
+            messageToCheck = INFO_COMPARE_OPERATION_RESULT_TRUE.get(dn);
+        }
+
+        return new Object[] {
+            args("-h", TestCaseUtils.getServerSocketAddress().getHostName(), "-p",
+                Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()), uid, dn), messageToCheck, "" };
+    }
+
+    @Test(dataProvider = "ldapCompareArgs")
+    public void testITLDAPSearch(String[] arguments, Object expectedOut, Object expectedErr) throws Exception {
+        ByteStringBuilder out = new ByteStringBuilder();
+        ByteStringBuilder err = new ByteStringBuilder();
+
+        try (PrintStream outStream = new PrintStream(out.asOutputStream());
+            PrintStream errStream = new PrintStream(err.asOutputStream())) {
+            LDAPCompare ldapCompare = new LDAPCompare(outStream, errStream);
+
+            ldapCompare.run(arguments);
+            checkOuputStreams(out, err, expectedOut, expectedErr);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPSearchITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPSearchITCase.java
new file mode 100644
index 0000000..545c6f0
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/LDAPSearchITCase.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.CliMessages.*;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_ERROR_PARSING_ARGS;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_TOOL_RESULT_CODE;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_LDAPSEARCH_MATCHING_ENTRY_COUNT;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_LDAPSEARCH_TOOL_DESCRIPTION;
+
+import java.io.PrintStream;
+import java.util.Random;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Simple integration tests to check the ldapsearch command. */
+@SuppressWarnings("javadoc")
+public class LDAPSearchITCase extends ToolsITCase {
+    private static final int NB_RAND_SIMPLE_SEARCH = 10;
+    private static final int NB_OTHER_SIMPLE_SEARCH = 3;
+
+    @DataProvider
+    public Object[][] ldapSearchArgs() throws Exception {
+        Object[][] data = new Object[NB_RAND_SIMPLE_SEARCH + NB_OTHER_SIMPLE_SEARCH][];
+
+        // Check if the help message is correctly prompted
+        data[0] = new Object[] { args("--help"), INFO_LDAPSEARCH_TOOL_DESCRIPTION.get(), "" };
+
+        // Check that there is a error message if no arguments were given to the
+        // ldapsearch command
+        data[1] = new Object[] { args(""), "", ERR_ERROR_PARSING_ARGS.get("") };
+
+        // Check if the help reference message is prompted if arguments failed to be parsed
+        data[2] = new Object[] {
+            args("-42"), "", INFO_GLOBAL_HELP_REFERENCE.get("java " + LDAPSearch.class.getCanonicalName()) };
+
+        // Perform some basic ldapsearch for random user in the test server
+        for (int i = 0; i < NB_RAND_SIMPLE_SEARCH; i++) {
+            long userID = new Random().nextInt(1000);
+            data[i + NB_OTHER_SIMPLE_SEARCH] = produceLDAPSearchBasicTestCase(userID);
+        }
+
+        return data;
+    }
+
+    private Object[] produceLDAPSearchBasicTestCase(long userID) {
+        String dn = String.format("uid=user.%d,ou=people,o=test", userID);
+        LocalizableMessage matchEntryCnt = INFO_LDAPSEARCH_MATCHING_ENTRY_COUNT.get(1);
+        LocalizableMessage resultSuccess =
+            ERR_TOOL_RESULT_CODE.get(ResultCode.SUCCESS.intValue(), ResultCode.SUCCESS.getName().toString());
+        return new Object[] {
+            args("--countEntries", "-h", TestCaseUtils.getServerSocketAddress().getHostName(), "-p",
+                Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()), "-b", dn, "(uid=user.%d)", "uid"),
+            matchEntryCnt, resultSuccess };
+    }
+
+    @Test(dataProvider = "ldapSearchArgs")
+    public void testITLDAPSearch(String[] arguments, Object expectedOut, Object expectedErr) throws Exception {
+        ByteStringBuilder out = new ByteStringBuilder();
+        ByteStringBuilder err = new ByteStringBuilder();
+
+        try (PrintStream outStream = new PrintStream(out.asOutputStream());
+            PrintStream errStream = new PrintStream(err.asOutputStream())) {
+            LDAPSearch ldapSearch = new LDAPSearch(outStream, errStream);
+            ldapSearch.run(arguments);
+            checkOuputStreams(out, err, expectedOut, expectedErr);
+        }
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFITCase.java
new file mode 100644
index 0000000..8018c2e
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFITCase.java
@@ -0,0 +1,168 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static org.fest.assertions.Assertions.*;
+import static org.forgerock.util.Utils.*;
+import static com.forgerock.opendj.ldap.CoreMessages.*;
+
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+import static com.forgerock.opendj.cli.CliMessages.INFO_GLOBAL_HELP_REFERENCE;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class MakeLDIFITCase extends ToolsITCase {
+
+    private static final String TEMP_OUTPUT_FILE = ".temp_test_file.ldif";
+    private static final String TEST_RESOURCE_PATH = "src/test/resources";
+    private static final String VALID_TEMPLATE_FILE_PATH =
+            Paths.get(TEST_RESOURCE_PATH, "valid_test_template.ldif").toString();
+    private static final boolean SUCCESS = true;
+    private static final boolean FAILURE = false;
+
+    private ByteStringBuilder out;
+    private ByteStringBuilder err;
+    private PrintStream outStream;
+    private PrintStream errStream;
+
+    @BeforeMethod
+    private void refreshStreams() {
+        out = new ByteStringBuilder();
+        err = new ByteStringBuilder();
+        outStream = new PrintStream(out.asOutputStream());
+        errStream = new PrintStream(err.asOutputStream());
+    }
+
+    @AfterMethod
+    private void closeStreams() {
+        closeSilently(outStream, errStream);
+    }
+
+    @DataProvider
+    Object[][] validArguments() throws Exception {
+        return new Object[][] {
+            { // check that help message is displayed
+              args("-H"),
+              expectedOutput(INFO_MAKELDIF_TOOL_DESCRIPTION.get()) },
+
+            { args("-c", "numusers=1", "example.template"),
+              // 2 base entries + users
+              expectedOutput(INFO_MAKELDIF_PROCESSING_COMPLETE.get(3)) },
+
+            { args("-c", "numusers=5", "example.template"),
+              // 2 base entries + users
+              expectedOutput(INFO_MAKELDIF_PROCESSING_COMPLETE.get(7)) },
+        };
+    }
+
+    @DataProvider
+    Object[][] invalidArguments() throws Exception {
+        return new Object[][] {
+            { // check that usage is written to output when arguments are invalid
+              args(),
+              expectedOutput(INFO_GLOBAL_HELP_REFERENCE.get("java " + MakeLDIF.class.getCanonicalName())) },
+
+            { // Check if the help reference message is prompted if arguments failed to be parsed
+              args("-42"),
+              expectedOutput(INFO_GLOBAL_HELP_REFERENCE.get("java " + MakeLDIF.class.getCanonicalName())) },
+
+            { args("-r", "unknown/path" , "example.template"),
+              expectedOutput(ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY.get("unknown/path")) },
+
+            { args("-o", "unknown/path" , "example.template"),
+              expectedOutput(ERR_MAKELDIF_UNABLE_TO_CREATE_LDIF.get("unknown/path", "")) },
+
+            { args("-s", "non-numeric" , "example.template"),
+              expectedOutput(ERR_ERROR_PARSING_ARGS.get("")) },
+        };
+    }
+
+    @Test(dataProvider = "validArguments")
+    public void testMakeLDIFValidUseCases(final String[] arguments, final LocalizableMessage expectedOut)
+            throws Exception {
+        run(arguments, SUCCESS, expectedOut);
+    }
+
+    @Test(dataProvider = "invalidArguments")
+    public void testMakeLDIFInvalidUseCases(final String[] arguments, final LocalizableMessage expectedErr)
+            throws Exception {
+        run(arguments, FAILURE, expectedErr);
+    }
+
+    /** See OPENDJ-2505 */
+    @Test
+    public void testMakeLDIFInvalidLineFolding() throws Exception {
+        final LocalizableMessage expectedOutput = ERR_LDIF_GEN_TOOL_EXCEPTION_DURING_PARSE.get(
+                ERR_TEMPLATE_FILE_INVALID_LEADING_SPACE.get(
+                        27, " \"lineFoldingTest\":\\[\"This line should not be accepted by the parser\"\\],"));
+        run(args("src/test/resources/invalid_test_template.ldif"), FAILURE, expectedOutput);
+    }
+
+    /** See OPENDJ-2505 */
+    @Test
+    public void testMakeLDIFSupportsLineFolding() throws Exception {
+        final Path tempOutputFile = Paths.get(TEST_RESOURCE_PATH, TEMP_OUTPUT_FILE);
+        run(args("-o", tempOutputFile.toString(), VALID_TEMPLATE_FILE_PATH),
+                SUCCESS, INFO_MAKELDIF_PROCESSING_COMPLETE.get(2));
+        assertFilesAreEquals(TEMP_OUTPUT_FILE, "expected_output.ldif");
+        Files.delete(tempOutputFile);
+    }
+
+    /** See OPENDJ-2505 and OPENDJ-2754 */
+    @Test
+    public void testMakeLDIFSupportsLineFoldingAndLineWrapping() throws Exception {
+        final Path tempOutputFile = Paths.get(TEST_RESOURCE_PATH, TEMP_OUTPUT_FILE);
+        run(args("-o", tempOutputFile.toString(), "-w", "80", VALID_TEMPLATE_FILE_PATH),
+                SUCCESS, INFO_MAKELDIF_PROCESSING_COMPLETE.get(2));
+        assertFilesAreEquals(TEMP_OUTPUT_FILE, "expected_output_80_column.ldif");
+        Files.delete(tempOutputFile);
+    }
+
+    private void assertFilesAreEquals(final String outputFile, final String expectedOutputFileName) throws IOException {
+        assertThat(Files.readAllBytes(Paths.get(TEST_RESOURCE_PATH, outputFile))).isEqualTo(
+                   Files.readAllBytes(Paths.get(TEST_RESOURCE_PATH, expectedOutputFileName)));
+    }
+
+    private void run(final String[] arguments, final boolean expectsSuccess, final LocalizableMessage expectedOutput)
+            throws Exception {
+        final MakeLDIF makeLDIF = new MakeLDIF(outStream, errStream);
+        int retCode = makeLDIF.run(arguments);
+        checkOuputStreams(out, err, expectedOutput, "");
+        if (expectsSuccess) {
+            assertThat(retCode).isEqualTo(0);
+        } else {
+            assertThat(retCode).isNotEqualTo(0);
+        }
+    }
+
+    /** A message the error output is expected to contain. */
+    private LocalizableMessage expectedOutput(LocalizableMessage val) {
+        return val;
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java
new file mode 100644
index 0000000..ca2c09c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java
@@ -0,0 +1,87 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2014-2015 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.Utils.MAX_LINE_WIDTH;
+import static com.forgerock.opendj.cli.Utils.wrapText;
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+
+/**
+ * Class used for the toolkit integration tests.
+ */
+@SuppressWarnings("javadoc")
+public abstract class ToolsITCase extends ForgeRockTestCase {
+
+    @BeforeClass
+    void setUp() throws Exception {
+        TestCaseUtils.startServer();
+    }
+
+    @AfterClass
+    void tearDown() throws Exception {
+        TestCaseUtils.stopServer();
+    }
+
+    /**
+     * Check both out and err outputs streams.
+     *
+     * @param out
+     *            output stream from the toolkit application
+     * @param err
+     *            error stream from the toolkit application
+     * @param expectedOutput
+     *            String or LocalizedMessage expected on output
+     * @param expectedError
+     *            String or LocalizedMessage expected on error output
+     * @throws Exception
+     */
+    protected void checkOuputStreams(ByteStringBuilder out, ByteStringBuilder err, Object expectedOutput,
+        Object expectedError) throws Exception {
+        // Check error output
+        checkOutputStream(out, expectedOutput);
+        checkOutputStream(err, expectedError);
+    }
+
+    protected void checkOutputStream(ByteStringBuilder out, Object expectedOutput) {
+        String lineSeparator = System.getProperty("line.separator");
+        String toCompare = expectedOutput.toString();
+
+        if (expectedOutput instanceof LocalizableMessage) {
+            toCompare = wrapText((LocalizableMessage) expectedOutput, MAX_LINE_WIDTH);
+        }
+
+        toCompare = toCompare.replace(lineSeparator, " ");
+
+        if (toCompare.isEmpty()) {
+            assertThat(out.toString().length()).isEqualTo(0);
+        } else {
+            assertThat(out.toString().replace(lineSeparator, " ")).contains(toCompare);
+        }
+
+    }
+
+    /** Arguments passed to the command. */
+    protected String[] args(String... arguments) {
+        return arguments;
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsTestCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsTestCase.java
new file mode 100644
index 0000000..2ea2c2e
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsTestCase.java
@@ -0,0 +1,30 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2010 Sun Microsystems, Inc.
+ * Portions copyright 2012 ForgeRock AS.
+ */
+
+package com.forgerock.opendj.ldap.tools;
+
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.Test;
+
+/**
+ * An abstract class that all tools unit tests should extend. A tool represents
+ * the classes found directly under the package com.forgerock.opendj.ldap.tools.
+ */
+
+@Test(groups = { "precommit", "tools", "sdk" })
+public abstract class ToolsTestCase extends ForgeRockTestCase {
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/resources/dummy-truststore b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/dummy-truststore
new file mode 100644
index 0000000..65c6eed
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/dummy-truststore
Binary files differ
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output.ldif b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output.ldif
new file mode 100644
index 0000000..e5095ca
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output.ldif
@@ -0,0 +1,11 @@
+dn: dc=example,dc=com
+dc: example
+
+dn: coretokenid=tokenId,dc=example,dc=com
+coretokenid: tokenId
+objectClass: top
+objectClass: frCoreToken
+coretokenstring08: /myrealm
+coretokenstring07: Bearer
+coretokenobject: {"redirectURI":["http://fake.com"],"acr":[],"clientID":["clientOIDC"],"lineFoldingTest":["This line should have been correctly folded"],"tokenName":["refresh_token"],"authModules":["LDAP"],"realm":["/myrealm"],"id":["fakeid"],"userName":["johndoe"],"tokenType":["Bearer"]}
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output_80_column.ldif b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output_80_column.ldif
new file mode 100644
index 0000000..45e611c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/expected_output_80_column.ldif
@@ -0,0 +1,14 @@
+dn: dc=example,dc=com
+dc: example
+
+dn: coretokenid=tokenId,dc=example,dc=com
+coretokenid: tokenId
+objectClass: top
+objectClass: frCoreToken
+coretokenstring08: /myrealm
+coretokenstring07: Bearer
+coretokenobject: {"redirectURI":["http://fake.com"],"acr":[],"clientID":["client
+ OIDC"],"lineFoldingTest":["This line should have been correctly folded"],"token
+ Name":["refresh_token"],"authModules":["LDAP"],"realm":["/myrealm"],"id":["fake
+ id"],"userName":["johndoe"],"tokenType":["Bearer"]}
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/resources/invalid_test_template.ldif b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/invalid_test_template.ldif
new file mode 100644
index 0000000..d629754
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/invalid_test_template.ldif
@@ -0,0 +1,29 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+
+branch: dc=example,dc=com
+subordinateTemplate: refreshToken:10
+
+template: refreshToken
+rdnAttr: coreTokenId
+coreTokenId: <random:hex:8>-<random:hex:4>-<random:hex:4>-<random:hex:4>-<random:hex:12>
+objectClass: top
+objectClass: frCoreToken
+coreTokenString08: /myrealm
+coreTokenString07: Bearer
+coreTokenObject: \{"redirectURI":\["http://fake.com"\],"acr":\[\],"clientID":\["clientOIDC"\],
+
+ "lineFoldingTest":\["This line should not be accepted by the parser"\],
+ "tokenName":\["refresh_token"\],"authModules":\["LDAP"\],"realm":\["{coreTokenString08}"\],
+ "id":\["fakeid"\],"userName":\["johndoe"\],"tokenType":\["Bearer"\]\}
\ No newline at end of file
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/resources/valid_test_template.ldif b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/valid_test_template.ldif
new file mode 100644
index 0000000..fa3f0c3
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/resources/valid_test_template.ldif
@@ -0,0 +1,28 @@
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions Copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+
+branch: dc=example,dc=com
+subordinateTemplate: refreshToken:1
+
+template: refreshToken
+rdnAttr: coreTokenId
+coreTokenId: tokenId
+objectClass: top
+objectClass: frCoreToken
+coreTokenString08: /myrealm
+coreTokenString07: Bearer
+coreTokenObject: \{"redirectURI":\["http://fake.com"\],"acr":\[\],"clientID":\["clientOIDC"\],
+ "lineFoldingTest":\["This line should have been correctly folded"\],
+ "tokenName":\["refresh_token"\],"authModules":\["LDAP"\],"realm":\["{coreTokenString08}"\],
+ "id":\["fakeid"\],"userName":\["johndoe"\],"tokenType":\["Bearer"\]\}
\ No newline at end of file
diff --git a/opendj-sdk/opendj-rest2ldap/README b/opendj-sdk/opendj-rest2ldap/README
new file mode 100644
index 0000000..f74610a
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/README
@@ -0,0 +1,11 @@
+OpenDJ REST to LDAP Gateway.
+
+This Maven project contains the OpenDJ REST to LDAP gateway support library. It
+is 100% Java based and requires Java 1.7.
+
+Complete documentation for this product may be found online
+at http://www.forgerock.com/opendj.html.
+
+This product is made available under the Common Development and Distribution
+License (CDDL).  The complete text for this license, and for alternate licenses
+of included components, may be found in the legal-notices directory.
diff --git a/opendj-sdk/opendj-rest2ldap/pom.xml b/opendj-sdk/opendj-rest2ldap/pom.xml
new file mode 100644
index 0000000..4bcce5e
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   The contents of this file are subject to the terms of the Common Development and
+   Distribution License (the License). You may not use this file except in compliance with the
+   License.
+
+   You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+   specific language governing permission and limitations under the License.
+
+   When distributing Covered Software, include this CDDL Header Notice in each file and include
+   the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+   Header, with the fields enclosed by brackets [] replaced by your own identifying
+   information: "Portions Copyright [year] [name of copyright owner]".
+
+   Copyright 2012-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>opendj-sdk-parent</artifactId>
+        <groupId>org.forgerock.opendj</groupId>
+        <version>4.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>opendj-rest2ldap</artifactId>
+    <name>OpenDJ Commons REST Adapter</name>
+    <description>This module includes APIs for accessing LDAP repositories using commons REST.</description>
+
+    <packaging>bundle</packaging>
+
+    <properties>
+        <opendj.osgi.import.additional>
+            org.forgerock.opendj.*;provide:=true,
+            org.forgerock.json.*;provide:=true
+        </opendj.osgi.import.additional>
+    </properties>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>org.forgerock.opendj</groupId>
+            <artifactId>opendj-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>json-resource</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock.commons</groupId>
+            <artifactId>json-resource-http</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.forgerock</groupId>
+            <artifactId>forgerock-build-tools</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>org.forgerock.opendj.rest2ldap</Export-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <reportSets>
+                    <reportSet>
+                        <reports>
+                            <report>dependencies</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
new file mode 100644
index 0000000..dcf269f
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -0,0 +1,390 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.NotSupportedException;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+
+/**
+ * An abstract LDAP attribute mapper which provides a simple mapping from a JSON
+ * value to a single LDAP attribute.
+ */
+abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends AttributeMapper {
+    List<Object> defaultJSONValues = emptyList();
+    final AttributeDescription ldapAttributeName;
+    private boolean isRequired;
+    private boolean isSingleValued;
+    private WritabilityPolicy writabilityPolicy = READ_WRITE;
+
+    AbstractLDAPAttributeMapper(final AttributeDescription ldapAttributeName) {
+        this.ldapAttributeName = ldapAttributeName;
+    }
+
+    /**
+     * Indicates that the LDAP attribute is mandatory and must be provided
+     * during create requests.
+     *
+     * @return This attribute mapper.
+     */
+    public final T isRequired() {
+        this.isRequired = true;
+        return getThis();
+    }
+
+    /**
+     * Indicates that multi-valued LDAP attribute should be represented as a
+     * single-valued JSON value, rather than an array of values.
+     *
+     * @return This attribute mapper.
+     */
+    public final T isSingleValued() {
+        this.isSingleValued = true;
+        return getThis();
+    }
+
+    /**
+     * Indicates whether or not the LDAP attribute supports updates. The default
+     * is {@link WritabilityPolicy#READ_WRITE}.
+     *
+     * @param policy
+     *            The writability policy.
+     * @return This attribute mapper.
+     */
+    public final T writability(final WritabilityPolicy policy) {
+        this.writabilityPolicy = policy;
+        return getThis();
+    }
+
+    boolean attributeIsSingleValued() {
+        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
+    }
+
+    @Override
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
+        return getNewLDAPAttributes(requestState, path, v).then(
+            new Function<Attribute, List<Attribute>, ResourceException>() {
+                @Override
+                public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException {
+                    if (!writabilityPolicy.canCreate(ldapAttributeName)) {
+                        if (!newLDAPAttribute.isEmpty() && !writabilityPolicy.discardWrites()) {
+                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
+                                    + "to create the read-only field '%s'", path));
+                        }
+                        return Collections.emptyList();
+                    } else if (newLDAPAttribute.isEmpty()) {
+                        if (isRequired) {
+                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
+                                    + "to remove the required field '%s'", path));
+                        }
+                        return Collections.emptyList();
+                    }
+
+                    return singletonList(newLDAPAttribute);
+                }
+            });
+    }
+
+    @Override
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final Set<String> ldapAttributes) {
+        ldapAttributes.add(ldapAttributeName.toString());
+    }
+
+    abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(
+                RequestState requestState, JsonPointer path, List<Object> newValues);
+
+    abstract T getThis();
+
+    @Override
+    Promise<List<Modification>, ResourceException> patch(
+                final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
+        try {
+            final JsonPointer field = operation.getField();
+            final JsonValue v = operation.getValue();
+
+            /*
+             * Reject any attempts to patch this field if it is read-only, even
+             * if it is configured to discard writes.
+             */
+            if (!writabilityPolicy.canWrite(ldapAttributeName)) {
+                throw new BadRequestException(i18n(
+                        "The request cannot be processed because it attempts to modify "
+                                + "the read-only field '%s'", path));
+            }
+
+            switch (field.size()) {
+            case 0:
+                /*
+                 * The patch operation targets the entire mapping. If this
+                 * mapping is multi-valued, then the patch value must be a list
+                 * of values to be added, removed, or replaced. If it is
+                 * single-valued then the patch value must not be a list.
+                 */
+                if (attributeIsSingleValued()) {
+                    if (v.isList()) {
+                        // Single-valued field violation.
+                        throw new BadRequestException(i18n(
+                                "The request cannot be processed because an array of values was "
+                                        + "provided for the single valued field '%s'", path));
+                    }
+                } else if (!v.isList() && !operation.isIncrement()
+                        && !(v.isNull() && (operation.isReplace() || operation.isRemove()))) {
+                    // Multi-valued field violation.
+                    throw new BadRequestException(i18n(
+                            "The request cannot be processed because an array of values was "
+                                    + "not provided for the multi-valued field '%s'", path));
+                }
+                break;
+            case 1:
+                /*
+                 * The patch operation targets a sub-field. If the sub-field
+                 * name is a number then it is an attempt to patch a single
+                 * value at a specific index. Rest2LDAP cannot support indexed
+                 * updates because LDAP attribute values are unordered. We will,
+                 * however, support the special index "-" indicating that a
+                 * value should be appended.
+                 */
+                final String fieldName = field.get(0);
+                if (fieldName.equals("-") && operation.isAdd()) {
+                    // Append a single value.
+                    if (attributeIsSingleValued()) {
+                        throw new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to append a "
+                                        + "value to the single valued field '%s'", path));
+                    } else if (v.isList()) {
+                        throw new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to "
+                                        + "perform an indexed append of an array of values to "
+                                        + "the multi-valued field '%s'", path.child(fieldName)));
+                    }
+                } else if (fieldName.matches("[0-9]+")) {
+                    // Array index - not allowed.
+                    throw new NotSupportedException(i18n(
+                            "The request cannot be processed because it included "
+                                    + "an indexed patch operation '%s' which is not supported "
+                                    + "by this resource provider", path.child(fieldName)));
+                } else {
+                    throw new BadRequestException(i18n(
+                            "The request cannot be processed because it included "
+                                    + "an unrecognized field '%s'", path.child(fieldName)));
+                }
+                break;
+            default:
+                /*
+                 * The patch operation targets the child of a sub-field. This is
+                 * not possible for a LDAP attribute mapper.
+                 */
+                throw new BadRequestException(i18n(
+                        "The request cannot be processed because it included "
+                                + "an unrecognized field '%s'", path.child(field.get(0))));
+            }
+
+            // Check that the values are compatible with the type of patch operation.
+            final List<Object> newValues = asList(v, Collections.emptyList());
+            final ModificationType modType;
+            if (operation.isAdd()) {
+                /*
+                 * Use a replace for single valued fields in case the underlying
+                 * LDAP attribute is multi-valued, or the attribute already
+                 * contains a value.
+                 */
+                modType =
+                        attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD;
+                if (newValues.isEmpty()) {
+                    throw new BadRequestException(i18n(
+                            "The request cannot be processed because it included "
+                                    + "an add patch operation but no value(s) for field '%s'", path
+                                    .child(field.get(0))));
+                }
+            } else if (operation.isRemove()) {
+                modType = ModificationType.DELETE;
+            } else if (operation.isReplace()) {
+                modType = ModificationType.REPLACE;
+            } else if (operation.isIncrement()) {
+                modType = ModificationType.INCREMENT;
+            } else {
+                throw new NotSupportedException(i18n(
+                        "The request cannot be processed because it included "
+                                + "an unsupported type of patch operation '%s'", operation
+                                .getOperation()));
+            }
+
+            // Create the modification.
+            if (newValues.isEmpty()) {
+                // Deleting the attribute.
+                if (isRequired) {
+                    return Promises.<List<Modification>, ResourceException> newExceptionPromise(
+                            new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to remove the required field '%s'",
+                                path)));
+                } else {
+                    return Promises.newResultPromise(
+                        singletonList(new Modification(modType, emptyAttribute(ldapAttributeName))));
+                }
+            } else {
+                return getNewLDAPAttributes(requestState, path, newValues)
+                        .then(new Function<Attribute, List<Modification>, ResourceException>() {
+                            @Override
+                            public List<Modification> apply(final Attribute value) {
+                                return singletonList(new Modification(modType, value));
+                            }
+                        });
+            }
+        } catch (final RuntimeException e) {
+            return Promises.newExceptionPromise(asResourceException(e));
+        } catch (final ResourceException e) {
+            return Promises.newExceptionPromise(e);
+        }
+    }
+
+    @Override
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
+        return getNewLDAPAttributes(requestState, path, v).then(
+            new Function<Attribute, List<Modification>, ResourceException>() {
+                @Override
+                public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException {
+                    // Get the existing LDAP attribute.
+                    final Attribute tmp = e.getAttribute(ldapAttributeName);
+                    final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
+                    /*
+                     * If the attribute is read-only then handle the following cases:
+                     * 1) new values are provided and they are the same as the existing values
+                     * 2) no new values are provided.
+                     */
+                    if (!writabilityPolicy.canWrite(ldapAttributeName)) {
+                        if (newLDAPAttribute.isEmpty()
+                                || newLDAPAttribute.equals(oldLDAPAttribute)
+                                || writabilityPolicy.discardWrites()) {
+                            // No change.
+                            return Collections.emptyList();
+                        }
+                        throw new BadRequestException(i18n(
+                            "The request cannot be processed because it attempts to modify the read-only field '%s'",
+                            path));
+                    }
+
+                    if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
+                        // No change.
+                        return Collections.emptyList();
+                    } else if (oldLDAPAttribute.isEmpty()) {
+                        // The attribute is being added.
+                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
+                    } else if (newLDAPAttribute.isEmpty()) {
+                        // The attribute is being deleted - this is not allowed if the attribute is required.
+                        if (isRequired) {
+                            throw new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to remove the required field '%s'",
+                                path));
+                        }
+                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
+                    } else {
+                        /*
+                         * We could do a replace, but try to save bandwidth and send diffs instead.
+                         * Perform deletes first in case we don't have an appropriate normalizer:
+                         * permissive add(x) followed by delete(x) is destructive, whereas
+                         * delete(x) followed by add(x) is idempotent when adding/removing the same value.
+                         */
+                        final List<Modification> modifications = new ArrayList<>(2);
+
+                        final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
+                        deletedValues.removeAll(newLDAPAttribute);
+                        if (!deletedValues.isEmpty()) {
+                            modifications.add(new Modification(ModificationType.DELETE, deletedValues));
+                        }
+
+                        final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
+                        addedValues.removeAll(oldLDAPAttribute);
+                        if (!addedValues.isEmpty()) {
+                            modifications.add(new Modification(ModificationType.ADD, addedValues));
+                        }
+                        return modifications;
+                    }
+                }
+            });
+    }
+
+    private List<Object> asList(final JsonValue v, final List<Object> defaultValues) {
+        if (isNullOrEmpty(v)) {
+            return defaultValues;
+        } else if (v.isList()) {
+            return v.asList();
+        } else {
+            return singletonList(v.getObject());
+        }
+    }
+
+    private void checkSchema(final JsonPointer path, final JsonValue v) throws BadRequestException {
+        if (attributeIsSingleValued()) {
+            if (v != null && v.isList()) {
+                // Single-valued field violation.
+                throw new BadRequestException(i18n(
+                        "The request cannot be processed because an array of values was "
+                                + "provided for the single valued field '%s'", path));
+            }
+        } else if (v != null && !v.isList()) {
+            // Multi-valued field violation.
+            throw new BadRequestException(i18n(
+                    "The request cannot be processed because an array of values was "
+                            + "not provided for the multi-valued field '%s'", path));
+        }
+    }
+
+    private Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
+        try {
+            // Ensure that the value is of the correct type.
+            checkSchema(path, v);
+            final List<Object> newValues = asList(v, defaultJSONValues);
+            if (newValues.isEmpty()) {
+                // Skip sub-class implementation if there are no values.
+                return Promises.newResultPromise(emptyAttribute(ldapAttributeName));
+            } else {
+                return getNewLDAPAttributes(requestState, path, newValues);
+            }
+        } catch (final Exception ex) {
+            return Promises.newExceptionPromise(asResourceException(ex));
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
new file mode 100644
index 0000000..d0fd363
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -0,0 +1,197 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.util.promise.Promise;
+
+/** An attribute mapper is responsible for converting JSON values to and from LDAP attributes. */
+public abstract class AttributeMapper {
+    /*
+     * This interface is an abstract class so that methods can be made package
+     * private until API is finalized.
+     */
+
+    AttributeMapper() {
+        // Nothing to do.
+    }
+
+    /**
+     * Maps a JSON value to one or more LDAP attributes, returning a promise
+     * once the transformation has completed. This method is invoked when a REST
+     * resource is created using a create request.
+     * <p>
+     * If the JSON value corresponding to this mapper is not present in the
+     * resource then this method will be invoked with a value of {@code null}.
+     * It is the responsibility of the mapper implementation to take appropriate
+     * action in this case, perhaps by substituting default LDAP values, or by
+     * returning a failed promise with an appropriate {@link ResourceException}.
+     *
+     * @param requestState
+     *            The request state.
+     * @param path
+     *            The pointer from the root of the JSON resource to this
+     *            attribute mapper. This may be used when constructing error
+     *            messages.
+     * @param v
+     *            The JSON value to be converted to LDAP attributes, which may
+     *            be {@code null} indicating that the JSON value was not present
+     *            in the resource.
+     * @return A {@link Promise} containing the result of the operation.
+     */
+    abstract Promise<List<Attribute>, ResourceException> create(
+            RequestState requestState, JsonPointer path, JsonValue v);
+
+    /**
+     * Adds the names of the LDAP attributes required by this attribute mapper
+     * to the provided set.
+     * <p>
+     * Implementations should only add the names of attributes found in the LDAP
+     * entry directly associated with the resource.
+     *
+     * @param requestState
+     *            The request state.
+     * @param path
+     *            The pointer from the root of the JSON resource to this
+     *            attribute mapper. This may be used when constructing error
+     *            messages.
+     * @param subPath
+     *            The targeted JSON field relative to this attribute mapper, or
+     *            root if all attributes associated with this mapper have been
+     *            targeted.
+     * @param ldapAttributes
+     *            The set into which the required LDAP attribute names should be
+     *            put.
+     */
+    abstract void getLDAPAttributes(
+            RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes);
+
+    /**
+     * Transforms the provided REST comparison filter parameters to an LDAP
+     * filter representation, returning a promise once the transformation has
+     * completed.
+     * <p>
+     * If an error occurred while constructing the LDAP filter, then a failed
+     * promise must be returned with an appropriate {@link ResourceException}
+     * indicating the problem which occurred.
+     *
+     * @param requestState
+     *            The request state.
+     * @param path
+     *            The pointer from the root of the JSON resource to this
+     *            attribute mapper. This may be used when constructing error
+     *            messages.
+     * @param subPath
+     *            The targeted JSON field relative to this attribute mapper, or
+     *            root if all attributes associated with this mapper have been
+     *            targeted.
+     * @param type
+     *            The type of REST comparison filter.
+     * @param operator
+     *            The name of the extended operator to use for the comparison,
+     *            or {@code null} if {@code type} is not
+     *            {@link FilterType#EXTENDED}.
+     * @param valueAssertion
+     *            The value assertion, or {@code null} if {@code type} is
+     *            {@link FilterType#PRESENT}.
+     * @return A {@link Promise} containing the result of the operation.
+     */
+    abstract Promise<Filter, ResourceException> getLDAPFilter(RequestState requestState, JsonPointer path,
+            JsonPointer subPath, FilterType type, String operator, Object valueAssertion);
+
+    /**
+     * Maps a JSON patch operation to one or more LDAP modifications, returning
+     * a promise once the transformation has completed. This method is invoked
+     * when a REST resource is modified using a patch request.
+     *
+     * @param requestState
+     *            The request state.
+     * @param path
+     *            The pointer from the root of the JSON resource to this
+     *            attribute mapper. This may be used when constructing error
+     *            messages.
+     * @param operation
+     *            The JSON patch operation to be converted to LDAP
+     *            modifications. The targeted JSON field will be relative to
+     *            this attribute mapper, or root if all attributes associated
+     *            with this mapper have been targeted.
+     * @return A {@link Promise} containing the result of the operation.
+     */
+    abstract Promise<List<Modification>, ResourceException> patch(
+            RequestState requestState, JsonPointer path, PatchOperation operation);
+
+    /**
+     * Maps one or more LDAP attributes to their JSON representation, returning
+     * a promise once the transformation has completed.
+     * <p>
+     * This method is invoked whenever an LDAP entry is converted to a REST
+     * resource, i.e. when responding to read, query, create, put, or patch
+     * requests.
+     * <p>
+     * If the LDAP attributes are not present in the entry, perhaps because they
+     * are optional, then implementations should return a successful promise
+     * with a result of {@code null}. If the LDAP attributes cannot be mapped
+     * for any other reason, perhaps because they are required but missing, or
+     * they contain unexpected content, then a failed promise must be returned
+     * with an appropriate exception indicating the problem which occurred.
+     *
+     * @param requestState
+     *            The request state.
+     * @param path
+     *            The pointer from the root of the JSON resource to this
+     *            attribute mapper. This may be used when constructing error
+     *            messages.
+     * @param e
+     *            The LDAP entry to be converted to JSON.
+     * @return A {@link Promise} containing the result of the operation.
+     */
+    abstract Promise<JsonValue, ResourceException> read(RequestState requestState, JsonPointer path, Entry e);
+
+    /**
+     * Maps a JSON value to one or more LDAP modifications, returning a promise
+     * once the transformation has completed. This method is invoked when a REST
+     * resource is modified using an update request.
+     * <p>
+     * If the JSON value corresponding to this mapper is not present in the
+     * resource then this method will be invoked with a value of {@code null}.
+     * It is the responsibility of the mapper implementation to take appropriate
+     * action in this case, perhaps by substituting default LDAP values, or by
+     * returning a failed promise with an appropriate {@link ResourceException}.
+     *
+     * @param requestState
+     *            The request state.
+     * @param v
+     *            The JSON value to be converted to LDAP attributes, which may
+     *            be {@code null} indicating that the JSON value was not present
+     *            in the resource.
+     * @return A {@link Promise} containing the result of the operation.
+     */
+    abstract Promise<List<Modification>, ResourceException> update(
+            RequestState requestState, JsonPointer path, Entry e, JsonValue v);
+
+    // TODO: methods for obtaining schema information (e.g. name, description, type information).
+    // TODO: methods for creating sort controls.
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
new file mode 100644
index 0000000..b991193
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -0,0 +1,84 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.rest2ldap.Utils.*;
+
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.services.context.AbstractContext;
+import org.forgerock.services.context.Context;
+
+/**
+ * A {@link Context} containing a cached pre-authenticated LDAP connection which
+ * should be re-used for performing subsequent LDAP operations. The LDAP
+ * connection is typically acquired while perform authentication in an HTTP
+ * servlet filter. It is the responsibility of the component which acquired the
+ * connection to release once processing has completed.
+ */
+public final class AuthenticatedConnectionContext extends AbstractContext {
+    /*
+     * TODO: this context does not support persistence because there is no
+     * obvious way to restore the connection. We could just persist the context
+     * and restore it as null, and let rest2ldap switch to using the factory +
+     * proxied authz.
+     */
+    private final Connection connection;
+
+    /**
+     * Creates a new pre-authenticated cached LDAP connection context having the
+     * provided parent and an ID automatically generated using
+     * {@code UUID.randomUUID()}.
+     *
+     * @param parent
+     *            The parent context.
+     * @param connection
+     *            The cached pre-authenticated LDAP connection which should be
+     *            re-used for subsequent LDAP operations.
+     */
+    public AuthenticatedConnectionContext(final Context parent, final Connection connection) {
+        super(ensureNotNull(parent), "authenticated connection");
+        this.connection = connection;
+    }
+
+    /**
+     * Creates a new pre-authenticated cached LDAP connection context having the
+     * provided ID and parent.
+     *
+     * @param id
+     *            The context ID.
+     * @param parent
+     *            The parent context.
+     * @param connection
+     *            The cached pre-authenticated LDAP connection which should be
+     *            re-used for subsequent LDAP operations.
+     */
+    AuthenticatedConnectionContext(final String id, final Context parent,
+            final Connection connection) {
+        super(id, "authenticated connection", ensureNotNull(parent));
+        this.connection = connection;
+    }
+
+    /**
+     * Returns the cached pre-authenticated LDAP connection which should be
+     * re-used for subsequent LDAP operations.
+     *
+     * @return The cached pre-authenticated LDAP connection which should be
+     *         re-used for subsequent LDAP operations.
+     */
+    Connection getConnection() {
+        return connection;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java
new file mode 100644
index 0000000..cb7d0fb
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java
@@ -0,0 +1,42 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+/**
+ * The policy which should be for performing authorization.
+ */
+public enum AuthorizationPolicy {
+    /**
+     * Use connections acquired from the LDAP connection factory. Don't use
+     * proxied authorization, and don't use cached pre-authenticated
+     * connections.
+     */
+    NONE,
+
+    /**
+     * Use the connection obtained during LDAP authentication. If no connection
+     * was passed through the authorization will fail.
+     */
+    REUSE,
+
+    /**
+     * Use proxied authorization with an authorization ID derived from the
+     * proxied authorization ID template. Proxied authorization will only be
+     * used if there is no pre-authenticated connection available.
+     */
+    PROXY;
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
new file mode 100644
index 0000000..0ee94a6
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -0,0 +1,149 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.forgerock.json.resource.ForbiddenException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * An authorization ID template used for mapping security context principals to
+ * AuthzID templates of the form
+ * <code>dn:uid={uid},ou={realm},dc=example,dc=com</code>, or
+ * <code>u:{uid}@{realm}.example.com</code>.
+ */
+final class AuthzIdTemplate {
+    private static interface Impl {
+        String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema)
+                throws ResourceException;
+    }
+
+    private static final Impl DN_IMPL = new Impl() {
+
+        @Override
+        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
+                final Schema schema) throws ResourceException {
+            final String authzId = String.format(Locale.ENGLISH, t.formatString, templateVariables);
+            try {
+                // Validate the DN.
+                DN.valueOf(authzId.substring(3), schema);
+            } catch (final IllegalArgumentException e) {
+                throw new ForbiddenException(
+                        i18n("The request could not be authorized because the required "
+                                + "security principal was not a valid LDAP DN"));
+            }
+            return authzId;
+        }
+    };
+
+    private static final Pattern DN_PATTERN = Pattern.compile("^dn:\\{[^}]+\\}$");
+
+    private static final Impl DN_TEMPLATE_IMPL = new Impl() {
+
+        @Override
+        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
+                final Schema schema) throws ResourceException {
+            return "dn:" + DN.format(t.dnFormatString, schema, templateVariables);
+        }
+
+    };
+
+    private static final Pattern KEY_RE = Pattern.compile("\\{([^}]+)\\}");
+
+    private static final Impl UID_TEMPLATE_IMPL = new Impl() {
+
+        @Override
+        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
+                final Schema schema) throws ResourceException {
+            return String.format(Locale.ENGLISH, t.formatString, templateVariables);
+        }
+
+    };
+
+    private final String dnFormatString;
+    private final String formatString;
+    private final List<String> keys = new ArrayList<>();
+    private final Impl pimpl;
+    private final String template;
+
+    AuthzIdTemplate(final String template) {
+        if (!template.startsWith("u:") && !template.startsWith("dn:")) {
+            throw new IllegalArgumentException("Invalid authorization ID template: " + template);
+        }
+
+        // Parse the template keys and replace them with %s for formatting.
+        final Matcher matcher = KEY_RE.matcher(template);
+        final StringBuffer buffer = new StringBuffer(template.length());
+        while (matcher.find()) {
+            matcher.appendReplacement(buffer, "%s");
+            keys.add(matcher.group(1));
+        }
+        matcher.appendTail(buffer);
+        this.formatString = buffer.toString();
+        this.template = template;
+
+        if (template.startsWith("dn:")) {
+            this.pimpl = DN_PATTERN.matcher(template).matches() ? DN_IMPL : DN_TEMPLATE_IMPL;
+            this.dnFormatString = formatString.substring(3);
+        } else {
+            this.pimpl = UID_TEMPLATE_IMPL;
+            this.dnFormatString = null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return template;
+    }
+
+    String formatAsAuthzId(final Map<String, Object> principals, final Schema schema)
+            throws ResourceException {
+        final String[] templateVariables = getPrincipalsForFormatting(principals);
+        return pimpl.formatAsAuthzId(this, templateVariables, schema);
+    }
+
+    private String[] getPrincipalsForFormatting(final Map<String, Object> principals)
+            throws ForbiddenException {
+        final String[] values = new String[keys.size()];
+        for (int i = 0; i < values.length; i++) {
+            final String key = keys.get(i);
+            final Object value = principals.get(key);
+            if (isJSONPrimitive(value)) {
+                values[i] = String.valueOf(value);
+            } else if (value != null) {
+                throw new ForbiddenException(i18n(
+                        "The request could not be authorized because the required "
+                                + "security principal '%s' had an invalid data type", key));
+            } else {
+                throw new ForbiddenException(i18n(
+                        "The request could not be authorized because the required "
+                                + "security principal '%s' could not be determined", key));
+            }
+        }
+        return values;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
new file mode 100644
index 0000000..101e53d
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -0,0 +1,135 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * Common configuration options.
+ */
+final class Config {
+    private final AuthorizationPolicy authzPolicy;
+    private final ConnectionFactory factory;
+    private final DecodeOptions options;
+    private final AuthzIdTemplate proxiedAuthzTemplate;
+    private final ReadOnUpdatePolicy readOnUpdatePolicy;
+    private final Schema schema;
+    private final boolean useSubtreeDelete;
+    private final boolean usePermissiveModify;
+
+    Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy,
+            final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate,
+            final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) {
+        this.factory = factory;
+        this.readOnUpdatePolicy = readOnUpdatePolicy;
+        this.authzPolicy = authzPolicy;
+        this.proxiedAuthzTemplate = proxiedAuthzTemplate;
+        this.useSubtreeDelete = useSubtreeDelete;
+        this.usePermissiveModify = usePermissiveModify;
+        this.schema = schema;
+        this.options = new DecodeOptions().setSchema(schema);
+    }
+
+    /**
+     * Returns the LDAP SDK connection factory which should be used when
+     * performing LDAP operations.
+     *
+     * @return The LDAP SDK connection factory which should be used when
+     *         performing LDAP operations.
+     */
+    ConnectionFactory connectionFactory() {
+        return factory;
+    }
+
+    /**
+     * Returns the decoding options which should be used when decoding controls
+     * in responses.
+     *
+     * @return The decoding options which should be used when decoding controls
+     *         in responses.
+     */
+    DecodeOptions decodeOptions() {
+        return options;
+    }
+
+    /**
+     * Returns the authorization policy which should be used for performing LDAP
+     * operations.
+     *
+     * @return The authorization policy which should be used for performing LDAP
+     *         operations.
+     */
+    AuthorizationPolicy getAuthorizationPolicy() {
+        return authzPolicy;
+    }
+
+    /**
+     * Returns the authorization ID template which should be used when proxied
+     * authorization is enabled.
+     *
+     * @return The authorization ID template which should be used when proxied
+     *         authorization is enabled.
+     */
+    AuthzIdTemplate getProxiedAuthorizationTemplate() {
+        return proxiedAuthzTemplate;
+    }
+
+    /**
+     * Returns {@code true} if modify requests should include the permissive
+     * modify control.
+     *
+     * @return {@code true} if modify requests should include the permissive
+     *         modify control.
+     */
+    boolean usePermissiveModify() {
+        return usePermissiveModify;
+    }
+
+    /**
+     * Returns {@code true} if delete requests should include the subtree delete
+     * control.
+     *
+     * @return {@code true} if delete requests should include the subtree delete
+     *         control.
+     */
+    boolean useSubtreeDelete() {
+        return useSubtreeDelete;
+    }
+
+    /**
+     * Returns the policy which should be used in order to read an entry before
+     * it is deleted, or after it is added or modified.
+     *
+     * @return The policy which should be used in order to read an entry before
+     *         it is deleted, or after it is added or modified.
+     */
+    ReadOnUpdatePolicy readOnUpdatePolicy() {
+        return readOnUpdatePolicy;
+    }
+
+    /**
+     * Returns the schema which should be used when attribute types and
+     * controls.
+     *
+     * @return The schema which should be used when attribute types and
+     *         controls.
+     */
+    Schema schema() {
+        return schema;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
new file mode 100644
index 0000000..6e226db
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
@@ -0,0 +1,85 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import org.forgerock.util.query.QueryFilter;
+
+/** An enumeration of the commons REST query comparison filter types. */
+enum FilterType {
+
+    /**
+     * Substring filter.
+     *
+     * @see QueryFilter#contains
+     */
+    CONTAINS,
+
+    /**
+     * Equality filter.
+     *
+     * @see QueryFilter#equalTo
+     */
+    EQUAL_TO,
+
+    /**
+     * Extended match filter.
+     *
+     * @see QueryFilter#comparisonFilter
+     */
+    EXTENDED,
+
+    /**
+     * Greater than ordering filter.
+     *
+     * @see QueryFilter#greaterThan
+     */
+    GREATER_THAN,
+
+    /**
+     * Greater than or equal to ordering filter.
+     *
+     * @see QueryFilter#greaterThanOrEqualTo
+     */
+    GREATER_THAN_OR_EQUAL_TO,
+
+    /**
+     * Less than ordering filter.
+     *
+     * @see QueryFilter#lessThan
+     */
+    LESS_THAN,
+
+    /**
+     * Less than or equal to ordering filter.
+     *
+     * @see QueryFilter#lessThanOrEqualTo
+     */
+    LESS_THAN_OR_EQUAL_TO,
+
+    /**
+     * Presence filter.
+     *
+     * @see QueryFilter#present
+     */
+    PRESENT,
+
+    /**
+     * Initial sub-string filter.
+     *
+     * @see QueryFilter#startsWith
+     */
+    STARTS_WITH
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
new file mode 100644
index 0000000..306cf3c
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
@@ -0,0 +1,385 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.json.resource.ResourceException.newResourceException;
+import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
+import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
+import static org.forgerock.opendj.ldap.Connections.uncloseable;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.Closeable;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.AuthenticationException;
+import org.forgerock.opendj.ldap.AuthorizationException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.SecurityContext;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+
+/** An LDAP based HTTP authentication filter. */
+final class HttpAuthenticationFilter implements org.forgerock.http.Filter, Closeable {
+
+    /** Indicates how authentication should be performed. */
+    private enum AuthenticationMethod {
+        SASL_PLAIN,
+        SEARCH_SIMPLE,
+        SIMPLE
+    }
+
+    private final Schema schema = Schema.getDefaultSchema();
+    private final String altAuthenticationPasswordHeader;
+    private final String altAuthenticationUsernameHeader;
+    private final AuthenticationMethod authenticationMethod;
+    private final ConnectionFactory bindLDAPConnectionFactory;
+    private final boolean reuseAuthenticatedConnection;
+    private final String saslAuthzIdTemplate;
+    private final DN searchBaseDN;
+    private final String searchFilterTemplate;
+    private final ConnectionFactory searchLDAPConnectionFactory;
+    private final SearchScope searchScope;
+    private final boolean supportAltAuthentication;
+
+    private final boolean supportHTTPBasicAuthentication;
+
+    HttpAuthenticationFilter(final JsonValue configuration) {
+        // Parse the authentication configuration.
+        final JsonValue authnConfig = configuration.get("authenticationFilter");
+        supportHTTPBasicAuthentication = authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
+
+        // Alternative HTTP authentication.
+        supportAltAuthentication = authnConfig.get("supportAltAuthentication").required().asBoolean();
+        if (supportAltAuthentication) {
+            altAuthenticationUsernameHeader = authnConfig.get("altAuthenticationUsernameHeader").required().asString();
+            altAuthenticationPasswordHeader = authnConfig.get("altAuthenticationPasswordHeader").required().asString();
+        } else {
+            altAuthenticationUsernameHeader = null;
+            altAuthenticationPasswordHeader = null;
+        }
+
+        // Should the authenticated connection should be cached for use by subsequent LDAP operations?
+        reuseAuthenticatedConnection = authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
+
+        // Parse the authentication method and associated parameters.
+        authenticationMethod = parseAuthenticationMethod(authnConfig);
+        switch (authenticationMethod) {
+        case SASL_PLAIN:
+            saslAuthzIdTemplate = authnConfig.get("saslAuthzIdTemplate").required().asString();
+            searchBaseDN = null;
+            searchScope = null;
+            searchFilterTemplate = null;
+            searchLDAPConnectionFactory = null;
+            break;
+        case SEARCH_SIMPLE:
+            searchBaseDN = DN.valueOf(authnConfig.get("searchBaseDN").required().asString(), schema);
+            searchScope = parseSearchScope(authnConfig);
+            searchFilterTemplate = authnConfig.get("searchFilterTemplate").required().asString();
+
+            // Parse the LDAP connection factory to be used for searches.
+            final String ldapFactoryName = authnConfig.get("searchLDAPConnectionFactory").required().asString();
+            searchLDAPConnectionFactory = Rest2LDAP
+                .configureConnectionFactory(configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+
+            saslAuthzIdTemplate = null;
+            break;
+        case SIMPLE:
+        default:
+            saslAuthzIdTemplate = null;
+            searchBaseDN = null;
+            searchScope = null;
+            searchFilterTemplate = null;
+            searchLDAPConnectionFactory = null;
+            break;
+        }
+
+        // Parse the LDAP connection factory to be used for binds.
+        final String ldapFactoryName = authnConfig.get("bindLDAPConnectionFactory").required().asString();
+        bindLDAPConnectionFactory = Rest2LDAP.configureConnectionFactory(
+            configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+    }
+
+    private static AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
+        if (configuration.isDefined("method")) {
+            final String method = configuration.get("method").asString();
+            if ("simple".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SIMPLE;
+            } else if ("sasl-plain".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SASL_PLAIN;
+            } else if ("search-simple".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SEARCH_SIMPLE;
+            } else {
+                throw new JsonValueException(configuration,
+                    "Illegal authentication method: must be either 'simple', 'sasl-plain', or 'search-simple'");
+            }
+        } else {
+            return AuthenticationMethod.SEARCH_SIMPLE;
+        }
+    }
+
+    private static SearchScope parseSearchScope(final JsonValue configuration) {
+        if (configuration.isDefined("searchScope")) {
+            final String scope = configuration.get("searchScope").asString();
+            if ("sub".equalsIgnoreCase(scope)) {
+                return SearchScope.WHOLE_SUBTREE;
+            } else if ("one".equalsIgnoreCase(scope)) {
+                return SearchScope.SINGLE_LEVEL;
+            } else {
+                throw new JsonValueException(configuration, "Illegal search scope: must be either 'sub' or 'one'");
+            }
+        } else {
+            return SearchScope.WHOLE_SUBTREE;
+        }
+    }
+
+    @Override
+    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
+                                                          final Handler next) {
+        // Store the authenticated connection so that it can be re-used by the handler if needed.
+        // However, make sure that it is closed on completion.
+        try {
+            final String headerUsername =
+                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationUsernameHeader) : null;
+            final String headerPassword =
+                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationPasswordHeader) : null;
+            final String headerAuthorization =
+                supportHTTPBasicAuthentication ? request.getHeaders().getFirst("Authorization") : null;
+
+            final String username;
+            final char[] password;
+            if (headerUsername != null) {
+                if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
+                    throw newResourceException(401);
+                }
+                username = headerUsername;
+                password = headerPassword.toCharArray();
+            } else if (headerAuthorization != null) {
+                final StringTokenizer st = new StringTokenizer(headerAuthorization);
+                final String method = st.nextToken();
+                if (method == null || !"BASIC".equalsIgnoreCase(method)) {
+                    throw newResourceException(401);
+                }
+                final String b64Credentials = st.nextToken();
+                if (b64Credentials == null) {
+                    throw newResourceException(401);
+                }
+                final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
+                final String[] usernameAndPassword = credentials.split(":");
+                if (usernameAndPassword.length != 2) {
+                    throw newResourceException(401);
+                }
+                username = usernameAndPassword[0];
+                password = usernameAndPassword[1].toCharArray();
+            } else {
+                throw newResourceException(401);
+            }
+
+            // If we've got here then we have a username and password.
+            switch (authenticationMethod) {
+            case SIMPLE: {
+                final Map<String, Object> authzid;
+                authzid = new LinkedHashMap<>(2);
+                authzid.put(AUTHZID_DN, username);
+                authzid.put(AUTHZID_ID, username);
+                return doBind(
+                    context, request, next, Requests.newSimpleBindRequest(username, password), username, authzid);
+            }
+            case SASL_PLAIN: {
+                final Map<String, Object> authzid;
+                final String bindId;
+                if (saslAuthzIdTemplate.startsWith("dn:")) {
+                    final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString();
+                    bindId = "dn:" + bindDN;
+                    authzid = new LinkedHashMap<>(2);
+                    authzid.put(AUTHZID_DN, bindDN);
+                    authzid.put(AUTHZID_ID, username);
+                } else {
+                    bindId = String.format(saslAuthzIdTemplate, username);
+                    authzid = Collections.singletonMap(AUTHZID_ID, (Object) username);
+                }
+                return doBind(context, request, next, newPlainSASLBindRequest(bindId, password), username, authzid);
+            }
+            default: // SEARCH_SIMPLE
+                final AtomicReference<Connection> savedConnection = new AtomicReference<>();
+                return searchLDAPConnectionFactory.getConnectionAsync()
+                    .thenAsync(doSearchForUser(username, savedConnection))
+                    .thenAsync(doBindAfterSearch(context, request, next, username, password, savedConnection),
+                        returnErrorAfterFailedSearch(savedConnection));
+            }
+        } catch (final Throwable t) {
+            return asErrorResponse(t);
+        }
+    }
+
+    private AsyncFunction<Connection, SearchResultEntry, LdapException> doSearchForUser(
+            final String username, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
+            @Override
+            public Promise<SearchResultEntry, LdapException> apply(final Connection connection) throws LdapException {
+                savedConnection.set(connection);
+                final Filter filter = Filter.format(searchFilterTemplate, username);
+                return connection.searchSingleEntryAsync(newSearchRequest(searchBaseDN, searchScope, filter, "1.1"));
+            }
+        };
+    }
+
+    private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
+            final Context context, final Request request, final Handler next, final String username,
+            final char[] password, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final SearchResultEntry entry) {
+                closeConnection(savedConnection);
+                final String bindDN = entry.getName().toString();
+                final Map<String, Object> authzid = new LinkedHashMap<>(2);
+                authzid.put(AUTHZID_DN, bindDN);
+                authzid.put(AUTHZID_ID, username);
+                return doBind(context, request, next, newSimpleBindRequest(bindDN, password), username, authzid);
+            }
+        };
+    }
+
+    /**
+     * Get a bind connection and then perform the bind operation, setting the cached connection and authorization
+     * credentials on completion.
+     */
+    private Promise<Response, NeverThrowsException> doBind(
+            final Context context, final Request request, final Handler next, final BindRequest bindRequest,
+            final String authcid, final Map<String, Object> authzid) {
+        final AtomicReference<Connection> savedConnection = new AtomicReference<>();
+        return bindLDAPConnectionFactory.getConnectionAsync()
+            .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                @Override
+                public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException {
+                    savedConnection.set(connection);
+                    return connection.bindAsync(bindRequest);
+                }
+            })
+            .thenAsync(doChain(context, request, next, authcid, authzid, savedConnection),
+                       returnErrorAfterFailedBind())
+            .thenFinally(new Runnable() {
+                @Override
+                public void run() {
+                    closeConnection(savedConnection);
+                }
+            });
+    }
+
+    private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
+            final Context context, final Request request, final Handler next, final String authcid,
+            final Map<String, Object> authzid, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<BindResult, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final BindResult result) {
+                // Pass through the authentication ID and authorization principals.
+                Context forwardedContext = new SecurityContext(context, authcid, authzid);
+
+                // Cache the pre-authenticated connection and prevent downstream
+                // components from closing it since this filter will close it.
+                if (reuseAuthenticatedConnection) {
+                    forwardedContext = new AuthenticatedConnectionContext(
+                            forwardedContext, uncloseable(savedConnection.get()));
+                }
+
+                return next.handle(forwardedContext, request);
+            }
+        };
+    }
+
+    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
+            final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
+                if (closeConnection(savedConnection)) {
+                    // The search error should not be passed as-is back to the user.
+                    if (e instanceof EntryNotFoundException || e instanceof MultipleEntriesFoundException) {
+                        return asErrorResponse(newLdapException(ResultCode.INVALID_CREDENTIALS, e));
+                    } else if (e instanceof AuthenticationException || e instanceof AuthorizationException) {
+                        return asErrorResponse(newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e));
+                    } else {
+                        return asErrorResponse(e);
+                    }
+                } else {
+                    return asErrorResponse(e);
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind() {
+        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
+                return asErrorResponse(e);
+            }
+        };
+    }
+
+    private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) {
+        final ResourceException e = asResourceException(t);
+        final Response response =
+            new Response().setStatus(Status.valueOf(e.getCode())).setEntity(e.toJsonValue().getObject());
+        return Promises.newResultPromise(response);
+    }
+
+    private boolean closeConnection(final AtomicReference<Connection> savedConnection) {
+        final Connection connection = savedConnection.get();
+        if (connection != null) {
+            connection.close();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void close() {
+        closeSilently(searchLDAPConnectionFactory, bindLDAPConnectionFactory);
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
new file mode 100644
index 0000000..f916a0d
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -0,0 +1,151 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
+import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
+import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
+import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+
+/**
+ * An attribute mapper which maps a single JSON attribute to a fixed value.
+ */
+final class JSONConstantAttributeMapper extends AttributeMapper {
+    private final JsonValue value;
+
+    JSONConstantAttributeMapper(final Object value) {
+        this.value = new JsonValue(value);
+    }
+
+    @Override
+    public String toString() {
+        return "constant(" + value + ")";
+    }
+
+    @Override
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
+        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
+            return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                    "The request cannot be processed because it attempts to create the read-only field '%s'", path)));
+        } else {
+            return Promises.newResultPromise(Collections.<Attribute> emptyList());
+        }
+    }
+
+    @Override
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
+            final Set<String> ldapAttributes) {
+        // Nothing to do.
+    }
+
+    @Override
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
+        final Filter filter;
+        final JsonValue subValue = value.get(subPath);
+        if (subValue == null) {
+            filter = alwaysFalse();
+        } else if (type == FilterType.PRESENT) {
+            filter = alwaysTrue();
+        } else if (value.isString() && valueAssertion instanceof String) {
+            final String v1 = toLowerCase(value.asString());
+            final String v2 = toLowerCase((String) valueAssertion);
+            switch (type) {
+            case CONTAINS:
+                filter = toFilter(v1.contains(v2));
+                break;
+            case STARTS_WITH:
+                filter = toFilter(v1.startsWith(v2));
+                break;
+            default:
+                filter = compare(type, v1, v2);
+                break;
+            }
+        } else if (value.isNumber() && valueAssertion instanceof Number) {
+            final Double v1 = value.asDouble();
+            final Double v2 = ((Number) valueAssertion).doubleValue();
+            filter = compare(type, v1, v2);
+        } else if (value.isBoolean() && valueAssertion instanceof Boolean) {
+            final Boolean v1 = value.asBoolean();
+            final Boolean v2 = (Boolean) valueAssertion;
+            filter = compare(type, v1, v2);
+        } else {
+            // This attribute mapper is a candidate but it does not match.
+            filter = alwaysFalse();
+        }
+        return Promises.newResultPromise(filter);
+    }
+
+    @Override
+    Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path,
+            final PatchOperation operation) {
+        return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                "The request cannot be processed because it attempts to patch the read-only field '%s'", path)));
+    }
+
+    @Override
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
+        return Promises.newResultPromise(value.copy());
+    }
+
+    @Override
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
+        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
+            return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                    "The request cannot be processed because it attempts to modify the read-only field '%s'", path)));
+        } else {
+            return Promises.newResultPromise(Collections.<Modification> emptyList());
+        }
+    }
+
+    private <T extends Comparable<T>> Filter compare(final FilterType type, final T v1, final T v2) {
+        switch (type) {
+        case EQUAL_TO:
+            return toFilter(v1.equals(v2));
+        case GREATER_THAN:
+            return toFilter(v1.compareTo(v2) > 0);
+        case GREATER_THAN_OR_EQUAL_TO:
+            return toFilter(v1.compareTo(v2) >= 0);
+        case LESS_THAN:
+            return toFilter(v1.compareTo(v2) < 0);
+        case LESS_THAN_OR_EQUAL_TO:
+            return toFilter(v1.compareTo(v2) <= 0);
+        default:
+            return alwaysFalse(); // Not supported.
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
new file mode 100644
index 0000000..382522b
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -0,0 +1,1054 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Arrays.asList;
+import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
+import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
+import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.json.resource.ActionRequest;
+import org.forgerock.json.resource.ActionResponse;
+import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.CreateRequest;
+import org.forgerock.json.resource.DeleteRequest;
+import org.forgerock.json.resource.NotSupportedException;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.PatchRequest;
+import org.forgerock.json.resource.PreconditionFailedException;
+import org.forgerock.json.resource.QueryRequest;
+import org.forgerock.json.resource.QueryResourceHandler;
+import org.forgerock.json.resource.QueryResponse;
+import org.forgerock.json.resource.ReadRequest;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.json.resource.ResourceResponse;
+import org.forgerock.json.resource.Responses;
+import org.forgerock.json.resource.UncategorizedException;
+import org.forgerock.json.resource.UpdateRequest;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldif.ChangeRecord;
+import org.forgerock.services.context.ClientContext;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.SecurityContext;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.query.QueryFilter;
+import org.forgerock.util.query.QueryFilterVisitor;
+
+/**
+ * A {@code CollectionResourceProvider} implementation which maps a JSON
+ * resource collection to LDAP entries beneath a base DN.
+ */
+final class LDAPCollectionResourceProvider implements CollectionResourceProvider {
+    /** Dummy exception used for signalling search success. */
+    private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
+
+    /** Empty decode options required for decoding response controls. */
+    private static final DecodeOptions DECODE_OPTIONS = new DecodeOptions();
+
+    private final List<Attribute> additionalLDAPAttributes;
+    private final AttributeMapper attributeMapper;
+    private final DN baseDN; // TODO: support template variables.
+    private final Config config;
+    private final AttributeDescription etagAttribute;
+    private final NameStrategy nameStrategy;
+
+    LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper,
+            final NameStrategy nameStrategy, final AttributeDescription etagAttribute,
+            final Config config, final List<Attribute> additionalLDAPAttributes) {
+        this.baseDN = baseDN;
+        this.attributeMapper = mapper;
+        this.config = config;
+        this.nameStrategy = nameStrategy;
+        this.etagAttribute = etagAttribute;
+        this.additionalLDAPAttributes = additionalLDAPAttributes;
+    }
+
+    @Override
+    public Promise<ActionResponse, ResourceException> actionCollection(
+            final Context context, final ActionRequest request) {
+        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
+                                                            new NotSupportedException("Not yet implemented"));
+    }
+
+    @Override
+    public Promise<ActionResponse, ResourceException> actionInstance(
+            final Context context, final String resourceId, final ActionRequest request) {
+        String actionId = request.getAction();
+        if (actionId.equals("passwordModify")) {
+            return passwordModify(context, resourceId, request);
+        }
+        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
+                new NotSupportedException("The action '" + actionId + "' is not supported"));
+    }
+
+    private Promise<ActionResponse, ResourceException> passwordModify(
+            final Context context, final String resourceId, final ActionRequest request) {
+        if (!context.containsContext(ClientContext.class)
+                || !context.asContext(ClientContext.class).isSecure()) {
+            return Promises.newExceptionPromise(ResourceException.newResourceException(
+                    ResourceException.FORBIDDEN, "Password modify requires a secure connection."));
+        }
+        if (!context.containsContext(SecurityContext.class)
+                || context.asContext(SecurityContext.class).getAuthenticationId() == null) {
+            return Promises.newExceptionPromise(ResourceException.newResourceException(
+                    ResourceException.FORBIDDEN, "Password modify requires user to be authenticated."));
+        }
+
+        final JsonValue jsonContent = request.getContent();
+        final String oldPassword;
+        final String newPassword;
+        try {
+            oldPassword = jsonContent.get("oldPassword").asString();
+            newPassword = jsonContent.get("newPassword").asString();
+        } catch (JsonValueException e) {
+            return Promises.newExceptionPromise(
+                    ResourceException.newResourceException(ResourceException.BAD_REQUEST, e.getLocalizedMessage(), e));
+        }
+
+        final RequestState requestState = wrap(context);
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, ActionResponse, ResourceException>() {
+                    @Override
+                    public Promise<ActionResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        List<JsonPointer> attrs = Collections.emptyList();
+                        return connection.searchSingleEntryAsync(searchRequest(requestState, resourceId, attrs))
+                                .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() {
+                                    @Override
+                                    public Promise<ActionResponse, ResourceException> apply(
+                                              final SearchResultEntry entry) {
+                                        PasswordModifyExtendedRequest pwdModifyRequest =
+                                                Requests.newPasswordModifyExtendedRequest();
+                                        pwdModifyRequest.setUserIdentity("dn: " + entry.getName());
+                                        pwdModifyRequest.setOldPassword(asBytes(oldPassword));
+                                        pwdModifyRequest.setNewPassword(asBytes(newPassword));
+                                        return connection.extendedRequestAsync(pwdModifyRequest)
+                                            .thenAsync(new AsyncFunction<PasswordModifyExtendedResult,
+                                                    ActionResponse, ResourceException>() {
+                                                @Override
+                                                public Promise<ActionResponse, ResourceException> apply(
+                                                        PasswordModifyExtendedResult value) throws ResourceException {
+                                                    JsonValue result = new JsonValue(new LinkedHashMap<>());
+                                                    byte[] generatedPwd = value.getGeneratedPassword();
+                                                    if (generatedPwd != null) {
+                                                        result = result.put("generatedPassword",
+                                                                ByteString.valueOfBytes(generatedPwd).toString());
+                                                    }
+                                                    return Responses.newActionResponse(result).asPromise();
+                                                }
+                                            }, ldapExceptionToResourceException());
+                                    }
+                                }, ldapExceptionToResourceException());
+                    }
+
+                    private AsyncFunction<LdapException, ActionResponse, ResourceException>
+                    ldapExceptionToResourceException() {
+                        return ldapToResourceException();
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    private byte[] asBytes(final String s) {
+        return s != null ? s.getBytes(StandardCharsets.UTF_8) : null;
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> createInstance(
+            final Context context, final CreateRequest request) {
+        final RequestState requestState = wrap(context);
+
+        return requestState.getConnection().thenAsync(
+            new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                @Override
+                public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                        throws ResourceException {
+                    // Calculate entry content.
+                    return attributeMapper.create(requestState, new JsonPointer(), request.getContent())
+                            .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
+                                @Override
+                                public Promise<ResourceResponse, ResourceException> apply(
+                                        final List<Attribute> attributes) {
+                                    // Perform add operation.
+                                    final AddRequest addRequest = newAddRequest(DN.rootDN());
+                                    for (final Attribute attribute : additionalLDAPAttributes) {
+                                        addRequest.addAttribute(attribute);
+                                    }
+                                    for (final Attribute attribute : attributes) {
+                                        addRequest.addAttribute(attribute);
+                                    }
+                                    try {
+                                        nameStrategy.setResourceId(requestState, getBaseDN(),
+                                                request.getNewResourceId(), addRequest);
+                                    } catch (final ResourceException e) {
+                                        return Promises.newExceptionPromise(e);
+                                    }
+                                    if (config.readOnUpdatePolicy() == CONTROLS) {
+                                        addRequest.addControl(PostReadRequestControl.newControl(
+                                                false, getLDAPAttributes(requestState, request.getFields())));
+                                    }
+                                    return connection.applyChangeAsync(addRequest)
+                                                     .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                ldapExceptionToResourceException());
+                                }
+                            });
+                }
+            }).thenFinally(close(requestState));
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> deleteInstance(
+            final Context context, final String resourceId, final DeleteRequest request) {
+        final RequestState requestState = wrap(context);
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        return requestState.getConnection()
+                .thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
+                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
+                        try {
+                            final ChangeRecord deleteRequest = newDeleteRequest(dn);
+                            if (config.readOnUpdatePolicy() == CONTROLS) {
+                                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+                                deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
+                            }
+                            if (config.useSubtreeDelete()) {
+                                deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
+                            }
+                            addAssertionControl(deleteRequest, request.getRevision());
+                            return connectionHolder.get().applyChangeAsync(deleteRequest)
+                                                         .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                    ldapExceptionToResourceException());
+
+                        } catch (final Exception e) {
+                            return Promises.newExceptionPromise(asResourceException(e));
+                        }
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> patchInstance(
+            final Context context, final String resourceId, final PatchRequest request) {
+        final RequestState requestState = wrap(context);
+
+        if (request.getPatchOperations().isEmpty()) {
+            return emptyPatchInstance(requestState, resourceId, request);
+        }
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        return requestState.getConnection()
+                .thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
+                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException {
+                        // Convert the patch operations to LDAP modifications.
+                        List<Promise<List<Modification>, ResourceException>> promises =
+                                new ArrayList<>(request.getPatchOperations().size());
+                        for (final PatchOperation operation : request.getPatchOperations()) {
+                            promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation));
+                        }
+
+                        return Promises.when(promises).thenAsync(
+                                new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
+                                    @Override
+                                    public Promise<ResourceResponse, ResourceException> apply(
+                                            final List<List<Modification>> result) {
+                                        // The patch operations have been converted successfully.
+                                        try {
+                                            final ModifyRequest modifyRequest = newModifyRequest(dn);
+
+                                            // Add the modifications.
+                                            for (final List<Modification> modifications : result) {
+                                                if (modifications != null) {
+                                                    modifyRequest.getModifications().addAll(modifications);
+                                                }
+                                            }
+
+                                            final List<String> attributes =
+                                                    asList(getLDAPAttributes(requestState, request.getFields()));
+                                            if (modifyRequest.getModifications().isEmpty()) {
+                                                // This patch is a no-op so just read the entry and check its version.
+                                                return connectionHolder.get()
+                                                        .readEntryAsync(dn, attributes)
+                                                        .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
+                                                                   ldapExceptionToResourceException());
+                                            } else {
+                                                // Add controls and perform the modify request.
+                                                if (config.readOnUpdatePolicy() == CONTROLS) {
+                                                    modifyRequest.addControl(
+                                                            PostReadRequestControl.newControl(false, attributes));
+                                                }
+                                                if (config.usePermissiveModify()) {
+                                                    modifyRequest.addControl(
+                                                            PermissiveModifyRequestControl.newControl(true));
+                                                }
+                                                addAssertionControl(modifyRequest, request.getRevision());
+                                                return connectionHolder.get()
+                                                        .applyChangeAsync(modifyRequest)
+                                                        .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                   ldapExceptionToResourceException());
+                                            }
+                                        } catch (final Exception e) {
+                                            return Promises.newExceptionPromise(asResourceException(e));
+                                        }
+                                    }
+                                });
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    /** Just read the entry and check its version. */
+    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(
+            final RequestState requestState, final String resourceId, final PatchRequest request) {
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        SearchRequest searchRequest = searchRequest(requestState, resourceId, request.getFields());
+                        return connection.searchSingleEntryAsync(searchRequest)
+                                         .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
+                                                    ldapExceptionToResourceException());
+                    }
+                });
+    }
+
+    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
+            final RequestState requestState, final PatchRequest request) {
+        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+            @Override
+            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
+                    throws ResourceException {
+                try {
+                    // Fail if there is a version mismatch.
+                    ensureMVCCVersionMatches(entry, request.getRevision());
+                    return adaptEntry(requestState, entry);
+                } catch (final Exception e) {
+                    return Promises.newExceptionPromise(asResourceException(e));
+                }
+            }
+        };
+    }
+
+    @Override
+    public Promise<QueryResponse, ResourceException> queryCollection(
+            final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
+        final RequestState requestState = wrap(context);
+
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() {
+                    @Override
+                    public Promise<QueryResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        // Calculate the filter (this may require the connection).
+                        return getLDAPFilter(requestState, request.getQueryFilter())
+                                            .thenAsync(runQuery(request, resourceHandler, requestState, connection));
+                    }
+                })
+                .thenFinally(close(requestState));
+    }
+
+    private Promise<Filter, ResourceException> getLDAPFilter(
+            final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) {
+        final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
+                new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitAndFilter(final Void unused,
+                            final List<QueryFilter<JsonPointer>> subFilters) {
+                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
+                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
+                            promises.add(subFilter.accept(this, unused));
+                        }
+
+                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
+                            @Override
+                            public Filter apply(final List<Filter> value) {
+                                // Check for unmapped filter components and optimize.
+                                final Iterator<Filter> i = value.iterator();
+                                while (i.hasNext()) {
+                                    final Filter f = i.next();
+                                    if (f == alwaysFalse()) {
+                                        return alwaysFalse();
+                                    } else if (f == alwaysTrue()) {
+                                        i.remove();
+                                    }
+                                }
+                                switch (value.size()) {
+                                case 0:
+                                    return alwaysTrue();
+                                case 1:
+                                    return value.get(0);
+                                default:
+                                    return Filter.and(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitBooleanLiteralFilter(
+                            final Void unused, final boolean value) {
+                        return Promises.newResultPromise(toFilter(value));
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitContainsFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitEqualsFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
+                            final JsonPointer field, final String operator, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
+                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitLessThanFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
+                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitNotFilter(
+                            final Void unused, final QueryFilter<JsonPointer> subFilter) {
+                        return subFilter.accept(this, unused).then(new Function<Filter, Filter, ResourceException>() {
+                            @Override
+                            public Filter apply(final Filter value) {
+                                if (value == null || value == alwaysFalse()) {
+                                    return alwaysTrue();
+                                } else if (value == alwaysTrue()) {
+                                    return alwaysFalse();
+                                } else {
+                                    return Filter.not(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitOrFilter(final Void unused,
+                            final List<QueryFilter<JsonPointer>> subFilters) {
+                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
+                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
+                            promises.add(subFilter.accept(this, unused));
+                        }
+
+                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
+                            @Override
+                            public Filter apply(final List<Filter> value) {
+                                // Check for unmapped filter components and optimize.
+                                final Iterator<Filter> i = value.iterator();
+                                while (i.hasNext()) {
+                                    final Filter f = i.next();
+                                    if (f == alwaysFalse()) {
+                                        i.remove();
+                                    } else if (f == alwaysTrue()) {
+                                        return alwaysTrue();
+                                    }
+                                }
+                                switch (value.size()) {
+                                case 0:
+                                    return alwaysFalse();
+                                case 1:
+                                    return value.get(0);
+                                default:
+                                    return Filter.or(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitPresentFilter(
+                            final Void unused, final JsonPointer field) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.PRESENT, null, null);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitStartsWithFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
+                    }
+
+                };
+        // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
+        return queryFilter.accept(visitor, null);
+    }
+
+    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
+            final QueryResourceHandler resourceHandler, final RequestState requestState, final Connection connection) {
+        return new AsyncFunction<Filter, QueryResponse, ResourceException>() {
+            /**
+             * The following fields are guarded by sequenceLock. In addition,
+             * the sequenceLock ensures that we send one JSON resource at a time
+             * back to the client.
+             */
+            private final Object sequenceLock = new Object();
+            private String cookie;
+            private ResourceException pendingResult;
+            private int pendingResourceCount;
+            private boolean resultSent;
+            private int totalResourceCount;
+
+            @Override
+            public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) {
+                if (ldapFilter == null || ldapFilter == alwaysFalse()) {
+                    // Avoid performing a search if the filter could not be mapped or if it will never match.
+                    return Promises.newResultPromise(Responses.newQueryResponse());
+                }
+                final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
+                // Perform the search.
+                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+                final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
+                                                                              : ldapFilter;
+                final SearchRequest searchRequest = newSearchRequest(
+                        getBaseDN(), SearchScope.SINGLE_LEVEL, searchFilter, attributes);
+
+                // Add the page results control. We can support the page offset by
+                // reading the next offset pages, or offset x page size resources.
+                final int pageResultStartIndex;
+                final int pageSize = request.getPageSize();
+                if (request.getPageSize() > 0) {
+                    final int pageResultEndIndex;
+                    if (request.getPagedResultsOffset() > 0) {
+                        pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
+                        pageResultEndIndex = pageResultStartIndex + pageSize;
+                    } else {
+                        pageResultStartIndex = 0;
+                        pageResultEndIndex = pageSize;
+                    }
+                    final ByteString cookie = request.getPagedResultsCookie() != null
+                            ? ByteString.valueOfBase64(request.getPagedResultsCookie()) : ByteString.empty();
+                    final SimplePagedResultsControl control =
+                            SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
+                    searchRequest.addControl(control);
+                } else {
+                    pageResultStartIndex = 0;
+                }
+
+                connection.searchAsync(searchRequest, new SearchResultHandler() {
+                    @Override
+                    public boolean handleEntry(final SearchResultEntry entry) {
+                        // Search result entries will be returned before the search result/error so the only reason
+                        // pendingResult will be non-null is if a mapping error has occurred.
+                        synchronized (sequenceLock) {
+                            if (pendingResult != null) {
+                                return false;
+                            }
+                            if (totalResourceCount++ < pageResultStartIndex) {
+                                // Haven't reached paged results threshold yet.
+                                return true;
+                            }
+                            pendingResourceCount++;
+                        }
+
+                        /*
+                         * FIXME: secondary asynchronous searches will complete in a non-deterministic order and
+                         * may cause the JSON resources to be returned in a different order to the order in which
+                         * the primary LDAP search results were received. This is benign at the moment, but will
+                         * need resolving when we implement server side sorting. A possible fix will be to use a
+                         * queue of pending resources (promises?). However, the queue cannot be unbounded in case
+                         * it grows very large, but it cannot be bounded either since that could cause a deadlock
+                         * between rest2ldap and the LDAP server (imagine the case where the server has a single
+                         * worker thread which is occupied processing the primary search).
+                         * The best solution is probably to process the primary search results in batches using
+                         * the paged results control.
+                         */
+                        final String id = nameStrategy.getResourceId(requestState, entry);
+                        final String revision = getRevisionFromEntry(entry);
+                        attributeMapper.read(requestState, new JsonPointer(), entry)
+                                       .thenOnResult(new ResultHandler<JsonValue>() {
+                                           @Override
+                                           public void handleResult(final JsonValue result) {
+                                               synchronized (sequenceLock) {
+                                                   pendingResourceCount--;
+                                                   if (!resultSent) {
+                                                       resourceHandler.handleResource(
+                                                               Responses.newResourceResponse(id, revision, result));
+                                                   }
+                                                   completeIfNecessary(promise);
+                                               }
+                                           }
+                                       }).thenOnException(new ExceptionHandler<ResourceException>() {
+                                           @Override
+                                           public void handleException(ResourceException exception) {
+                                               synchronized (sequenceLock) {
+                                                   pendingResourceCount--;
+                                                   completeIfNecessary(exception, promise);
+                                               }
+                                           }
+                                       });
+                        return true;
+                    }
+
+                    @Override
+                    public boolean handleReference(final SearchResultReference reference) {
+                        // TODO: should this be classed as an error since
+                        // rest2ldap assumes entries are all colocated?
+                        return true;
+                    }
+
+                }).thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        synchronized (sequenceLock) {
+                            if (request.getPageSize() > 0) {
+                                try {
+                                    final SimplePagedResultsControl control =
+                                            result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
+                                    if (control != null && !control.getCookie().isEmpty()) {
+                                        cookie = control.getCookie().toBase64String();
+                                    }
+                                } catch (final DecodeException e) {
+                                    // FIXME: need some logging.
+                                }
+                            }
+                            completeIfNecessary(SUCCESS, promise);
+                        }
+                    }
+                }).thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException exception) {
+                        synchronized (sequenceLock) {
+                            completeIfNecessary(asResourceException(exception), promise);
+                        }
+                    }
+                });
+
+                return promise;
+            }
+
+            /** This method must be invoked with the sequenceLock held. */
+            private void completeIfNecessary(
+                    final ResourceException e, final PromiseImpl<QueryResponse, ResourceException> handler) {
+                if (pendingResult == null) {
+                    pendingResult = e;
+                }
+                completeIfNecessary(handler);
+            }
+
+            /**
+             * Close out the query result set if there are no more pending
+             * resources and the LDAP result has been received.
+             * This method must be invoked with the sequenceLock held.
+             */
+            private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) {
+                if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
+                    if (pendingResult == SUCCESS) {
+                        handler.handleResult(Responses.newQueryResponse(cookie));
+                    } else {
+                        handler.handleException(pendingResult);
+                    }
+                    resultSent = true;
+                }
+            }
+        };
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> readInstance(
+            final Context context, final String resourceId, final ReadRequest request) {
+        final RequestState requestState = wrap(context);
+
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(Connection connection)
+                            throws ResourceException {
+                        // Do the search.
+                        SearchRequest searchRequest = searchRequest(requestState, resourceId, request.getFields());
+                        return connection.searchSingleEntryAsync(searchRequest)
+                                    .thenAsync(
+                                        new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                                            @Override
+                                            public Promise<ResourceResponse, ResourceException> apply(
+                                                    SearchResultEntry entry) throws ResourceException {
+                                                return adaptEntry(requestState, entry);
+                                            }
+                                        },
+                                        ldapExceptionToResourceException());
+                    }
+                })
+                .thenFinally(close(requestState));
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> updateInstance(
+            final Context context, final String resourceId, final UpdateRequest request) {
+        final RequestState requestState = wrap(context);
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+
+        return requestState.getConnection().thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        List<JsonPointer> attrs = Collections.emptyList();
+                        SearchRequest searchRequest = searchRequest(requestState, resourceId, attrs);
+                        return connection.searchSingleEntryAsync(searchRequest)
+                                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                                    @Override
+                                    public Promise<ResourceResponse, ResourceException> apply(
+                                            final SearchResultEntry entry) {
+                                        try {
+                                            // Fail-fast if there is a version mismatch.
+                                            ensureMVCCVersionMatches(entry, request.getRevision());
+
+                                            // Create the modify request.
+                                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
+                                            if (config.readOnUpdatePolicy() == CONTROLS) {
+                                                final String[] attributes =
+                                                        getLDAPAttributes(requestState, request.getFields());
+                                                modifyRequest.addControl(
+                                                        PostReadRequestControl.newControl(false, attributes));
+                                            }
+                                            if (config.usePermissiveModify()) {
+                                                modifyRequest.addControl(
+                                                        PermissiveModifyRequestControl.newControl(true));
+                                            }
+                                            addAssertionControl(modifyRequest, request.getRevision());
+
+                                            // Determine the set of changes that need to be performed.
+                                            return attributeMapper.update(
+                                                        requestState, new JsonPointer(), entry, request.getContent())
+                                                    .thenAsync(new AsyncFunction<
+                                                            List<Modification>, ResourceResponse, ResourceException>() {
+                                                        @Override
+                                                        public Promise<ResourceResponse, ResourceException> apply(
+                                                                List<Modification> modifications)
+                                                                throws ResourceException {
+                                                            if (modifications.isEmpty()) {
+                                                                // No changes to be performed so just return
+                                                                // the entry that we read.
+                                                                return adaptEntry(requestState, entry);
+                                                            }
+                                                            // Perform the modify operation.
+                                                            modifyRequest.getModifications().addAll(modifications);
+                                                            return connection.applyChangeAsync(modifyRequest).thenAsync(
+                                                                    postUpdateResultAsyncFunction(requestState),
+                                                                    ldapExceptionToResourceException());
+                                                        }
+                                                    });
+                                        } catch (final Exception e) {
+                                            return Promises.newExceptionPromise(asResourceException(e));
+                                        }
+                                    }
+                                }, ldapExceptionToResourceException());
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    private Promise<ResourceResponse, ResourceException> adaptEntry(
+            final RequestState requestState, final Entry entry) {
+        final String actualResourceId = nameStrategy.getResourceId(requestState, entry);
+        final String revision = getRevisionFromEntry(entry);
+        return attributeMapper.read(requestState, new JsonPointer(), entry)
+                              .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
+                                  @Override
+                                  public ResourceResponse apply(final JsonValue value) {
+                                      return Responses.newResourceResponse(
+                                              actualResourceId, revision, new JsonValue(value));
+                                  }
+                              });
+    }
+
+    private void addAssertionControl(final ChangeRecord request, final String expectedRevision)
+            throws ResourceException {
+        if (expectedRevision != null) {
+            ensureMVCCSupported();
+            request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
+                    etagAttribute.toString(), expectedRevision)));
+        }
+    }
+
+    private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction(
+            final RequestState requestState, final String resourceId, final String revision) {
+        return new AsyncFunction<Connection, DN, ResourceException>() {
+            @Override
+            public Promise<DN, ResourceException> apply(Connection connection) {
+                final String ldapAttribute =
+                        (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
+                final SearchRequest searchRequest =
+                        nameStrategy.createSearchRequest(requestState, getBaseDN(), resourceId)
+                                    .addAttribute(ldapAttribute);
+                if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
+                    // There's no point in doing a search because we already know the DN.
+                    return Promises.newResultPromise(searchRequest.getName());
+                }
+                return connection.searchSingleEntryAsync(searchRequest)
+                        .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
+                            @Override
+                            public Promise<DN, ResourceException> apply(SearchResultEntry entry)
+                                    throws ResourceException {
+                                try {
+                                    // Fail-fast if there is a version mismatch.
+                                    ensureMVCCVersionMatches(entry, revision);
+                                    // Perform update operation.
+                                    return Promises.newResultPromise(entry.getName());
+                                } catch (final Exception e) {
+                                    return Promises.newExceptionPromise(asResourceException(e));
+                                }
+                            }
+                        }, new AsyncFunction<LdapException, DN, ResourceException>() {
+                            @Override
+                            public Promise<DN, ResourceException> apply(LdapException ldapException)
+                                    throws ResourceException {
+                                return Promises.newExceptionPromise(asResourceException(ldapException));
+                            }
+                        });
+            }
+        };
+    }
+
+    private void ensureMVCCSupported() throws NotSupportedException {
+        if (etagAttribute == null) {
+            throw new NotSupportedException(
+                    i18n("Multi-version concurrency control is not supported by this resource"));
+        }
+    }
+
+    private void ensureMVCCVersionMatches(final Entry entry, final String expectedRevision) throws ResourceException {
+        if (expectedRevision != null) {
+            ensureMVCCSupported();
+            final String actualRevision = entry.parseAttribute(etagAttribute).asString();
+            if (actualRevision == null) {
+                throw new PreconditionFailedException(i18n(
+                        "The resource could not be accessed because it did not contain any "
+                                + "version information, when the version '%s' was expected", expectedRevision));
+            } else if (!expectedRevision.equals(actualRevision)) {
+                throw new PreconditionFailedException(i18n(
+                        "The resource could not be accessed because the expected version '%s' "
+                                + "does not match the current version '%s'", expectedRevision, actualRevision));
+            }
+        }
+    }
+
+    private DN getBaseDN() {
+        return baseDN;
+    }
+
+    /**
+     * Determines the set of LDAP attributes to request in an LDAP read (search,
+     * post-read), based on the provided list of JSON pointers.
+     *
+     * @param requestState
+     *          The request state.
+     * @param requestedAttributes
+     *          The list of resource attributes to be read.
+     * @return The set of LDAP attributes associated with the resource
+     *         attributes.
+     */
+    private String[] getLDAPAttributes(
+            final RequestState requestState, final Collection<JsonPointer> requestedAttributes) {
+        // Get all the LDAP attributes required by the attribute mappers.
+        final Set<String> requestedLDAPAttributes;
+        if (requestedAttributes.isEmpty()) {
+            // Full read.
+            requestedLDAPAttributes = new LinkedHashSet<>();
+            attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(),
+                    requestedLDAPAttributes);
+        } else {
+            // Partial read.
+            requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
+            for (final JsonPointer requestedAttribute : requestedAttributes) {
+                attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute,
+                        requestedLDAPAttributes);
+            }
+        }
+
+        // Get the LDAP attributes required by the Etag and name stategies.
+        nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes);
+        if (etagAttribute != null) {
+            requestedLDAPAttributes.add(etagAttribute.toString());
+        }
+        return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
+    }
+
+    private String getRevisionFromEntry(final Entry entry) {
+        return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
+    }
+
+    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
+            final RequestState requestState) {
+        // The handler which will be invoked for the LDAP add result.
+        return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
+            @Override
+            public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException {
+                // FIXME: handle USE_SEARCH policy.
+                Entry entry;
+                try {
+                    final PostReadResponseControl postReadControl =
+                        result.getControl(PostReadResponseControl.DECODER, config.decodeOptions());
+                    if (postReadControl != null) {
+                        entry = postReadControl.getEntry();
+                    } else {
+                        final PreReadResponseControl preReadControl =
+                            result.getControl(PreReadResponseControl.DECODER, config.decodeOptions());
+                        if (preReadControl != null) {
+                            entry = preReadControl.getEntry();
+                        } else {
+                            entry = null;
+                        }
+                    }
+                } catch (final DecodeException e) {
+                    // FIXME: log something?
+                    entry = null;
+                }
+                if (entry != null) {
+                    return adaptEntry(requestState, entry);
+                } else {
+                    return Promises.newResultPromise(
+                            Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() {
+        return ldapToResourceException();
+    }
+
+    private <R> AsyncFunction<LdapException, R, ResourceException> ldapToResourceException() {
+        // The handler which will be invoked for the LDAP add result.
+        return new AsyncFunction<LdapException, R, ResourceException>() {
+            @Override
+            public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException {
+                return Promises.newExceptionPromise(asResourceException(ldapException));
+            }
+        };
+    }
+
+    private SearchRequest searchRequest(
+            final RequestState requestState, final String resourceId, final List<JsonPointer> requestedAttributes) {
+        final String[] attributes = getLDAPAttributes(requestState, requestedAttributes);
+        return nameStrategy.createSearchRequest(requestState, getBaseDN(), resourceId).addAttribute(attributes);
+    }
+
+    private RequestState wrap(final Context context) {
+        return new RequestState(config, context);
+    }
+
+    private Runnable close(final RequestState requestState) {
+        return new Runnable() {
+            @Override
+            public void run() {
+                requestState.close();
+            }
+        };
+    }
+
+    private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) {
+        return new ResultHandler<Connection>() {
+            @Override
+            public void handleResult(Connection connection) {
+                connectionHolder.set(connection);
+            }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
new file mode 100644
index 0000000..69e764e
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
@@ -0,0 +1,99 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+import java.util.Set;
+
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+
+/**
+ * A name strategy is responsible for naming REST resources and LDAP entries.
+ */
+abstract class NameStrategy {
+    /*
+     * This interface is an abstract class so that methods can be made package
+     * private until API is finalized.
+     */
+
+    NameStrategy() {
+        // Nothing to do.
+    }
+
+    /**
+     * Returns a search request which can be used to obtain the specified REST
+     * resource.
+     *
+     * @param requestState
+     *            The request state.
+     * @param baseDN
+     *            The search base DN.
+     * @param resourceId
+     *            The resource ID.
+     * @return A search request which can be used to obtain the specified REST
+     *         resource.
+     */
+    abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId);
+
+    /**
+     * Adds the name of any LDAP attribute required by this name strategy to the
+     * provided set.
+     *
+     * @param requestState
+     *            The request state.
+     * @param ldapAttributes
+     *            The set into which any required LDAP attribute name should be
+     *            put.
+     */
+    abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes);
+
+    /**
+     * Retrieves the resource ID from the provided LDAP entry. Implementations
+     * may use the entry DN as well as any attributes in order to determine the
+     * resource ID.
+     *
+     * @param requestState
+     *            The request state.
+     * @param entry
+     *            The LDAP entry from which the resource ID should be obtained.
+     * @return The resource ID.
+     */
+    abstract String getResourceId(RequestState requestState, Entry entry);
+
+    /**
+     * Sets the resource ID in the provided LDAP entry. Implementations are
+     * responsible for setting the entry DN as well as any attributes associated
+     * with the resource ID.
+     *
+     * @param requestState
+     *            The request state.
+     * @param baseDN
+     *            The baseDN to use when constructing the entry's DN.
+     * @param resourceId
+     *            The resource ID.
+     * @param entry
+     *            The LDAP entry whose DN and resource ID attributes are to be
+     *            set.
+     * @throws ResourceException
+     *             If the resource ID cannot be determined.
+     */
+    abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry)
+            throws ResourceException;
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
new file mode 100644
index 0000000..03cc53a
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -0,0 +1,339 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.json.resource.PatchOperation.operation;
+import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+
+/** An attribute mapper which maps JSON objects to LDAP attributes. */
+public final class ObjectAttributeMapper extends AttributeMapper {
+
+    private static final class Mapping {
+        private final AttributeMapper mapper;
+        private final String name;
+
+        private Mapping(final String name, final AttributeMapper mapper) {
+            this.name = name;
+            this.mapper = mapper;
+        }
+
+        @Override
+        public String toString() {
+            return name + " -> " + mapper;
+        }
+    }
+
+    private final Map<String, Mapping> mappings = new LinkedHashMap<>();
+
+    ObjectAttributeMapper() {
+        // Nothing to do.
+    }
+
+    /**
+     * Creates a mapping for an attribute contained in the JSON object.
+     *
+     * @param name
+     *            The name of the JSON attribute to be mapped.
+     * @param mapper
+     *            The attribute mapper responsible for mapping the JSON
+     *            attribute to LDAP attribute(s).
+     * @return A reference to this attribute mapper.
+     */
+    public ObjectAttributeMapper attribute(final String name, final AttributeMapper mapper) {
+        mappings.put(toLowerCase(name), new Mapping(name, mapper));
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "object(" + mappings.values() + ")";
+    }
+
+    @Override
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
+        try {
+            /*
+             * First check that the JSON value is an object and that the fields
+             * it contains are known by this mapper.
+             */
+            final Map<String, Mapping> missingMappings = checkMapping(path, v);
+
+            // Accumulate the results of the subordinate mappings.
+            final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>();
+
+            // Invoke mappings for which there are values provided.
+            if (v != null && !v.isNull()) {
+                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+                    final Mapping mapping = getMapping(me.getKey());
+                    final JsonValue subValue = new JsonValue(me.getValue());
+                    promises.add(mapping.mapper.create(requestState, path.child(me.getKey()), subValue));
+                }
+            }
+
+            // Invoke mappings for which there were no values provided.
+            for (final Mapping mapping : missingMappings.values()) {
+                promises.add(mapping.mapper.create(requestState, path.child(mapping.name), null));
+            }
+
+            return Promises.when(promises)
+                           .then(this.<Attribute> accumulateResults());
+        } catch (final Exception e) {
+            return Promises.newExceptionPromise(asResourceException(e));
+        }
+    }
+
+    @Override
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
+            final Set<String> ldapAttributes) {
+        if (subPath.isEmpty()) {
+            // Request all subordinate mappings.
+            for (final Mapping mapping : mappings.values()) {
+                mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes);
+            }
+        } else {
+            // Request single subordinate mapping.
+            final Mapping mapping = getMapping(subPath);
+            if (mapping != null) {
+                mapping.mapper.getLDAPAttributes(
+                        requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
+            }
+        }
+    }
+
+    @Override
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
+        final Mapping mapping = getMapping(subPath);
+        if (mapping != null) {
+            return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)),
+                    subPath.relativePointer(), type, operator, valueAssertion);
+        } else {
+            /*
+             * Either the filter targeted the entire object (i.e. it was "/"),
+             * or it targeted an unrecognized attribute within the object.
+             * Either way, the filter will never match.
+             */
+            return Promises.newResultPromise(alwaysFalse());
+        }
+    }
+
+    @Override
+    Promise<List<Modification>, ResourceException> patch(
+            final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
+        try {
+            final JsonPointer field = operation.getField();
+            final JsonValue v = operation.getValue();
+
+            if (field.isEmpty()) {
+                /*
+                 * The patch operation applies to this object. We'll handle this
+                 * by allowing the JSON value to be a partial object and
+                 * add/remove/replace only the provided values.
+                 */
+                checkMapping(path, v);
+
+                // Accumulate the results of the subordinate mappings.
+                final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
+
+                // Invoke mappings for which there are values provided.
+                if (!v.isNull()) {
+                    for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+                        final Mapping mapping = getMapping(me.getKey());
+                        final JsonValue subValue = new JsonValue(me.getValue());
+                        final PatchOperation subOperation =
+                                operation(operation.getOperation(), field /* empty */, subValue);
+                        promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation));
+                    }
+                }
+
+                return Promises.when(promises)
+                               .then(this.<Modification> accumulateResults());
+            } else {
+                /*
+                 * The patch operation targets a subordinate field. Create a new
+                 * patch operation targeting the field and forward it to the
+                 * appropriate mapper.
+                 */
+                final String fieldName = field.get(0);
+                final Mapping mapping = getMapping(fieldName);
+                if (mapping == null) {
+                    throw new BadRequestException(i18n(
+                            "The request cannot be processed because it included "
+                                    + "an unrecognized field '%s'", path.child(fieldName)));
+                }
+                final PatchOperation subOperation =
+                        operation(operation.getOperation(), field.relativePointer(), v);
+                return mapping.mapper.patch(requestState, path.child(fieldName), subOperation);
+            }
+        } catch (final Exception ex) {
+            return Promises.newExceptionPromise(asResourceException(ex));
+        }
+    }
+
+    @Override
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
+        /*
+         * Use an accumulator which will aggregate the results from the
+         * subordinate mappers into a single list. On completion, the
+         * accumulator combines the results into a single JSON map object.
+         */
+        final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
+                new ArrayList<>(mappings.size());
+
+        for (final Mapping mapping : mappings.values()) {
+            promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e)
+                    .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
+                        @Override
+                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
+                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
+                                                 : null;
+                        }
+                    }));
+        }
+
+        return Promises.when(promises)
+                .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() {
+                    @Override
+                    public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
+                        if (value.isEmpty()) {
+                            /*
+                             * No subordinate attributes, so omit the entire
+                             * JSON object from the resource.
+                             */
+                            return null;
+                        } else {
+                            // Combine the sub-attributes into a single JSON object.
+                            final Map<String, Object> result = new LinkedHashMap<>(value.size());
+                            for (final Map.Entry<String, JsonValue> e : value) {
+                                if (e != null) {
+                                    result.put(e.getKey(), e.getValue().getObject());
+                                }
+                            }
+                            return new JsonValue(result);
+                        }
+                    }
+                });
+    }
+
+    @Override
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
+        try {
+            // First check that the JSON value is an object and that the fields
+            // it contains are known by this mapper.
+            final Map<String, Mapping> missingMappings = checkMapping(path, v);
+
+            // Accumulate the results of the subordinate mappings.
+            final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
+
+            // Invoke mappings for which there are values provided.
+            if (v != null && !v.isNull()) {
+                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+                    final Mapping mapping = getMapping(me.getKey());
+                    final JsonValue subValue = new JsonValue(me.getValue());
+                    promises.add(mapping.mapper.update(requestState, path.child(me.getKey()), e, subValue));
+                }
+            }
+
+            // Invoke mappings for which there were no values provided.
+            for (final Mapping mapping : missingMappings.values()) {
+                promises.add(mapping.mapper.update(requestState, path.child(mapping.name), e, null));
+            }
+
+            return Promises.when(promises)
+                           .then(this.<Modification> accumulateResults());
+        } catch (final Exception ex) {
+            return Promises.newExceptionPromise(asResourceException(ex));
+        }
+    }
+
+    private <T> Function<List<List<T>>, List<T>, ResourceException> accumulateResults() {
+        return new Function<List<List<T>>, List<T>, ResourceException>() {
+            @Override
+            public List<T> apply(final List<List<T>> value) {
+                switch (value.size()) {
+                case 0:
+                    return Collections.emptyList();
+                case 1:
+                    return value.get(0);
+                default:
+                    final List<T> attributes = new ArrayList<>(value.size());
+                    for (final List<T> a : value) {
+                        attributes.addAll(a);
+                    }
+                    return attributes;
+                }
+            }
+        };
+    }
+
+    /** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */
+    private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
+            throws ResourceException {
+        final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
+        if (v != null && !v.isNull()) {
+            if (v.isMap()) {
+                for (final String attribute : v.asMap().keySet()) {
+                    if (missingMappings.remove(toLowerCase(attribute)) == null) {
+                        throw new BadRequestException(i18n(
+                                "The request cannot be processed because it included "
+                                        + "an unrecognized field '%s'", path.child(attribute)));
+                    }
+                }
+            } else {
+                throw new BadRequestException(i18n(
+                        "The request cannot be processed because it included "
+                                + "the field '%s' whose value is the wrong type: "
+                                + "an object is expected", path));
+            }
+        }
+        return missingMappings;
+    }
+
+    private Mapping getMapping(final JsonPointer jsonAttribute) {
+        return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0));
+    }
+
+    private Mapping getMapping(final String jsonAttribute) {
+        return mappings.get(toLowerCase(jsonAttribute));
+    }
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnUpdatePolicy.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnUpdatePolicy.java
new file mode 100644
index 0000000..16ec961
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnUpdatePolicy.java
@@ -0,0 +1,48 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+/**
+ * The policy which should be used in order to read an entry before it is
+ * deleted, or after it is added or modified.
+ */
+public enum ReadOnUpdatePolicy {
+    /**
+     * The LDAP entry will not be read when an update is performed. More
+     * specifically, the REST resource will not be returned as part of a create,
+     * delete, patch, or update request.
+     */
+    DISABLED,
+
+    /**
+     * The LDAP entry will be read atomically using the RFC 4527 read-entry
+     * controls. More specifically, the REST resource will be returned as part
+     * of a create, delete, patch, or update request, and it will reflect the
+     * state of the resource at the time the update was performed. This policy
+     * requires that the LDAP server supports RFC 4527.
+     */
+    CONTROLS,
+
+    /**
+     * The LDAP entry will be read non-atomically using an LDAP search when an
+     * update is performed. More specifically, the REST resource will be
+     * returned as part of a create, delete, patch, or update request, but it
+     * may not reflect the state of the resource at the time the update was
+     * performed.
+     */
+    SEARCH;
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
new file mode 100644
index 0000000..cf0fd1c
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -0,0 +1,372 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
+
+/**
+ * An attribute mapper which provides a mapping from a JSON value to a single DN
+ * valued LDAP attribute.
+ */
+public final class ReferenceAttributeMapper extends AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
+    /**
+     * The maximum number of candidate references to allow in search filters.
+     */
+    private static final int SEARCH_MAX_CANDIDATES = 1000;
+
+    private final DN baseDN;
+    private Filter filter;
+    private final AttributeMapper mapper;
+    private final AttributeDescription primaryKey;
+    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
+
+    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
+        final AttributeDescription primaryKey, final AttributeMapper mapper) {
+        super(ldapAttributeName);
+        this.baseDN = baseDN;
+        this.primaryKey = primaryKey;
+        this.mapper = mapper;
+    }
+
+    /**
+     * Sets the filter which should be used when searching for referenced LDAP
+     * entries. The default is {@code (objectClass=*)}.
+     *
+     * @param filter
+     *            The filter which should be used when searching for referenced
+     *            LDAP entries.
+     * @return This attribute mapper.
+     */
+    public ReferenceAttributeMapper searchFilter(final Filter filter) {
+        this.filter = ensureNotNull(filter);
+        return this;
+    }
+
+    /**
+     * Sets the filter which should be used when searching for referenced LDAP
+     * entries. The default is {@code (objectClass=*)}.
+     *
+     * @param filter
+     *            The filter which should be used when searching for referenced
+     *            LDAP entries.
+     * @return This attribute mapper.
+     */
+    public ReferenceAttributeMapper searchFilter(final String filter) {
+        return searchFilter(Filter.valueOf(filter));
+    }
+
+    /**
+     * Sets the search scope which should be used when searching for referenced
+     * LDAP entries. The default is {@link SearchScope#WHOLE_SUBTREE}.
+     *
+     * @param scope
+     *            The search scope which should be used when searching for
+     *            referenced LDAP entries.
+     * @return This attribute mapper.
+     */
+    public ReferenceAttributeMapper searchScope(final SearchScope scope) {
+        this.scope = ensureNotNull(scope);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "reference(" + ldapAttributeName + ")";
+    }
+
+    @Override
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
+
+        return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion)
+                .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
+                    @Override
+                    public Promise<Filter, ResourceException> apply(final Filter result) {
+                        // Search for all referenced entries and construct a filter.
+                        final SearchRequest request = createSearchRequest(result);
+                        final List<Filter> subFilters = new LinkedList<>();
+
+                        return requestState.getConnection().thenAsync(
+                                new AsyncFunction<Connection, Filter, ResourceException>() {
+                                    @Override
+                                    public Promise<Filter, ResourceException> apply(final Connection connection)
+                                            throws ResourceException {
+                                        return connection.searchAsync(request, new SearchResultHandler() {
+                                            @Override
+                                            public boolean handleEntry(final SearchResultEntry entry) {
+                                                if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
+                                                    subFilters.add(Filter.equality(
+                                                            ldapAttributeName.toString(), entry.getName()));
+                                                    return true;
+                                                } else {
+                                                    // No point in continuing - maximum candidates reached.
+                                                    return false;
+                                                }
+                                            }
+
+                                            @Override
+                                            public boolean handleReference(final SearchResultReference reference) {
+                                                // Ignore references.
+                                                return true;
+                                            }
+                                        }).then(new Function<Result, Filter, ResourceException>() {
+                                            @Override
+                                            public Filter apply(Result result) throws ResourceException {
+                                                if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
+                                                    throw asResourceException(
+                                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
+                                                } else if (subFilters.size() == 1) {
+                                                    return subFilters.get(0);
+                                                } else {
+                                                    return Filter.or(subFilters);
+                                                }
+                                            }
+                                        }, new Function<LdapException, Filter, ResourceException>() {
+                                            @Override
+                                            public Filter apply(LdapException exception) throws ResourceException {
+                                                throw asResourceException(exception);
+                                            }
+                                        });
+                                    }
+                                });
+                    }
+                });
+    }
+
+    @Override
+    Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
+        /*
+         * For each value use the subordinate mapper to obtain the LDAP primary
+         * key, the perform a search for each one to find the corresponding entries.
+         */
+        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
+        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
+        final AtomicReference<ResourceException> exception = new AtomicReference<>();
+        final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create();
+
+        for (final Object value : newValues) {
+            mapper.create(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
+                @Override
+                public void handleResult(List<Attribute> result) {
+                    Attribute primaryKeyAttribute = null;
+                    for (final Attribute attribute : result) {
+                        if (attribute.getAttributeDescription().equals(primaryKey)) {
+                            primaryKeyAttribute = attribute;
+                            break;
+                        }
+                    }
+
+                    if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
+                        promise.handleException(new BadRequestException(
+                                i18n("The request cannot be processed because the reference field '%s' contains "
+                                        + "a value which does not contain a primary key", path)));
+                    }
+
+                    if (primaryKeyAttribute.size() > 1) {
+                        promise.handleException(new BadRequestException(
+                                i18n("The request cannot be processed because the reference field '%s' contains "
+                                        + "a value which contains multiple primary keys", path)));
+                    }
+
+                    // Now search for the referenced entry in to get its DN.
+                    final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
+                    final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
+                    final SearchRequest search = createSearchRequest(filter);
+                    requestState.getConnection().thenOnResult(new ResultHandler<Connection>() {
+                        @Override
+                        public void handleResult(Connection connection) {
+                            connection.searchSingleEntryAsync(search)
+                                      .thenOnResult(new ResultHandler<SearchResultEntry>() {
+                                          @Override
+                                          public void handleResult(final SearchResultEntry result) {
+                                              synchronized (newLDAPAttribute) {
+                                                  newLDAPAttribute.add(result.getName());
+                                              }
+                                              completeIfNecessary();
+                                          }
+                                      }).thenOnException(new ExceptionHandler<LdapException>() {
+                                          @Override
+                                          public void handleException(final LdapException error) {
+                                              ResourceException re;
+                                              try {
+                                                  throw error;
+                                              } catch (final EntryNotFoundException e) {
+                                                  re = new BadRequestException(i18n(
+                                                          "The request cannot be processed because the resource "
+                                                          + "'%s' referenced in field '%s' does not exist",
+                                                          primaryKeyValue.toString(), path));
+                                              } catch (final MultipleEntriesFoundException e) {
+                                                  re = new BadRequestException(i18n(
+                                                          "The request cannot be processed because the resource "
+                                                          + "'%s' referenced in field '%s' is ambiguous",
+                                                          primaryKeyValue.toString(), path));
+                                              } catch (final LdapException e) {
+                                                  re = asResourceException(e);
+                                              }
+                                              exception.compareAndSet(null, re);
+                                              completeIfNecessary();
+                                          }
+                                      });
+                        }
+                    });
+                }
+
+                private void completeIfNecessary() {
+                    if (pendingSearches.decrementAndGet() == 0) {
+                        if (exception.get() != null) {
+                            promise.handleException(exception.get());
+                        } else {
+                            promise.handleResult(newLDAPAttribute);
+                        }
+                    }
+                }
+            });
+        }
+        return promise;
+    }
+
+    @Override
+    ReferenceAttributeMapper getThis() {
+        return this;
+    }
+
+    @Override
+    Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) {
+        final Attribute attribute = e.getAttribute(ldapAttributeName);
+        if (attribute == null || attribute.isEmpty()) {
+            return Promises.newResultPromise(null);
+        } else if (attributeIsSingleValued()) {
+            try {
+                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
+                return readEntry(c, path, dn);
+            } catch (final Exception ex) {
+                // The LDAP attribute could not be decoded.
+                return Promises.newExceptionPromise(asResourceException(ex));
+            }
+        } else {
+            try {
+                final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
+
+                final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
+                for (final DN dn : dns) {
+                    promises.add(readEntry(c, path, dn));
+                }
+
+                return Promises.when(promises)
+                               .then(new Function<List<JsonValue>, JsonValue, ResourceException>() {
+                                   @Override
+                                   public JsonValue apply(final List<JsonValue> value) {
+                                       if (value.isEmpty()) {
+                                           // No values, so omit the entire JSON object from the resource.
+                                           return null;
+                                       } else {
+                                           // Combine values into a single JSON array.
+                                           final List<Object> result = new ArrayList<>(value.size());
+                                           for (final JsonValue e : value) {
+                                               if (e != null) {
+                                                   result.add(e.getObject());
+                                               }
+                                           }
+                                           return result.isEmpty() ? null : new JsonValue(result);
+                                       }
+                                   }
+                               });
+            } catch (final Exception ex) {
+                // The LDAP attribute could not be decoded.
+                return Promises.newExceptionPromise(asResourceException(ex));
+            }
+        }
+    }
+
+    private SearchRequest createSearchRequest(final Filter result) {
+        final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
+        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
+    }
+
+    private Promise<JsonValue, ResourceException> readEntry(
+            final RequestState requestState, final JsonPointer path, final DN dn) {
+        final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
+        mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes);
+        return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() {
+            @Override
+            public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException {
+                final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue();
+                final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
+                final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT, searchFilter, attributes);
+
+                return connection.searchSingleEntryAsync(request)
+                        .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
+                            @Override
+                            public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
+                                return mapper.read(requestState, path, result);
+                            }
+                        }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
+                            @Override
+                            public Promise<JsonValue, ResourceException> apply(final LdapException error) {
+                                if (error instanceof EntryNotFoundException) {
+                                    // Ignore missing entry since it cannot be mapped.
+                                    return Promises.newResultPromise(null);
+                                }
+                                return Promises.newExceptionPromise(asResourceException(error));
+                            }
+                        });
+            }
+        });
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
new file mode 100644
index 0000000..486d6f6
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
@@ -0,0 +1,435 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import java.io.Closeable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+
+import org.forgerock.json.resource.InternalServerErrorException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.SecurityContext;
+import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
+
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
+import static org.forgerock.opendj.rest2ldap.Utils.*;
+
+/**
+ * Common request information passed to containers and mappers.
+ * A new @{code RequestState} is allocated for each REST request.
+ */
+final class RequestState implements Closeable {
+
+    /** A cached read request - see cachedReads for more information. */
+    private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> {
+        private SearchResultEntry cachedEntry;
+        private final String cachedFilterString;
+        /** Guarded by cachedPromiseLatch. */
+        private LdapPromise<Result> cachedPromise;
+        private final CountDownLatch cachedPromiseLatch = new CountDownLatch(1);
+        private final SearchRequest cachedRequest;
+        private volatile Result cachedResult;
+        private final ConcurrentLinkedQueue<SearchResultHandler> waitingResultHandlers = new ConcurrentLinkedQueue<>();
+
+        CachedRead(final SearchRequest request, final SearchResultHandler resultHandler) {
+            this.cachedRequest = request;
+            this.cachedFilterString = request.getFilter().toString();
+            this.waitingResultHandlers.add(resultHandler);
+        }
+
+        @Override
+        public boolean handleEntry(final SearchResultEntry entry) {
+            cachedEntry = entry;
+            return true;
+        }
+
+        @Override
+        public void handleException(final LdapException exception) {
+            handleResult(exception.getResult());
+        }
+
+        @Override
+        public boolean handleReference(final SearchResultReference reference) {
+            // Ignore - should never happen for a base object search.
+            return true;
+        }
+
+        @Override
+        public void handleResult(final Result result) {
+            cachedResult = result;
+            drainQueue();
+        }
+
+        void addResultHandler(final SearchResultHandler resultHandler) {
+            // Fast path.
+            if (cachedResult != null) {
+                invokeResultHandler(resultHandler);
+                return;
+            }
+            // Enqueue and re-check.
+            waitingResultHandlers.add(resultHandler);
+            if (cachedResult != null) {
+                drainQueue();
+            }
+        }
+
+        LdapPromise<Result> getPromise() {
+            // Perform uninterrupted wait since this method is unlikely to block for a long time.
+            boolean wasInterrupted = false;
+            while (true) {
+                try {
+                    cachedPromiseLatch.await();
+                    if (wasInterrupted) {
+                        Thread.currentThread().interrupt();
+                    }
+                    return cachedPromise;
+                } catch (final InterruptedException e) {
+                    wasInterrupted = true;
+                }
+            }
+        }
+
+        boolean isMatchingRead(final SearchRequest request) {
+            // Cached reads are always base object.
+            return request.getScope().equals(SearchScope.BASE_OBJECT)
+                    // Filters must match.
+                    && request.getFilter().toString().equals(cachedFilterString)
+                    // List of requested attributes must match.
+                    && request.getAttributes().equals(cachedRequest.getAttributes());
+        }
+
+        void setPromise(final LdapPromise<Result> promise) {
+            cachedPromise = promise;
+            cachedPromiseLatch.countDown();
+        }
+
+        private void drainQueue() {
+            SearchResultHandler resultHandler;
+            while ((resultHandler = waitingResultHandlers.poll()) != null) {
+                invokeResultHandler(resultHandler);
+            }
+        }
+
+        private void invokeResultHandler(final SearchResultHandler searchResultHandler) {
+            if (cachedEntry != null) {
+                searchResultHandler.handleEntry(cachedEntry);
+            }
+        }
+    }
+
+    /**
+     * An LRU cache of recent reads requests. This is used in order to reduce
+     * the number of repeated read operations performed when resolving DN
+     * references.
+     */
+    @SuppressWarnings("serial")
+    private final Map<DN, CachedRead> cachedReads = new LinkedHashMap<DN, CachedRead>() {
+        private static final int MAX_CACHED_ENTRIES = 32;
+
+        @Override
+        protected boolean removeEldestEntry(final Map.Entry<DN, CachedRead> eldest) {
+            return size() > MAX_CACHED_ENTRIES;
+        }
+    };
+
+    private final Config config;
+    private final Context context;
+    private Connection connection;
+    private Control proxiedAuthzControl;
+
+    RequestState(final Config config, final Context context) {
+        this.config = config;
+        this.context = context;
+
+        /* Re-use the pre-authenticated connection if available and the authorization policy allows. */
+        if (config.getAuthorizationPolicy() != AuthorizationPolicy.NONE
+                && context.containsContext(AuthenticatedConnectionContext.class)) {
+            this.connection = wrap(context.asContext(AuthenticatedConnectionContext.class).getConnection());
+        } else {
+            this.connection = null; // We'll allocate the connection.
+        }
+    }
+
+    @Override
+    public void close() {
+        connection.close();
+    }
+
+    Config getConfig() {
+        return config;
+    }
+
+    Context getContext() {
+        return context;
+    }
+
+    /**
+     * Performs common processing required before handling an HTTP request,
+     * including calculating the proxied authorization request control. Then
+     * return a promise containing a valid LDAP connection or a
+     * {@link ResourceException} if an error is detected.
+     * <p>
+     * This method should be called at most once per request.
+     * @return A {@link Promise} containing a valid {@link Connection}
+     */
+    Promise<Connection, ResourceException> getConnection() {
+        /*
+         * Compute the proxied authorization control from the content of the
+         * security context if present. Only do this if we are not using a
+         * cached connection since cached connections are supposed to have been
+         * pre-authenticated and therefore do not require proxied authorization.
+         */
+        if (connection == null && config.getAuthorizationPolicy() == AuthorizationPolicy.PROXY) {
+            if (context.containsContext(SecurityContext.class)) {
+                try {
+                    final SecurityContext securityContext = context.asContext(SecurityContext.class);
+                    final String authzId = config.getProxiedAuthorizationTemplate().formatAsAuthzId(
+                            securityContext.getAuthorization(), config.schema());
+                    proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId);
+                } catch (final ResourceException e) {
+                    return Promises.newExceptionPromise(e);
+                }
+            } else {
+                return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
+                        i18n("The request could not be authorized because it did not contain a security context")));
+            }
+        }
+
+        /*
+         * Now get the LDAP connection to use for processing subsequent LDAP
+         * requests. A null factory indicates that Rest2LDAP has been configured
+         * to re-use the LDAP connection which was used for authentication.
+         */
+        if (connection != null) {
+            return Promises.newResultPromise(connection);
+        } else if (config.connectionFactory() != null) {
+            final PromiseImpl<Connection, ResourceException> promise = PromiseImpl.create();
+            config.connectionFactory().getConnectionAsync().thenOnResult(new ResultHandler<Connection>() {
+                @Override
+                public final void handleResult(final Connection result) {
+                    connection = wrap(result);
+                    promise.handleResult(connection);
+                }
+            }).thenOnException(new ExceptionHandler<LdapException>() {
+                @Override
+                public final void handleException(final LdapException exception) {
+                    promise.handleException(asResourceException(exception));
+                }
+            });
+            return promise;
+        } else {
+            return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
+                    i18n("The request could not be processed because there was no LDAP connection available for use")));
+        }
+    }
+
+    /**
+     * Adds read caching support to the provided connection as well
+     * functionality which automatically adds the proxied authorization control
+     * if needed.
+     */
+    private Connection wrap(final Connection connection) {
+        /* We only use async methods so no need to wrap sync methods. */
+        return new AbstractAsynchronousConnection() {
+            @Override
+            public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+                return connection.abandonAsync(request);
+            }
+
+            @Override
+            public LdapPromise<Result> addAsync(final AddRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                return connection.addAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public void addConnectionEventListener(final ConnectionEventListener listener) {
+                connection.addConnectionEventListener(listener);
+            }
+
+            @Override
+            public LdapPromise<BindResult> bindAsync(final BindRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                /*
+                 * Simple brute force implementation in case the bind operation
+                 * modifies an entry: clear the cachedReads.
+                 */
+                evictAll();
+                return connection.bindAsync(request, intermediateResponseHandler);
+            }
+
+            @Override
+            public void close() {
+                connection.close();
+            }
+
+            @Override
+            public void close(final UnbindRequest request, final String reason) {
+                connection.close(request, reason);
+            }
+
+            @Override
+            public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                return connection.compareAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public LdapPromise<Result> deleteAsync(final DeleteRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                evict(request.getName());
+                return connection.deleteAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                /*
+                 * Simple brute force implementation in case the extended
+                 * operation modifies an entry: clear the cachedReads.
+                 */
+                evictAll();
+                return connection.extendedRequestAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public boolean isClosed() {
+                return connection.isClosed();
+            }
+
+            @Override
+            public boolean isValid() {
+                return connection.isValid();
+            }
+
+            @Override
+            public LdapPromise<Result> modifyAsync(final ModifyRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                evict(request.getName());
+                return connection.modifyAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler) {
+                // Simple brute force implementation: clear the cachedReads.
+                evictAll();
+                return connection.modifyDNAsync(withControls(request), intermediateResponseHandler);
+            }
+
+            @Override
+            public void removeConnectionEventListener(final ConnectionEventListener listener) {
+                connection.removeConnectionEventListener(listener);
+            }
+
+            /** Try and re-use a cached result if possible. */
+            @Override
+            public LdapPromise<Result> searchAsync(final SearchRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
+                /*
+                 * Don't attempt caching if this search is not a read (base
+                 * object), or if the search request passed in an intermediate
+                 * response handler.
+                 */
+                if (!request.getScope().equals(SearchScope.BASE_OBJECT) || intermediateResponseHandler != null) {
+                    return connection.searchAsync(withControls(request), intermediateResponseHandler, entryHandler);
+                }
+
+                // This is a read request and a candidate for caching.
+                final CachedRead cachedRead;
+                synchronized (cachedReads) {
+                    cachedRead = cachedReads.get(request.getName());
+                }
+                if (cachedRead != null && cachedRead.isMatchingRead(request)) {
+                    // The cached read matches this read request.
+                    cachedRead.addResultHandler(entryHandler);
+                    return cachedRead.getPromise();
+                } else {
+                    // Cache the read, possibly evicting a non-matching cached read.
+                    final CachedRead pendingCachedRead = new CachedRead(request, entryHandler);
+                    synchronized (cachedReads) {
+                        cachedReads.put(request.getName(), pendingCachedRead);
+                    }
+                    final LdapPromise<Result> promise = connection
+                            .searchAsync(withControls(request), intermediateResponseHandler, pendingCachedRead)
+                            .thenOnResult(pendingCachedRead).thenOnException(pendingCachedRead);
+                    pendingCachedRead.setPromise(promise);
+                    return promise;
+                }
+            }
+
+            @Override
+            public String toString() {
+                return connection.toString();
+            }
+
+            private void evict(final DN name) {
+                synchronized (cachedReads) {
+                    cachedReads.remove(name);
+                }
+            }
+
+            private void evictAll() {
+                synchronized (cachedReads) {
+                    cachedReads.clear();
+                }
+            }
+
+            private <R extends Request> R withControls(final R request) {
+                if (proxiedAuthzControl != null) {
+                    request.addControl(proxiedAuthzControl);
+                }
+                return request;
+            }
+        };
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
new file mode 100644
index 0000000..8782d36
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -0,0 +1,1126 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Arrays.asList;
+import static org.forgerock.json.resource.ResourceException.newResourceException;
+import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
+import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
+import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType;
+import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
+import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.AssertionFailureException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AuthenticationException;
+import org.forgerock.opendj.ldap.AuthorizationException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConnectionException;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.ConstraintViolationException;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LDAPConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
+import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SSLContextBuilder;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TimeoutResultException;
+import org.forgerock.opendj.ldap.TrustManagers;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.Options;
+import org.forgerock.util.time.Duration;
+
+/** Provides core factory methods and builders for constructing LDAP resource collections. */
+public final class Rest2LDAP {
+    /** Indicates whether or not LDAP client connections should use SSL or StartTLS. */
+    private enum ConnectionSecurity {
+        NONE, SSL, STARTTLS
+    }
+
+    /**
+     * Specifies the mechanism which should be used for trusting certificates
+     * presented by the LDAP server.
+     */
+    private enum TrustManagerType {
+        TRUSTALL, JVM, FILE
+    }
+
+    /** A builder for incrementally constructing LDAP resource collections. */
+    public static final class Builder {
+        private final List<Attribute> additionalLDAPAttributes = new LinkedList<>();
+        private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE;
+        private DN baseDN; // TODO: support template variables.
+        private AttributeDescription etagAttribute;
+        private ConnectionFactory factory;
+        private NameStrategy nameStrategy;
+        private AuthzIdTemplate proxiedAuthzTemplate;
+        private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
+        private AttributeMapper rootMapper;
+        private Schema schema = Schema.getDefaultSchema();
+        private boolean usePermissiveModify;
+        private boolean useSubtreeDelete;
+
+        private Builder() {
+            useEtagAttribute();
+            useClientDNNaming("uid");
+        }
+
+        /**
+         * Specifies an additional LDAP attribute which should be included with
+         * new LDAP entries when they are created. Use this method to specify
+         * the LDAP objectClass attribute.
+         *
+         * @param attribute
+         *            The additional LDAP attribute to be included with new LDAP
+         *            entries.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder additionalLDAPAttribute(final Attribute attribute) {
+            additionalLDAPAttributes.add(attribute);
+            return this;
+        }
+
+        /**
+         * Specifies an additional LDAP attribute which should be included with
+         * new LDAP entries when they are created. Use this method to specify
+         * the LDAP objectClass attribute.
+         *
+         * @param attribute
+         *            The name of the additional LDAP attribute to be included
+         *            with new LDAP entries.
+         * @param values
+         *            The value(s) of the additional LDAP attribute.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder additionalLDAPAttribute(final String attribute, final Object... values) {
+            return additionalLDAPAttribute(new LinkedAttribute(ad(attribute), values));
+        }
+
+        /**
+         * Sets the policy which should be for performing authorization.
+         *
+         * @param policy
+         *            The policy which should be for performing authorization.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder authorizationPolicy(final AuthorizationPolicy policy) {
+            this.authzPolicy = ensureNotNull(policy);
+            return this;
+        }
+
+        /**
+         * Sets the base DN beneath which LDAP entries (resources) are to be found.
+         *
+         * @param dn
+         *            The base DN.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder baseDN(final DN dn) {
+            ensureNotNull(dn);
+            this.baseDN = dn;
+            return this;
+        }
+
+        /**
+         * Sets the base DN beneath which LDAP entries (resources) are to be found.
+         *
+         * @param dn
+         *            The base DN.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder baseDN(final String dn) {
+            return baseDN(DN.valueOf(dn, schema));
+        }
+
+        /**
+         * Creates a new LDAP resource collection configured using this builder.
+         *
+         * @return The new LDAP resource collection.
+         */
+        public CollectionResourceProvider build() {
+            ensureNotNull(baseDN);
+            if (rootMapper == null) {
+                throw new IllegalStateException("No mappings provided");
+            }
+            switch (authzPolicy) {
+            case NONE:
+                if (factory == null) {
+                    throw new IllegalStateException(
+                            "A connection factory must be specified when the authorization policy is 'none'");
+                }
+                break;
+            case PROXY:
+                if (proxiedAuthzTemplate == null) {
+                    throw new IllegalStateException(
+                            "Proxied authorization enabled but no template defined");
+                }
+                if (factory == null) {
+                    throw new IllegalStateException(
+                            "A connection factory must be specified when using proxied authorization");
+                }
+                break;
+            case REUSE:
+                // This is always ok.
+                break;
+            }
+            return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy,
+                    etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy,
+                            proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema),
+                    additionalLDAPAttributes);
+        }
+
+        /**
+         * Configures the JSON to LDAP mapping using the provided JSON
+         * configuration. The caller is still required to set the connection
+         * factory. See the sample configuration file for a detailed description
+         * of its content.
+         *
+         * @param configuration
+         *            The JSON configuration.
+         * @return A reference to this LDAP resource collection builder.
+         * @throws IllegalArgumentException
+         *             If the configuration is invalid.
+         */
+        public Builder configureMapping(final JsonValue configuration) {
+            baseDN(configuration.get("baseDN").required().asString());
+
+            final JsonValue readOnUpdatePolicy = configuration.get("readOnUpdatePolicy");
+            if (!readOnUpdatePolicy.isNull()) {
+                readOnUpdatePolicy(readOnUpdatePolicy.asEnum(ReadOnUpdatePolicy.class));
+            }
+
+            for (final JsonValue v : configuration.get("additionalLDAPAttributes")) {
+                final String type = v.get("type").required().asString();
+                final List<Object> values = v.get("values").required().asList();
+                additionalLDAPAttribute(new LinkedAttribute(type, values));
+            }
+
+            final JsonValue namingStrategy = configuration.get("namingStrategy");
+            if (!namingStrategy.isNull()) {
+                final String name = namingStrategy.get("strategy").required().asString();
+                if (name.equalsIgnoreCase("clientDNNaming")) {
+                    useClientDNNaming(namingStrategy.get("dnAttribute").required().asString());
+                } else if (name.equalsIgnoreCase("clientNaming")) {
+                    useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
+                            namingStrategy.get("idAttribute").required().asString());
+                } else if (name.equalsIgnoreCase("serverNaming")) {
+                    useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
+                            namingStrategy.get("idAttribute").required().asString());
+                } else {
+                    throw new IllegalArgumentException(
+                            "Illegal naming strategy. Must be one of: clientDNNaming, clientNaming, or serverNaming");
+                }
+            }
+
+            final JsonValue etagAttribute = configuration.get("etagAttribute");
+            if (!etagAttribute.isNull()) {
+                useEtagAttribute(etagAttribute.asString());
+            }
+
+            /*
+             * Default to false, even though it is supported by OpenDJ, because
+             * it requires additional permissions.
+             */
+            if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) {
+                useSubtreeDelete();
+            }
+
+            /*
+             * Default to true because it is supported by OpenDJ and does not
+             * require additional permissions.
+             */
+            if (configuration.get("usePermissiveModify").defaultTo(true).asBoolean()) {
+                usePermissiveModify();
+            }
+
+            mapper(configureObjectMapper(configuration.get("attributes").required()));
+
+            return this;
+        }
+
+        /**
+         * Sets the LDAP connection factory to be used for accessing the LDAP
+         * directory. Each HTTP request will obtain a single connection from the
+         * factory and then close it once the HTTP response has been sent. It is
+         * recommended that the provided connection factory supports connection
+         * pooling.
+         *
+         * @param factory
+         *            The LDAP connection factory to be used for accessing the
+         *            LDAP directory.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder ldapConnectionFactory(final ConnectionFactory factory) {
+            this.factory = factory;
+            return this;
+        }
+
+        /**
+         * Sets the attribute mapper which should be used for mapping JSON
+         * resources to and from LDAP entries.
+         *
+         * @param mapper
+         *            The attribute mapper.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder mapper(final AttributeMapper mapper) {
+            this.rootMapper = mapper;
+            return this;
+        }
+
+        /**
+         * Sets the authorization ID template which will be used for proxied
+         * authorization. Template parameters are specified by including the
+         * parameter name surrounded by curly braces. The template should
+         * contain fields which are expected to be found in the security context
+         * create during authentication, e.g. "dn:{dn}" or "u:{id}".
+         *
+         * @param template
+         *            The authorization ID template which will be used for
+         *            proxied authorization.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder proxyAuthzIdTemplate(final String template) {
+            this.proxiedAuthzTemplate = template != null ? new AuthzIdTemplate(template) : null;
+            return this;
+        }
+
+        /**
+         * Sets the policy which should be used in order to read an entry before
+         * it is deleted, or after it is added or modified. The default read on
+         * update policy is to use {@link ReadOnUpdatePolicy#CONTROLS controls}.
+         *
+         * @param policy
+         *            The policy which should be used in order to read an entry
+         *            before it is deleted, or after it is added or modified.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) {
+            this.readOnUpdatePolicy = ensureNotNull(policy);
+            return this;
+        }
+
+        /**
+         * Sets the schema which should be used when attribute types and
+         * controls.
+         *
+         * @param schema
+         *            The schema which should be used when attribute types and
+         *            controls.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder schema(final Schema schema) {
+            this.schema = ensureNotNull(schema);
+            return this;
+        }
+
+        /**
+         * Indicates that the JSON resource ID must be provided by the user, and
+         * will be used for naming the associated LDAP entry. More specifically,
+         * LDAP entry names will be derived by appending a single RDN to the
+         * {@link #baseDN(String) base DN} composed of the specified attribute
+         * type and LDAP value taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the user provides the
+         * resource name when creating new resources, which means it must be
+         * included in the resource content when not specified explicitly in the
+         * create request.
+         *
+         * @param attribute
+         *            The LDAP attribute which will be used for naming.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useClientDNNaming(final AttributeType attribute) {
+            this.nameStrategy = new DNNameStrategy(attribute);
+            return this;
+        }
+
+        /**
+         * Indicates that the JSON resource ID must be provided by the user, and
+         * will be used for naming the associated LDAP entry. More specifically,
+         * LDAP entry names will be derived by appending a single RDN to the
+         * {@link #baseDN(String) base DN} composed of the specified attribute
+         * type and LDAP value taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the user provides the
+         * resource name when creating new resources, which means it must be
+         * included in the resource content when not specified explicitly in the
+         * create request.
+         *
+         * @param attribute
+         *            The LDAP attribute which will be used for naming.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useClientDNNaming(final String attribute) {
+            return useClientDNNaming(at(attribute));
+        }
+
+        /**
+         * Indicates that the JSON resource ID must be provided by the user, but
+         * will not be used for naming the associated LDAP entry. Instead the
+         * JSON resource ID will be taken from the {@code idAttribute} in the
+         * LDAP entry, and the LDAP entry name will be derived by appending a
+         * single RDN to the {@link #baseDN(String) base DN} composed of the
+         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the user provides the
+         * resource name when creating new resources, which means it must be
+         * included in the resource content when not specified explicitly in the
+         * create request.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @param idAttribute
+         *            The attribute which will be used for JSON resource IDs.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useClientNaming(final AttributeType dnAttribute,
+                final AttributeDescription idAttribute) {
+            this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, false);
+            return this;
+        }
+
+        /**
+         * Indicates that the JSON resource ID must be provided by the user, but
+         * will not be used for naming the associated LDAP entry. Instead the
+         * JSON resource ID will be taken from the {@code idAttribute} in the
+         * LDAP entry, and the LDAP entry name will be derived by appending a
+         * single RDN to the {@link #baseDN(String) base DN} composed of the
+         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the user provides the
+         * resource name when creating new resources, which means it must be
+         * included in the resource content when not specified explicitly in the
+         * create request.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @param idAttribute
+         *            The attribute which will be used for JSON resource IDs.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useClientNaming(final String dnAttribute, final String idAttribute) {
+            return useClientNaming(at(dnAttribute), ad(idAttribute));
+        }
+
+        /**
+         * Indicates that the "etag" LDAP attribute should be used for resource
+         * versioning. This is the default behavior.
+         *
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useEtagAttribute() {
+            return useEtagAttribute("etag");
+        }
+
+        /**
+         * Indicates that the provided LDAP attribute should be used for
+         * resource versioning. The "etag" attribute will be used by default.
+         *
+         * @param attribute
+         *            The name of the attribute to use for versioning, or
+         *            {@code null} if resource versioning will not supported.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useEtagAttribute(final AttributeDescription attribute) {
+            this.etagAttribute = attribute;
+            return this;
+        }
+
+        /**
+         * Indicates that the provided LDAP attribute should be used for
+         * resource versioning. The "etag" attribute will be used by default.
+         *
+         * @param attribute
+         *            The name of the attribute to use for versioning, or
+         *            {@code null} if resource versioning will not supported.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useEtagAttribute(final String attribute) {
+            return useEtagAttribute(attribute != null ? ad(attribute) : null);
+        }
+
+        /**
+         * Indicates that all LDAP modify operations should be performed using
+         * the LDAP permissive modify control. The default behavior is to not
+         * use the permissive modify control. Use of the control is strongly
+         * recommended.
+         *
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder usePermissiveModify() {
+            this.usePermissiveModify = true;
+            return this;
+        }
+
+        /**
+         * Indicates that the JSON resource ID will be derived from the server
+         * provided "entryUUID" LDAP attribute. The LDAP entry name will be
+         * derived by appending a single RDN to the {@link #baseDN(String) base
+         * DN} composed of the {@code dnAttribute} taken from the LDAP entry
+         * once attribute mapping has been performed.
+         * <p>
+         * Note that this naming policy requires that the server provides the
+         * resource name when creating new resources, which means it must not be
+         * specified in the create request, nor included in the resource
+         * content.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) {
+            return useServerNaming(dnAttribute, AttributeDescription
+                    .create(getEntryUUIDAttributeType()));
+        }
+
+        /**
+         * Indicates that the JSON resource ID will be derived from the server
+         * provided "entryUUID" LDAP attribute. The LDAP entry name will be
+         * derived by appending a single RDN to the {@link #baseDN(String) base
+         * DN} composed of the {@code dnAttribute} taken from the LDAP entry
+         * once attribute mapping has been performed.
+         * <p>
+         * Note that this naming policy requires that the server provides the
+         * resource name when creating new resources, which means it must not be
+         * specified in the create request, nor included in the resource
+         * content.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useServerEntryUUIDNaming(final String dnAttribute) {
+            return useServerEntryUUIDNaming(at(dnAttribute));
+        }
+
+        /**
+         * Indicates that the JSON resource ID must not be provided by the user,
+         * and will not be used for naming the associated LDAP entry. Instead
+         * the JSON resource ID will be taken from the {@code idAttribute} in
+         * the LDAP entry, and the LDAP entry name will be derived by appending
+         * a single RDN to the {@link #baseDN(String) base DN} composed of the
+         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the server provides the
+         * resource name when creating new resources, which means it must not be
+         * specified in the create request, nor included in the resource
+         * content.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @param idAttribute
+         *            The attribute which will be used for JSON resource IDs.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useServerNaming(final AttributeType dnAttribute,
+                final AttributeDescription idAttribute) {
+            this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, true);
+            return this;
+        }
+
+        /**
+         * Indicates that the JSON resource ID must not be provided by the user,
+         * and will not be used for naming the associated LDAP entry. Instead
+         * the JSON resource ID will be taken from the {@code idAttribute} in
+         * the LDAP entry, and the LDAP entry name will be derived by appending
+         * a single RDN to the {@link #baseDN(String) base DN} composed of the
+         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
+         * has been performed.
+         * <p>
+         * Note that this naming policy requires that the server provides the
+         * resource name when creating new resources, which means it must not be
+         * specified in the create request, nor included in the resource
+         * content.
+         *
+         * @param dnAttribute
+         *            The attribute which will be used for naming LDAP entries.
+         * @param idAttribute
+         *            The attribute which will be used for JSON resource IDs.
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useServerNaming(final String dnAttribute, final String idAttribute) {
+            return useServerNaming(at(dnAttribute), ad(idAttribute));
+        }
+
+        /**
+         * Indicates that all LDAP delete operations should be performed using
+         * the LDAP subtree delete control. The default behavior is to not use
+         * the subtree delete control.
+         *
+         * @return A reference to this LDAP resource collection builder.
+         */
+        public Builder useSubtreeDelete() {
+            this.useSubtreeDelete = true;
+            return this;
+        }
+
+        private AttributeDescription ad(final String attribute) {
+            return AttributeDescription.valueOf(attribute, schema);
+        }
+
+        private AttributeType at(final String attribute) {
+            return schema.getAttributeType(attribute);
+        }
+
+        private AttributeMapper configureMapper(final JsonValue mapper) {
+            if (mapper.isDefined("constant")) {
+                return constant(mapper.get("constant").getObject());
+            } else if (mapper.isDefined("simple")) {
+                final JsonValue config = mapper.get("simple");
+                final SimpleAttributeMapper s =
+                        simple(ad(config.get("ldapAttribute").required().asString()));
+                if (config.isDefined("defaultJSONValue")) {
+                    s.defaultJSONValue(config.get("defaultJSONValue").getObject());
+                }
+                if (config.get("isBinary").defaultTo(false).asBoolean()) {
+                    s.isBinary();
+                }
+                if (config.get("isRequired").defaultTo(false).asBoolean()) {
+                    s.isRequired();
+                }
+                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
+                    s.isSingleValued();
+                }
+                s.writability(parseWritability(mapper, config));
+                return s;
+            } else if (mapper.isDefined("reference")) {
+                final JsonValue config = mapper.get("reference");
+                final AttributeDescription ldapAttribute =
+                        ad(config.get("ldapAttribute").required().asString());
+                final DN baseDN = DN.valueOf(config.get("baseDN").required().asString(), schema);
+                final AttributeDescription primaryKey =
+                        ad(config.get("primaryKey").required().asString());
+                final AttributeMapper m = configureMapper(config.get("mapper").required());
+                final ReferenceAttributeMapper r = reference(ldapAttribute, baseDN, primaryKey, m);
+                if (config.get("isRequired").defaultTo(false).asBoolean()) {
+                    r.isRequired();
+                }
+                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
+                    r.isSingleValued();
+                }
+                if (config.isDefined("searchFilter")) {
+                    r.searchFilter(config.get("searchFilter").asString());
+                }
+                r.writability(parseWritability(mapper, config));
+                return r;
+            } else if (mapper.isDefined("object")) {
+                return configureObjectMapper(mapper.get("object"));
+            } else {
+                throw new JsonValueException(mapper,
+                        "Illegal mapping: must contain constant, simple, or object");
+            }
+        }
+
+        private ObjectAttributeMapper configureObjectMapper(final JsonValue mapper) {
+            final ObjectAttributeMapper object = object();
+            for (final String attribute : mapper.keys()) {
+                object.attribute(attribute, configureMapper(mapper.get(attribute)));
+            }
+            return object;
+        }
+
+        private WritabilityPolicy parseWritability(final JsonValue mapper, final JsonValue config) {
+            if (config.isDefined("writability")) {
+                final String writability = config.get("writability").asString();
+                if (writability.equalsIgnoreCase("readOnly")) {
+                    return WritabilityPolicy.READ_ONLY;
+                } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) {
+                    return WritabilityPolicy.READ_ONLY_DISCARD_WRITES;
+                } else if (writability.equalsIgnoreCase("createOnly")) {
+                    return WritabilityPolicy.CREATE_ONLY;
+                } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) {
+                    return WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES;
+                } else if (writability.equalsIgnoreCase("readWrite")) {
+                    return WritabilityPolicy.READ_WRITE;
+                } else {
+                    throw new JsonValueException(mapper,
+                            "Illegal writability: must be one of readOnly, readOnlyDiscardWrites, "
+                                    + "createOnly, createOnlyDiscardWrites, or readWrite");
+                }
+            } else {
+                return WritabilityPolicy.READ_WRITE;
+            }
+        }
+    }
+
+    private static final class AttributeNameStrategy extends NameStrategy {
+        private final AttributeDescription dnAttribute;
+        private final AttributeDescription idAttribute;
+        private final boolean isServerProvided;
+
+        private AttributeNameStrategy(final AttributeType dnAttribute,
+                final AttributeDescription idAttribute, final boolean isServerProvided) {
+            this.dnAttribute = AttributeDescription.create(dnAttribute);
+            if (this.dnAttribute.equals(idAttribute)) {
+                throw new IllegalArgumentException("DN and ID attributes must be different");
+            }
+            this.idAttribute = ensureNotNull(idAttribute);
+            this.isServerProvided = isServerProvided;
+        }
+
+        @Override
+        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
+            return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute
+                    .toString(), resourceId));
+        }
+
+        @Override
+        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
+            ldapAttributes.add(idAttribute.toString());
+        }
+
+        @Override
+        String getResourceId(final RequestState requestState, final Entry entry) {
+            return entry.parseAttribute(idAttribute).asString();
+        }
+
+        @Override
+        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
+                final Entry entry) throws ResourceException {
+            if (isServerProvided) {
+                if (resourceId != null) {
+                    throw new BadRequestException("Resources cannot be created with a "
+                            + "client provided resource ID");
+                }
+            } else {
+                entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(resourceId)));
+            }
+            final String rdnValue = entry.parseAttribute(dnAttribute).asString();
+            final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue);
+            entry.setName(baseDN.child(rdn));
+        }
+    }
+
+    private static final class DNNameStrategy extends NameStrategy {
+        private final AttributeDescription attribute;
+
+        private DNNameStrategy(final AttributeType attribute) {
+            this.attribute = AttributeDescription.create(attribute);
+        }
+
+        @Override
+        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
+            return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter
+                    .objectClassPresent());
+        }
+
+        @Override
+        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
+            ldapAttributes.add(attribute.toString());
+        }
+
+        @Override
+        String getResourceId(final RequestState requestState, final Entry entry) {
+            return entry.parseAttribute(attribute).asString();
+        }
+
+        @Override
+        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
+                final Entry entry) throws ResourceException {
+            if (resourceId != null) {
+                entry.setName(baseDN.child(rdn(resourceId)));
+                entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOfUtf8(resourceId)));
+            } else if (entry.getAttribute(attribute) != null) {
+                entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString())));
+            } else {
+                throw new BadRequestException("Resources cannot be created without a "
+                        + "client provided resource ID");
+            }
+        }
+
+        private RDN rdn(final String resourceId) {
+            return new RDN(attribute.getAttributeType(), resourceId);
+        }
+    }
+
+    /**
+     * Adapts a {@code Throwable} to a {@code ResourceException}. If the
+     * {@code Throwable} is an LDAP {@link LdapException} then an
+     * appropriate {@code ResourceException} is returned, otherwise an
+     * {@code InternalServerErrorException} is returned.
+     *
+     * @param t
+     *            The {@code Throwable} to be converted.
+     * @return The equivalent resource exception.
+     */
+    public static ResourceException asResourceException(final Throwable t) {
+        int resourceResultCode;
+        try {
+            throw t;
+        } catch (final ResourceException e) {
+            return e;
+        } catch (final AssertionFailureException e) {
+            resourceResultCode = ResourceException.VERSION_MISMATCH;
+        } catch (final ConstraintViolationException e) {
+            final ResultCode rc = e.getResult().getResultCode();
+            if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) {
+                resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC.
+            } else {
+                // Schema violation, etc.
+                resourceResultCode = ResourceException.BAD_REQUEST;
+            }
+        } catch (final AuthenticationException e) {
+            resourceResultCode = 401;
+        } catch (final AuthorizationException e) {
+            resourceResultCode = ResourceException.FORBIDDEN;
+        } catch (final ConnectionException e) {
+            resourceResultCode = ResourceException.UNAVAILABLE;
+        } catch (final EntryNotFoundException e) {
+            resourceResultCode = ResourceException.NOT_FOUND;
+        } catch (final MultipleEntriesFoundException e) {
+            resourceResultCode = ResourceException.INTERNAL_ERROR;
+        } catch (final TimeoutResultException e) {
+            resourceResultCode = 408;
+        } catch (final LdapException e) {
+            final ResultCode rc = e.getResult().getResultCode();
+            if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) {
+                resourceResultCode = 413; // Request Entity Too Large
+            } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
+                resourceResultCode = 413; // Request Entity Too Large
+            } else {
+                resourceResultCode = ResourceException.INTERNAL_ERROR;
+            }
+        } catch (final Throwable tmp) {
+            resourceResultCode = ResourceException.INTERNAL_ERROR;
+        }
+        return newResourceException(resourceResultCode, t.getMessage(), t);
+    }
+
+    /**
+     * Returns a builder for incrementally constructing LDAP resource
+     * collections.
+     *
+     * @return An LDAP resource collection builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Creates a new connection factory using the named configuration in the
+     * provided JSON list of factory configurations. See the sample
+     * configuration file for a detailed description of its content.
+     *
+     * @param configuration
+     *            The JSON configuration.
+     * @param name
+     *            The name of the connection factory configuration to be parsed.
+     * @param providerClassLoader
+     *            The {@link ClassLoader} used to fetch the
+     *            {@link org.forgerock.opendj.ldap.spi.TransportProvider}.
+     *            This can be useful in OSGI environments.
+     * @return A new connection factory using the provided JSON configuration.
+     * @throws IllegalArgumentException
+     *             If the configuration is invalid.
+     */
+    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
+                                                               final String name,
+                                                               final ClassLoader providerClassLoader) {
+        final JsonValue normalizedConfiguration =
+                normalizeConnectionFactory(configuration, name, 0);
+        return configureConnectionFactory(normalizedConfiguration, providerClassLoader);
+    }
+
+    /**
+     * Creates a new connection factory using the named configuration in the
+     * provided JSON list of factory configurations. See the sample
+     * configuration file for a detailed description of its content.
+     *
+     * @param configuration
+     *            The JSON configuration.
+     * @param name
+     *            The name of the connection factory configuration to be parsed.
+     * @return A new connection factory using the provided JSON configuration.
+     * @throws IllegalArgumentException
+     *             If the configuration is invalid.
+     */
+    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
+            final String name) {
+        return configureConnectionFactory(configuration, name, null);
+    }
+
+    /**
+     * Returns an attribute mapper which maps a single JSON attribute to a JSON
+     * constant.
+     *
+     * @param value
+     *            The constant JSON value (a Boolean, Number, String, Map, or
+     *            List).
+     * @return The attribute mapper.
+     */
+    public static AttributeMapper constant(final Object value) {
+        return new JSONConstantAttributeMapper(value);
+    }
+
+    /**
+     * Returns an attribute mapper which maps JSON objects to LDAP attributes.
+     *
+     * @return The attribute mapper.
+     */
+    public static ObjectAttributeMapper object() {
+        return new ObjectAttributeMapper();
+    }
+
+    /**
+     * Returns an attribute mapper which provides a mapping from a JSON value to
+     * a single DN valued LDAP attribute.
+     *
+     * @param attribute
+     *            The DN valued LDAP attribute to be mapped.
+     * @param baseDN
+     *            The search base DN for performing reverse lookups.
+     * @param primaryKey
+     *            The search primary key LDAP attribute to use for performing
+     *            reverse lookups.
+     * @param mapper
+     *            An attribute mapper which will be used to map LDAP attributes
+     *            in the referenced entry.
+     * @return The attribute mapper.
+     */
+    public static ReferenceAttributeMapper reference(final AttributeDescription attribute,
+            final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) {
+        return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper);
+    }
+
+    /**
+     * Returns an attribute mapper which provides a mapping from a JSON value to
+     * a single DN valued LDAP attribute.
+     *
+     * @param attribute
+     *            The DN valued LDAP attribute to be mapped.
+     * @param baseDN
+     *            The search base DN for performing reverse lookups.
+     * @param primaryKey
+     *            The search primary key LDAP attribute to use for performing
+     *            reverse lookups.
+     * @param mapper
+     *            An attribute mapper which will be used to map LDAP attributes
+     *            in the referenced entry.
+     * @return The attribute mapper.
+     */
+    public static ReferenceAttributeMapper reference(final String attribute, final String baseDN,
+            final String primaryKey, final AttributeMapper mapper) {
+        return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN),
+                AttributeDescription.valueOf(primaryKey), mapper);
+    }
+
+    /**
+     * Returns an attribute mapper which provides a simple mapping from a JSON
+     * value to a single LDAP attribute.
+     *
+     * @param attribute
+     *            The LDAP attribute to be mapped.
+     * @return The attribute mapper.
+     */
+    public static SimpleAttributeMapper simple(final AttributeDescription attribute) {
+        return new SimpleAttributeMapper(attribute);
+    }
+
+    /**
+     * Returns an attribute mapper which provides a simple mapping from a JSON
+     * value to a single LDAP attribute.
+     *
+     * @param attribute
+     *            The LDAP attribute to be mapped.
+     * @return The attribute mapper.
+     */
+    public static SimpleAttributeMapper simple(final String attribute) {
+        return simple(AttributeDescription.valueOf(attribute));
+    }
+
+    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
+                                                                final ClassLoader providerClassLoader) {
+        final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
+        final Duration heartBeatInterval = new Duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
+
+        final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong();
+        final Duration heartBeatTimeout = new Duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS);
+
+        final Options options = Options.defaultOptions()
+                                       .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader)
+                                       .set(HEARTBEAT_ENABLED, true)
+                                       .set(HEARTBEAT_INTERVAL, heartBeatInterval)
+                                       .set(HEARTBEAT_TIMEOUT, heartBeatTimeout)
+                                       .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval);
+
+        // Parse pool parameters,
+        final int connectionPoolSize =
+                Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
+
+        // Parse authentication parameters.
+        if (configuration.isDefined("authentication")) {
+            final JsonValue authn = configuration.get("authentication");
+            if (authn.isDefined("simple")) {
+                final JsonValue simple = authn.get("simple");
+                final BindRequest bindRequest =
+                        Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(),
+                                simple.get("bindPassword").required().asString().toCharArray());
+                options.set(AUTHN_BIND_REQUEST, bindRequest);
+            } else {
+                throw new IllegalArgumentException("Only simple authentication is supported");
+            }
+        }
+
+        // Parse SSL/StartTLS parameters.
+        final ConnectionSecurity connectionSecurity =
+                configuration.get("connectionSecurity").defaultTo(ConnectionSecurity.NONE).asEnum(
+                        ConnectionSecurity.class);
+        if (connectionSecurity != ConnectionSecurity.NONE) {
+            try {
+                // Configure SSL.
+                final SSLContextBuilder builder = new SSLContextBuilder();
+
+                // Parse trust store configuration.
+                final TrustManagerType trustManagerType =
+                        configuration.get("trustManager").defaultTo(TrustManagerType.TRUSTALL)
+                                .asEnum(TrustManagerType.class);
+                switch (trustManagerType) {
+                case TRUSTALL:
+                    builder.setTrustManager(TrustManagers.trustAll());
+                    break;
+                case JVM:
+                    // Do nothing: JVM trust manager is the default.
+                    break;
+                case FILE:
+                    final String fileName =
+                            configuration.get("fileBasedTrustManagerFile").required().asString();
+                    final String password =
+                            configuration.get("fileBasedTrustManagerPassword").asString();
+                    final String type = configuration.get("fileBasedTrustManagerType").asString();
+                    builder.setTrustManager(TrustManagers.checkUsingTrustStore(fileName,
+                            password != null ? password.toCharArray() : null, type));
+                    break;
+                }
+                options.set(SSL_CONTEXT, builder.getSSLContext());
+                options.set(SSL_USE_STARTTLS,
+                            connectionSecurity == ConnectionSecurity.STARTTLS);
+            } catch (GeneralSecurityException | IOException e) {
+                // Rethrow as unchecked exception.
+                throw new IllegalArgumentException(e);
+            }
+        }
+
+        // Parse primary data center.
+        final JsonValue primaryLDAPServers = configuration.get("primaryLDAPServers");
+        if (!primaryLDAPServers.isList() || primaryLDAPServers.size() == 0) {
+            throw new IllegalArgumentException("No primaryLDAPServers");
+        }
+        final ConnectionFactory primary = parseLDAPServers(primaryLDAPServers, connectionPoolSize, options);
+
+        // Parse secondary data center(s).
+        final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers");
+        ConnectionFactory secondary = null;
+        if (secondaryLDAPServers.isList()) {
+            if (secondaryLDAPServers.size() > 0) {
+                secondary = parseLDAPServers(secondaryLDAPServers, connectionPoolSize, options);
+            }
+        } else if (!secondaryLDAPServers.isNull()) {
+            throw new IllegalArgumentException("Invalid secondaryLDAPServers configuration");
+        }
+
+        // Create fail-over.
+        if (secondary != null) {
+            return newFailoverLoadBalancer(asList(primary, secondary), options);
+        } else {
+            return primary;
+        }
+    }
+
+    private static JsonValue normalizeConnectionFactory(final JsonValue configuration,
+            final String name, final int depth) {
+        // Protect against infinite recursion in the configuration.
+        if (depth > 100) {
+            throw new IllegalArgumentException(
+                    "The LDAP server configuration '"
+                            + name
+                            + "' could not be parsed because of potential circular inheritance dependencies");
+        }
+
+        final JsonValue current = configuration.get(name).required();
+        if (current.isDefined("inheritFrom")) {
+            // Inherit missing fields from inherited configuration.
+            final JsonValue parent =
+                    normalizeConnectionFactory(configuration,
+                            current.get("inheritFrom").asString(), depth + 1);
+            final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap());
+            normalized.putAll(current.asMap());
+            normalized.remove("inheritFrom");
+            return new JsonValue(normalized);
+        } else {
+            // No normalization required.
+            return current;
+        }
+    }
+
+    private static ConnectionFactory parseLDAPServers(JsonValue config, int poolSize, Options options) {
+        final List<ConnectionFactory> servers = new ArrayList<>(config.size());
+        for (final JsonValue server : config) {
+            final String host = server.get("hostname").required().asString();
+            final int port = server.get("port").required().asInteger();
+            final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
+            if (poolSize > 1) {
+                servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS));
+            } else {
+                servers.add(factory);
+            }
+        }
+        if (servers.size() > 1) {
+            return newRoundRobinLoadBalancer(servers, options);
+        } else {
+            return servers.get(0);
+        }
+    }
+
+    private Rest2LDAP() {
+        // Prevent instantiation.
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
new file mode 100644
index 0000000..54a8b25
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -0,0 +1,167 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.http.util.Json.*;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
+import static org.forgerock.util.Utils.*;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.forgerock.http.Handler;
+import org.forgerock.http.HttpApplication;
+import org.forgerock.http.HttpApplicationException;
+import org.forgerock.http.handler.Handlers;
+import org.forgerock.http.io.Buffer;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.RequestHandler;
+import org.forgerock.json.resource.Router;
+import org.forgerock.json.resource.http.CrestHttp;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.Factory;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Rest2ldap HTTP application. */
+public final class Rest2LDAPHttpApplication implements HttpApplication {
+    private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
+
+    private static final class HttpHandler implements Handler, Closeable {
+        private final ConnectionFactory ldapConnectionFactory;
+        private final Handler delegate;
+
+        HttpHandler(final JsonValue configuration) {
+            ldapConnectionFactory = createLdapConnectionFactory(configuration);
+            try {
+                delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory));
+            } catch (final RuntimeException e) {
+                closeSilently(ldapConnectionFactory);
+                throw e;
+            }
+        }
+
+        private static RequestHandler createRouter(
+                final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) {
+            final AuthorizationPolicy authzPolicy = configuration.get("servlet")
+                    .get("authorizationPolicy")
+                    .required()
+                    .asEnum(AuthorizationPolicy.class);
+            final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
+            final JsonValue mappings = configuration.get("servlet").get("mappings").required();
+
+            final Router router = new Router();
+            for (final String mappingUrl : mappings.keys()) {
+                final JsonValue mapping = mappings.get(mappingUrl);
+                final CollectionResourceProvider provider = Rest2LDAP.builder()
+                        .ldapConnectionFactory(ldapConnectionFactory)
+                        .authorizationPolicy(authzPolicy)
+                        .proxyAuthzIdTemplate(proxyAuthzTemplate)
+                        .configureMapping(mapping)
+                        .build();
+                router.addRoute(Router.uriTemplate(mappingUrl), provider);
+            }
+            return router;
+        }
+
+        private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) {
+            final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString();
+            if (ldapFactoryName != null) {
+                return configureConnectionFactory(
+                        configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+            }
+            return null;
+        }
+
+        @Override
+        public void close() {
+            closeSilently(ldapConnectionFactory);
+        }
+
+        @Override
+        public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
+            return delegate.handle(context, request);
+        }
+    }
+
+    private final URL configurationUrl;
+    private HttpHandler handler;
+    private HttpAuthenticationFilter filter;
+
+    /**
+     * Default constructor called by the HTTP Framework which will use the
+     * default configuration file location.
+     */
+    public Rest2LDAPHttpApplication() {
+        this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
+    }
+
+    /**
+     * Creates a new Rest2LDAP HTTP application using the provided configuration URL.
+     *
+     * @param configurationURL
+     *            The URL to the JSON configuration file.
+     */
+    public Rest2LDAPHttpApplication(final URL configurationURL) {
+        Reject.ifNull(configurationURL, "The configuration URL must not be null");
+        this.configurationUrl = configurationURL;
+    }
+
+    private static JsonValue readJson(final URL resource) throws IOException {
+        try (InputStream in = resource.openStream()) {
+            return new JsonValue(readJsonLenient(in));
+        }
+    }
+
+    @Override
+    public Handler start() throws HttpApplicationException {
+        try {
+            final JsonValue configuration = readJson(configurationUrl);
+            handler = new HttpHandler(configuration);
+            filter = new HttpAuthenticationFilter(configuration);
+            return Handlers.chainOf(handler, filter);
+        } catch (final Exception e) {
+            // TODO i18n, once supported in opendj-rest2ldap
+            final String errorMsg = "Unable to start Rest2Ldap Http Application";
+            LOG.error(errorMsg, e);
+            stop();
+            throw new HttpApplicationException(errorMsg, e);
+        }
+    }
+
+    @Override
+    public Factory<Buffer> getBufferFactory() {
+        // Use container default buffer factory.
+        return null;
+    }
+
+    @Override
+    public void stop() {
+        closeSilently(handler, filter);
+        handler = null;
+        filter = null;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
new file mode 100644
index 0000000..0035d55
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -0,0 +1,182 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+
+import static java.util.Collections.*;
+
+import static org.forgerock.opendj.ldap.Filter.*;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
+import static org.forgerock.opendj.rest2ldap.Utils.*;
+import static org.forgerock.util.promise.Promises.newExceptionPromise;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+
+/**
+ * An attribute mapper which provides a simple mapping from a JSON value to a
+ * single LDAP attribute.
+ */
+public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> {
+    private Function<ByteString, ?, NeverThrowsException> decoder;
+    private Function<Object, ByteString, NeverThrowsException> encoder;
+
+    SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
+        super(ldapAttributeName);
+    }
+
+    /**
+     * Sets the decoder which will be used for converting LDAP attribute values
+     * to JSON values.
+     *
+     * @param f
+     *            The function to use for decoding LDAP attribute values.
+     * @return This attribute mapper.
+     */
+    public SimpleAttributeMapper decoder(final Function<ByteString, ?, NeverThrowsException> f) {
+        this.decoder = f;
+        return this;
+    }
+
+    /**
+     * Sets the default JSON value which should be substituted when the LDAP
+     * attribute is not found in the LDAP entry.
+     *
+     * @param defaultValue
+     *            The default JSON value.
+     * @return This attribute mapper.
+     */
+    public SimpleAttributeMapper defaultJSONValue(final Object defaultValue) {
+        this.defaultJSONValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
+        return this;
+    }
+
+    /**
+     * Sets the encoder which will be used for converting JSON values to LDAP
+     * attribute values.
+     *
+     * @param f
+     *            The function to use for encoding LDAP attribute values.
+     * @return This attribute mapper.
+     */
+    public SimpleAttributeMapper encoder(final Function<Object, ByteString, NeverThrowsException> f) {
+        this.encoder = f;
+        return this;
+    }
+
+    /**
+     * Indicates that JSON values are base 64 encodings of binary data. Calling
+     * this method is equivalent to the following:
+     *
+     * <pre>
+     * mapper.decoder(...); // function that converts binary data to base 64
+     * mapper.encoder(...); // function that converts base 64 to binary data
+     * </pre>
+     *
+     * @return This attribute mapper.
+     */
+    public SimpleAttributeMapper isBinary() {
+        decoder = byteStringToBase64();
+        encoder = base64ToByteString();
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "simple(" + ldapAttributeName + ")";
+    }
+
+    @Override
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
+        if (subPath.isEmpty()) {
+            try {
+                final ByteString va =
+                        valueAssertion != null ? encoder().apply(valueAssertion) : null;
+                return newResultPromise(toFilter(type, ldapAttributeName.toString(), va));
+            } catch (final Exception e) {
+                // Invalid assertion value - bad request.
+                return newExceptionPromise((ResourceException) new BadRequestException(i18n(
+                        "The request cannot be processed because it contained an "
+                                + "illegal filter assertion value '%s' for field '%s'",
+                        String.valueOf(valueAssertion), path), e));
+            }
+        } else {
+            // This attribute mapper does not support partial filtering.
+            return newResultPromise(alwaysFalse());
+        }
+    }
+
+    @Override
+    Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
+        try {
+            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
+        } catch (final Exception ex) {
+            return newExceptionPromise((ResourceException) new BadRequestException(i18n(
+                    "The request cannot be processed because an error occurred while "
+                            + "encoding the values for the field '%s': %s", path, ex.getMessage())));
+        }
+    }
+
+    @Override
+    SimpleAttributeMapper getThis() {
+        return this;
+    }
+
+    @Override
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
+        try {
+            final Object value;
+            if (attributeIsSingleValued()) {
+                value =
+                        e.parseAttribute(ldapAttributeName).as(decoder(),
+                                defaultJSONValues.isEmpty() ? null : defaultJSONValues.get(0));
+            } else {
+                final Set<Object> s =
+                        e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJSONValues);
+                value = s.isEmpty() ? null : new ArrayList<>(s);
+            }
+            return newResultPromise(value != null ? new JsonValue(value) : null);
+        } catch (final Exception ex) {
+            // The LDAP attribute could not be decoded.
+            return newExceptionPromise(asResourceException(ex));
+        }
+    }
+
+    private Function<ByteString, ?, NeverThrowsException> decoder() {
+        return decoder == null ? byteStringToJson(ldapAttributeName) : decoder;
+    }
+
+    private Function<Object, ByteString, NeverThrowsException> encoder() {
+        return encoder == null ? jsonToByteString(ldapAttributeName) : encoder;
+    }
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
new file mode 100644
index 0000000..e810f21
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -0,0 +1,223 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static javax.xml.bind.DatatypeConverter.parseDateTime;
+import static javax.xml.bind.DatatypeConverter.printDateTime;
+import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
+import static org.forgerock.opendj.ldap.Functions.byteStringToBoolean;
+import static org.forgerock.opendj.ldap.Functions.byteStringToGeneralizedTime;
+import static org.forgerock.opendj.ldap.Functions.byteStringToLong;
+import static org.forgerock.opendj.ldap.Functions.byteStringToString;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
+import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import org.forgerock.json.JsonValue;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.GeneralizedTime;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.schema.Syntax;
+import org.forgerock.util.Function;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/**
+ * Internal utility methods.
+ */
+final class Utils {
+
+    private static final Function<Object, ByteString, NeverThrowsException> BASE64_TO_BYTESTRING =
+            new Function<Object, ByteString, NeverThrowsException>() {
+                @Override
+                public ByteString apply(final Object value) {
+                    return ByteString.valueOfBase64(String.valueOf(value));
+                }
+            };
+
+    private static final Function<ByteString, String, NeverThrowsException> BYTESTRING_TO_BASE64 =
+            new Function<ByteString, String, NeverThrowsException>() {
+                @Override
+                public String apply(final ByteString value) {
+                    return value.toBase64String();
+                }
+            };
+
+
+    static Object attributeToJson(final Attribute a) {
+        final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription());
+        final boolean isSingleValued = a.getAttributeDescription().getAttributeType().isSingleValue();
+        return isSingleValued ? a.parse().as(f) : asList(a.parse().asSetOf(f));
+    }
+
+    static Function<Object, ByteString, NeverThrowsException> base64ToByteString() {
+        return BASE64_TO_BYTESTRING;
+    }
+
+    static Function<ByteString, String, NeverThrowsException> byteStringToBase64() {
+        return BYTESTRING_TO_BASE64;
+    }
+
+    static Function<ByteString, Object, NeverThrowsException> byteStringToJson(final AttributeDescription ad) {
+        return new Function<ByteString, Object, NeverThrowsException>() {
+            @Override
+            public Object apply(final ByteString value) {
+                final Syntax syntax = ad.getAttributeType().getSyntax();
+                if (syntax.equals(getBooleanSyntax())) {
+                    return byteStringToBoolean().apply(value);
+                } else if (syntax.equals(getIntegerSyntax())) {
+                    return byteStringToLong().apply(value);
+                } else if (syntax.equals(getGeneralizedTimeSyntax())) {
+                    return printDateTime(byteStringToGeneralizedTime().apply(value)
+                            .toCalendar());
+                } else {
+                    return byteStringToString().apply(value);
+                }
+            }
+        };
+    }
+
+    static <T> T ensureNotNull(final T object) {
+        if (object == null) {
+            throw new NullPointerException();
+        }
+        return object;
+    }
+
+    static <T> T ensureNotNull(final T object, final String message) {
+        if (object == null) {
+            throw new NullPointerException(message);
+        }
+        return object;
+    }
+
+    static String getAttributeName(final Attribute a) {
+        return a.getAttributeDescription().withoutOption("binary").toString();
+    }
+
+    /**
+     * Stub formatter for i18n strings.
+     *
+     * @param format
+     *            The format string.
+     * @param args
+     *            The string arguments.
+     * @return The formatted string.
+     */
+    static String i18n(final String format, final Object... args) {
+        return String.format(format, args);
+    }
+
+    static boolean isJSONPrimitive(final Object value) {
+        return value instanceof String || value instanceof Boolean || value instanceof Number;
+    }
+
+    static boolean isNullOrEmpty(final JsonValue v) {
+        return v == null || v.isNull() || (v.isList() && v.size() == 0);
+    }
+
+    static Attribute jsonToAttribute(final Object value, final AttributeDescription ad) {
+        return jsonToAttribute(value, ad, jsonToByteString(ad));
+    }
+
+    static Attribute jsonToAttribute(final Object value, final AttributeDescription ad,
+            final Function<Object, ByteString, NeverThrowsException> f) {
+        if (isJSONPrimitive(value)) {
+            return new LinkedAttribute(ad, f.apply(value));
+        } else if (value instanceof Collection<?>) {
+            final Attribute a = new LinkedAttribute(ad);
+            for (final Object o : (Collection<?>) value) {
+                a.add(f.apply(o));
+            }
+            return a;
+        } else {
+            throw new IllegalArgumentException("Unrecognized type of JSON value: "
+                    + value.getClass().getName());
+        }
+    }
+
+    static Function<Object, ByteString, NeverThrowsException> jsonToByteString(final AttributeDescription ad) {
+        return new Function<Object, ByteString, NeverThrowsException>() {
+            @Override
+            public ByteString apply(final Object value) {
+                if (isJSONPrimitive(value)) {
+                    final Syntax syntax = ad.getAttributeType().getSyntax();
+                    if (syntax.equals(getGeneralizedTimeSyntax())) {
+                        return ByteString.valueOfObject(GeneralizedTime.valueOf(parseDateTime(value.toString())));
+                    } else {
+                        return ByteString.valueOfObject(value);
+                    }
+                } else {
+                    throw new IllegalArgumentException("Unrecognized type of JSON value: "
+                            + value.getClass().getName());
+                }
+            }
+        };
+    }
+
+    static Filter toFilter(final boolean value) {
+        return value ? Filter.alwaysTrue() : Filter.alwaysFalse();
+    }
+
+    static Filter toFilter(final FilterType type, final String ldapAttribute, final ByteString valueAssertion) {
+        switch (type) {
+        case CONTAINS:
+            return Filter.substrings(ldapAttribute, null, Collections.singleton(valueAssertion), null);
+        case STARTS_WITH:
+            return Filter.substrings(ldapAttribute, valueAssertion, null, null);
+        case EQUAL_TO:
+            return Filter.equality(ldapAttribute, valueAssertion);
+        case GREATER_THAN:
+            return Filter.greaterThan(ldapAttribute, valueAssertion);
+        case GREATER_THAN_OR_EQUAL_TO:
+            return Filter.greaterOrEqual(ldapAttribute, valueAssertion);
+        case LESS_THAN:
+            return Filter.lessThan(ldapAttribute, valueAssertion);
+        case LESS_THAN_OR_EQUAL_TO:
+            return Filter.lessOrEqual(ldapAttribute, valueAssertion);
+        case PRESENT:
+            return Filter.present(ldapAttribute);
+        case EXTENDED:
+        default:
+            return alwaysFalse(); // Not supported.
+        }
+    }
+
+    static String toLowerCase(final String s) {
+        return s != null ? s.toLowerCase(Locale.ENGLISH) : null;
+    }
+
+    private static <T> List<T> asList(final Collection<T> c) {
+        if (c instanceof List) {
+            return (List<T>) c;
+        }
+        return new ArrayList<>(c);
+    }
+
+    /** Prevent instantiation. */
+    private Utils() {
+        // No implementation required.
+    }
+
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java
new file mode 100644
index 0000000..d300b62
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java
@@ -0,0 +1,79 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+
+/**
+ * The writability policy determines whether or not an attribute supports
+ * updates.
+ */
+public enum WritabilityPolicy {
+    // @formatter:off
+    /**
+     * The attribute cannot be provided when creating a new resource, nor
+     * modified afterwards. Attempts to update the attribute will result in an
+     * error.
+     */
+    READ_ONLY(false),
+
+    /**
+     * The attribute cannot be provided when creating a new resource, nor
+     * modified afterwards. Attempts to update the attribute will not result in
+     * an error (the new values will be ignored).
+     */
+    READ_ONLY_DISCARD_WRITES(true),
+
+    /**
+     * The attribute may be provided when creating a new resource, but cannot be
+     * modified afterwards. Attempts to update the attribute will result in an
+     * error.
+     */
+    CREATE_ONLY(false),
+
+    /**
+     * The attribute may be provided when creating a new resource, but cannot be
+     * modified afterwards. Attempts to update the attribute will not result in
+     * an error (the new values will be ignored).
+     */
+    CREATE_ONLY_DISCARD_WRITES(true),
+
+    /**
+     * The attribute may be provided when creating a new resource, and modified
+     * afterwards.
+     */
+    READ_WRITE(false);
+    // @formatter:on
+
+    private final boolean discardWrites;
+
+    private WritabilityPolicy(final boolean discardWrites) {
+        this.discardWrites = discardWrites;
+    }
+
+    boolean canCreate(final AttributeDescription attribute) {
+        return this != READ_ONLY && !attribute.getAttributeType().isNoUserModification();
+    }
+
+    boolean canWrite(final AttributeDescription attribute) {
+        return this == READ_WRITE && !attribute.getAttributeType().isNoUserModification();
+    }
+
+    boolean discardWrites() {
+        return discardWrites;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java
new file mode 100755
index 0000000..e92fb9e
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2012 ForgeRock AS.
+ */
+
+/**
+ * APIs for implementing REST to LDAP gateways.
+ */
+package org.forgerock.opendj.rest2ldap;
+
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/resources/META-INF/services/org.forgerock.http.HttpApplication b/opendj-sdk/opendj-rest2ldap/src/main/resources/META-INF/services/org.forgerock.http.HttpApplication
new file mode 100644
index 0000000..40ee013
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/main/resources/META-INF/services/org.forgerock.http.HttpApplication
@@ -0,0 +1,16 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions copyright [year] [name of copyright owner]".
+#
+# Copyright 2015 ForgeRock AS.
+#
+org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication
diff --git a/opendj-sdk/opendj-rest2ldap/src/site/xdoc/index.xml.vm b/opendj-sdk/opendj-rest2ldap/src/site/xdoc/index.xml.vm
new file mode 100644
index 0000000..b210923
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/site/xdoc/index.xml.vm
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2013-2015 ForgeRock AS.
+-->
+<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+  <properties>
+    <title>About ${project.name}</title>
+    <author email="opendj-dev@forgerock.org">${project.organization.name}</author>
+  </properties>
+  <body>
+    <section name="About ${project.name}">
+      <p>
+        ${project.description}
+      </p>
+    </section>
+    <section name="Documentation for ${project.name}">
+      <p>
+        Javadoc for this module can be found <a href="apidocs/index.html">here</a>.
+      </p>
+    </section>
+    <section name="Get ${project.name}">
+      <p>
+        Start developing your applications by obtaining ${project.name}
+        using any of the following methods:
+    </p>
+      <subsection name="Maven">
+        <p>
+          By far the simplest method is to develop your application using Maven
+          and add the following settings to your <b>pom.xml</b>:
+        </p>
+        <source>&lt;repositories>
+  &lt;repository>
+    &lt;id>forgerock-staging-repository&lt;/id>
+    &lt;name>ForgeRock Release Repository&lt;/name>
+    &lt;url>${mavenRepoReleases}&lt;/url>
+    &lt;snapshots>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/snapshots>
+  &lt;/repository>
+  &lt;repository>
+    &lt;id>forgerock-snapshots-repository&lt;/id>
+    &lt;name>ForgeRock Snapshot Repository&lt;/name>
+    &lt;url>${mavenRepoSnapshots}&lt;/url>
+    &lt;releases>
+      &lt;enabled>false&lt;/enabled>
+    &lt;/releases>
+  &lt;/repository>
+&lt;/repositories>
+
+...
+
+&lt;dependencies>
+  &lt;dependency>
+    &lt;groupId>${project.groupId}&lt;/groupId>
+    &lt;artifactId>${project.artifactId}&lt;/artifactId>
+    &lt;version>${project.version}&lt;/version>
+  &lt;/dependency>
+&lt;/dependencies></source>
+      </subsection>
+      <subsection name="Download">
+        <p>
+          If you are not using Maven then you will need to download a pre-built
+          binary from the ForgeRock Maven repository, along with any compile
+          time <a href="dependencies.html">dependencies</a>:
+        </p>
+        <ul>
+          <li><a href="${mavenRepoReleases}/org/forgerock/opendj/${project.artifactId}">Stable releases</a></li>
+          <li><a href="${mavenRepoSnapshots}/org/forgerock/opendj/${project.artifactId}/${project.version}">Latest development snapshot</a></li>
+        </ul>
+      </subsection>
+      <subsection name="Build">
+        <p>
+          For the DIY enthusiasts you can build it yourself by checking out the
+          latest code using <a href="source-repository.html">Subversion</a>
+          and building it with Maven 3.
+        </p>
+      </subsection>
+    </section>
+    <section name="Getting started">
+      <p>
+        The following example shows how ${project.name} may be used:
+      </p>
+      <source>TODO</source>
+    </section>
+  </body>
+</document>
diff --git a/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
new file mode 100644
index 0000000..b791036
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
@@ -0,0 +1,133 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.forgerock.json.resource.ForbiddenException;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the AuthzIdTemplate class.
+ */
+@SuppressWarnings({ "javadoc" })
+@Test
+public final class AuthzIdTemplateTest extends ForgeRockTestCase {
+
+    @DataProvider
+    public Object[][] templateData() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                "dn:uid={uid},ou={realm},dc=example,dc=com",
+                "dn:uid=test.user,ou=acme,dc=example,dc=com",
+                map("uid", "test.user", "realm", "acme")
+            },
+            {
+                // Should perform DN quoting.
+                "dn:uid={uid},ou={realm},dc=example,dc=com",
+                "dn:uid=test.user,ou=test\\+cn=quoting,dc=example,dc=com",
+                map("uid", "test.user", "realm", "test+cn=quoting")
+            },
+            {
+                // Should not perform DN quoting.
+                "dn:{dn}",
+                "dn:uid=test.user,ou=acme,dc=example,dc=com",
+                map("dn", "uid=test.user,ou=acme,dc=example,dc=com")
+            },
+            {
+                "u:{uid}@{realm}.example.com",
+                "u:test.user@acme.example.com",
+                map("uid", "test.user", "realm", "acme")
+            },
+            {
+                // Should not perform any DN quoting.
+                "u:{uid}@{realm}.example.com",
+                "u:test.user@test+cn=quoting.example.com",
+                map("uid", "test.user", "realm", "test+cn=quoting")
+            },
+        };
+        // @formatter:on
+
+    }
+
+    @Test(dataProvider = "templateData")
+    public void testTemplates(final String template, final String expected,
+            Map<String, Object> principals) throws Exception {
+        assertThat(
+                new AuthzIdTemplate(template)
+                        .formatAsAuthzId(principals, Schema.getDefaultSchema()))
+                .isEqualTo(expected);
+    }
+
+    @DataProvider
+    public Object[][] invalidTemplateData() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                "dn:uid={uid},ou={realm},dc=example,dc=com",
+                map("uid", "test.user")
+            },
+            {
+                // Malformed DN.
+                "dn:{dn}",
+                map("dn", "uid")
+            },
+            {
+                "u:{uid}@{realm}.example.com",
+                map("uid", "test.user")
+            },
+        };
+        // @formatter:on
+
+    }
+
+    @Test(dataProvider = "invalidTemplateData", expectedExceptions = ForbiddenException.class)
+    public void testInvalidTemplateData(final String template, Map<String, Object> principals)
+            throws Exception {
+        new AuthzIdTemplate(template).formatAsAuthzId(principals, Schema.getDefaultSchema());
+    }
+
+    @DataProvider
+    public Object[][] invalidTemplates() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                "x:uid={uid},ou={realm},dc=example,dc=com"
+            },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "invalidTemplates", expectedExceptions = IllegalArgumentException.class)
+    public void testInvalidTemplates(final String template) throws Exception {
+        new AuthzIdTemplate(template);
+    }
+
+    private Map<String, Object> map(String... keyValues) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            map.put(keyValues[i], keyValues[i + 1]);
+        }
+        return map;
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
new file mode 100644
index 0000000..9c9d9c4
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -0,0 +1,776 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Arrays.asList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.json.JsonValue.field;
+import static org.forgerock.json.JsonValue.json;
+import static org.forgerock.json.JsonValue.object;
+import static org.forgerock.json.resource.PatchOperation.add;
+import static org.forgerock.json.resource.PatchOperation.increment;
+import static org.forgerock.json.resource.PatchOperation.remove;
+import static org.forgerock.json.resource.PatchOperation.replace;
+import static org.forgerock.json.resource.Requests.newDeleteRequest;
+import static org.forgerock.json.resource.Requests.newPatchRequest;
+import static org.forgerock.json.resource.Requests.newQueryRequest;
+import static org.forgerock.json.resource.Requests.newReadRequest;
+import static org.forgerock.json.resource.Requests.newUpdateRequest;
+import static org.forgerock.json.resource.Resources.newCollection;
+import static org.forgerock.json.resource.Resources.newInternalConnection;
+import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.ldap.Functions.byteStringToInteger;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.constant;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple;
+import static org.forgerock.opendj.rest2ldap.TestUtils.asResource;
+import static org.forgerock.opendj.rest2ldap.TestUtils.content;
+import static org.forgerock.opendj.rest2ldap.TestUtils.ctx;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.Connection;
+import org.forgerock.json.resource.NotFoundException;
+import org.forgerock.json.resource.NotSupportedException;
+import org.forgerock.json.resource.PreconditionFailedException;
+import org.forgerock.json.resource.QueryResponse;
+import org.forgerock.json.resource.Requests;
+import org.forgerock.json.resource.ResourceResponse;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.query.QueryFilter;
+import org.testng.annotations.Test;
+
+/** Tests that CREST requests are correctly mapped to LDAP. */
+@SuppressWarnings({ "javadoc" })
+@Test
+public final class BasicRequestsTest extends ForgeRockTestCase {
+    // FIXME: we need to test the request handler, not internal connections,
+    // so that we can check that the request handler is returning everything.
+    // FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend).
+
+    private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
+
+    @Test
+    public void testQueryAll() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result = connection.query(
+            ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources);
+        assertThat(resources).hasSize(5);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testQueryNone() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result = connection.query(
+            ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources);
+        assertThat(resources).hasSize(0);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testQueryPageResultsCookie() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new ArrayList<>();
+
+        // Read first page.
+        QueryResponse result = connection.query(
+                ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
+        assertThat(result.getPagedResultsCookie()).isNotNull();
+        assertThat(resources).hasSize(2);
+        assertThat(resources.get(0).getId()).isEqualTo("test1");
+        assertThat(resources.get(1).getId()).isEqualTo("test2");
+
+        String cookie = result.getPagedResultsCookie();
+        resources.clear();
+
+        // Read second page.
+        result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+        assertThat(result.getPagedResultsCookie()).isNotNull();
+        assertThat(resources).hasSize(2);
+        assertThat(resources.get(0).getId()).isEqualTo("test3");
+        assertThat(resources.get(1).getId()).isEqualTo("test4");
+
+        cookie = result.getPagedResultsCookie();
+        resources.clear();
+
+        // Read third page.
+        result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(resources).hasSize(1);
+        assertThat(resources.get(0).getId()).isEqualTo("test5");
+    }
+
+    @Test
+    public void testQueryPageResultsIndexed() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new ArrayList<>();
+        QueryResponse result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
+        assertThat(result.getPagedResultsCookie()).isNotNull();
+        assertThat(resources).hasSize(2);
+        assertThat(resources.get(0).getId()).isEqualTo("test3");
+        assertThat(resources.get(1).getId()).isEqualTo("test4");
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testDelete() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1"));
+        checkResourcesAreEqual(resource, getTestUser1(12345));
+        connection.read(ctx(), newReadRequest("/test1"));
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testDeleteMVCCMatch() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
+        checkResourcesAreEqual(resource, getTestUser1(12345));
+        connection.read(ctx(), newReadRequest("/test1"));
+    }
+
+    @Test(expectedExceptions = PreconditionFailedException.class)
+    public void testDeleteMVCCNoMatch() throws Exception {
+        final Connection connection = newConnection();
+        connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346"));
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testDeleteNotFound() throws Exception {
+        final Connection connection = newConnection();
+        connection.delete(ctx(), newDeleteRequest("/missing"));
+    }
+
+    @Test
+    public void testPatch() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")));
+        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+    }
+
+    @Test
+    public void testPatchEmpty() throws Exception {
+        final List<Request> requests = new LinkedList<>();
+        final Connection connection = newConnection(requests);
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
+        checkResourcesAreEqual(resource1, getTestUser1(12345));
+
+        /*
+         * Check that no modify operation was sent (only a single search should
+         * be sent in order to get the current resource).
+         */
+        assertThat(requests).hasSize(1);
+        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
+
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1(12345));
+    }
+
+    @Test
+    public void testPatchAddOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("description", asList("one", "two"));
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
+                        "two"))));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test
+    public void testPatchAddOptionalAttributeIndexAppend() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("description", asList("one", "two"));
+        final ResourceResponse resource1 = connection.patch(
+            ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchConstantAttribute() throws Exception {
+        newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
+    }
+
+    @Test
+    public void testPatchDeleteOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
+        checkResourcesAreEqual(resource1, getTestUser1(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1(12345));
+    }
+
+    @Test
+    public void testPatchIncrement() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("singleNumber", 100);
+        newContent.put("multiNumber", asList(200, 300));
+
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
+            add("/singleNumber", 0),
+            add("/multiNumber", asList(100, 200)),
+            increment("/singleNumber", 100),
+            increment("/multiNumber", 100)));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchMissingRequiredAttribute() throws Exception {
+        newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
+    }
+
+    @Test
+    public void testPatchModifyOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three"))));
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("description", asList("one", "two", "three"));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test(expectedExceptions = NotSupportedException.class)
+    public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description/0", "junk")));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description/-",
+                asList("one", "two"))));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchMultiValuedAttributeWithSingleValue() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", "one")));
+    }
+
+    @Test
+    public void testPatchMVCCMatch() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource1 = connection.patch(
+            ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
+        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+    }
+
+    @Test(expectedExceptions = PreconditionFailedException.class)
+    public void testPatchMVCCNoMatch() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testPatchNotFound() throws Exception {
+        newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchReadOnlyAttribute() throws Exception {
+        // Etag is read-only.
+        newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
+    }
+
+    @Test
+    public void testPatchReplacePartialObject() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue expected = json(object(
+            field("schemas", asList("urn:scim:schemas:core:1.0")),
+            field("_id", "test1"),
+            field("_rev", "12345"),
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
+            replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
+        checkResourcesAreEqual(resource1, expected);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, expected);
+    }
+
+    @Test
+    public void testPatchReplaceWholeObject() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = json(object(
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final JsonValue expected = json(object(
+            field("schemas", asList("urn:scim:schemas:core:1.0")),
+            field("_id", "test1"),
+            field("_rev", "12345"),
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent)));
+        checkResourcesAreEqual(resource1, expected);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, expected);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
+    }
+
+    @Test(expectedExceptions = NotSupportedException.class)
+    public void testPatchSingleValuedAttributeIndexNumber() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname", asList("black",
+                "white"))));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchUnknownAttribute() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk")));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchUnknownSubAttribute() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy", "junk")));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchUnknownSubSubAttribute() throws Exception {
+        final Connection connection = newConnection();
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy/dummy", "junk")));
+    }
+
+    @Test
+    public void testRead() throws Exception {
+        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource, getTestUser1(12345));
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testReadNotFound() throws Exception {
+        newConnection().read(ctx(), newReadRequest("/missing"));
+    }
+
+    @Test
+    public void testReadSelectAllFields() throws Exception {
+        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
+        checkResourcesAreEqual(resource, getTestUser1(12345));
+    }
+
+    @Test
+    public void testReadSelectPartial() throws Exception {
+        final ResourceResponse resource = newConnection().read(
+            ctx(), newReadRequest("/test1").addField("/name/surname"));
+        assertThat(resource.getId()).isEqualTo("test1");
+        assertThat(resource.getRevision()).isEqualTo("12345");
+        assertThat(resource.getContent().get("_id").asString()).isNull();
+        assertThat(resource.getContent().get("name").asMap()).isNull();
+        assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
+        assertThat(resource.getContent().get("_rev").asString()).isNull();
+    }
+
+    /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */
+    @Test(enabled = false)
+    public void testReadSelectPartialInsensitive() throws Exception {
+        final ResourceResponse resource = newConnection().read(
+            ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
+        assertThat(resource.getId()).isEqualTo("test1");
+        assertThat(resource.getRevision()).isEqualTo("12345");
+        assertThat(resource.getContent().get("_id").asString()).isNull();
+        assertThat(resource.getContent().get("/name/displayName").asString()).isNull();
+        assertThat(resource.getContent().get("/name/surname").asString()).isEqualTo("user 1");
+        assertThat(resource.getContent().get("_rev").asString()).isNull();
+    }
+
+    @Test
+    public void testUpdate() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource1 = connection.update(
+            ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
+        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+    }
+
+    @Test
+    public void testUpdateNoChange() throws Exception {
+        final List<Request> requests = new LinkedList<>();
+        final Connection connection = newConnection(requests);
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
+
+        // Check that no modify operation was sent
+        // (only a single search should be sent in order to get the current resource).
+        assertThat(requests).hasSize(1);
+        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
+
+        checkResourcesAreEqual(resource1, getTestUser1(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1(12345));
+    }
+
+    @Test
+    public void testUpdateAddOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.put("description", asList("one", "two"));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testUpdateConstantAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.put("schemas", asList("junk"));
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+    }
+
+    @Test
+    public void testUpdateDeleteOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.put("description", asList("one", "two"));
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        newContent.remove("description");
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testUpdateMissingRequiredAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.get("name").remove("surname");
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+    }
+
+    @Test
+    public void testUpdateModifyOptionalAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.put("description", asList("one", "two"));
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        newContent.put("description", asList("three"));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        checkResourcesAreEqual(resource1, newContent);
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
+    @Test
+    public void testUpdateMVCCMatch() throws Exception {
+        final Connection connection = newConnection();
+        final ResourceResponse resource1 =
+                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
+        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+    }
+
+    @Test(expectedExceptions = PreconditionFailedException.class)
+    public void testUpdateMVCCNoMatch() throws Exception {
+        final Connection connection = newConnection();
+        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
+                .setRevision("12346"));
+    }
+
+    @Test(expectedExceptions = NotFoundException.class)
+    public void testUpdateNotFound() throws Exception {
+        final Connection connection = newConnection();
+        connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testUpdateReadOnlyAttribute() throws Exception {
+        final Connection connection = newConnection();
+        // Etag is read-only.
+        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.put("surname", asList("black", "white"));
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testUpdateUnknownAttribute() throws Exception {
+        final Connection connection = newConnection();
+        final JsonValue newContent = getTestUser1Updated(12345);
+        newContent.add("dummy", "junk");
+        connection.update(ctx(), newUpdateRequest("/test1", newContent));
+    }
+
+    private Connection newConnection() throws IOException {
+        return newConnection(new LinkedList<Request>());
+    }
+
+    private Connection newConnection(final List<Request> requests) throws IOException {
+        return newInternalConnection(newCollection(builder(requests).build()));
+    }
+
+    private Builder builder(final List<Request> requests) throws IOException {
+        return Rest2LDAP.builder()
+                .ldapConnectionFactory(getConnectionFactory(requests))
+                .baseDN("dc=test")
+                .useEtagAttribute()
+                .useClientDNNaming("uid")
+                .readOnUpdatePolicy(ReadOnUpdatePolicy.CONTROLS)
+                .authorizationPolicy(AuthorizationPolicy.NONE)
+                .additionalLDAPAttribute("objectClass", "top", "person")
+                .mapper(object()
+                        .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0")))
+                        .attribute("_id", simple("uid").isSingleValued()
+                                                       .isRequired()
+                                                       .writability(WritabilityPolicy.CREATE_ONLY))
+                        .attribute("name", object().attribute("displayName", simple("cn").isSingleValued()
+                                                                                         .isRequired())
+                                                    .attribute("surname", simple("sn").isSingleValued().isRequired()))
+                        .attribute("_rev", simple("etag").isSingleValued()
+                                                         .isRequired()
+                                                         .writability(WritabilityPolicy.READ_ONLY))
+                        .attribute("description", simple("description"))
+                        .attribute("singleNumber", simple("singleNumber").decoder(byteStringToInteger())
+                                                                         .isSingleValued())
+                        .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger())));
+    }
+
+    private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
+        final ResourceResponse expectedResource = asResource(expected);
+        assertThat(actual.getId()).isEqualTo(expectedResource.getId());
+        assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
+        assertThat(actual.getContent().getObject()).isEqualTo(
+                expectedResource.getContent().getObject());
+    }
+
+    private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException {
+        // @formatter:off
+        final MemoryBackend backend =
+                new MemoryBackend(new LDIFEntryReader(
+                        "dn: dc=test",
+                        "objectClass: domain",
+                        "objectClass: top",
+                        "dc: com",
+                        "",
+                        "dn: uid=test1,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test1",
+                        "userpassword: password",
+                        "cn: test user 1",
+                        "sn: user 1",
+                        "etag: 12345",
+                        "",
+                        "dn: uid=test2,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test2",
+                        "userpassword: password",
+                        "cn: test user 2",
+                        "sn: user 2",
+                        "etag: 67890",
+                        "",
+                        "dn: uid=test3,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test3",
+                        "userpassword: password",
+                        "cn: test user 3",
+                        "sn: user 3",
+                        "etag: 33333",
+                        "",
+                        "dn: uid=test4,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test4",
+                        "userpassword: password",
+                        "cn: test user 4",
+                        "sn: user 4",
+                        "etag: 44444",
+                        "",
+                        "dn: uid=test5,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test5",
+                        "userpassword: password",
+                        "cn: test user 5",
+                        "sn: user 5",
+                        "etag: 55555"
+                ));
+        // @formatter:on
+
+        return newInternalConnectionFactory(recordRequests(backend, requests));
+    }
+
+    private RequestHandler<RequestContext> recordRequests(
+            final RequestHandler<RequestContext> handler, final List<Request> requests) {
+        return new RequestHandler<RequestContext>() {
+            @Override
+            public void handleAdd(RequestContext requestContext, AddRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleAdd(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleBind(RequestContext requestContext, int version, BindRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<BindResult> resultHandler) {
+                requests.add(request);
+                handler.handleBind(requestContext, version, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleCompare(RequestContext requestContext, CompareRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<CompareResult> resultHandler) {
+                requests.add(request);
+                handler.handleCompare(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleDelete(RequestContext requestContext, DeleteRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleDelete(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public <R extends ExtendedResult> void handleExtendedRequest(
+                    RequestContext requestContext, ExtendedRequest<R> request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<R> resultHandler) {
+                requests.add(request);
+                handler.handleExtendedRequest(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleModify(RequestContext requestContext, ModifyRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleModify(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleModifyDN(RequestContext requestContext, ModifyDNRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    LdapResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleModifyDN(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleSearch(RequestContext requestContext, SearchRequest request,
+                IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler,
+                LdapResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler,
+                    resultHandler);
+            }
+
+        };
+    }
+
+    private JsonValue getTestUser1(final int rev) {
+        return content(object(
+                field("schemas", asList("urn:scim:schemas:core:1.0")),
+                field("_id", "test1"),
+                field("_rev", String.valueOf(rev)),
+                field("name", object(field("displayName", "test user 1"),
+                                     field("surname", "user 1")))));
+    }
+
+    private JsonValue getTestUser1Updated(final int rev) {
+        return content(object(
+                field("schemas", asList("urn:scim:schemas:core:1.0")),
+                field("_id", "test1"),
+                field("_rev", String.valueOf(rev)),
+                field("name", object(field("displayName", "changed"),
+                                     field("surname", "user 1")))));
+    }
+}
diff --git a/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
new file mode 100644
index 0000000..f436de2
--- /dev/null
+++ b/opendj-sdk/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
@@ -0,0 +1,96 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.json.JsonValue.json;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.ResourceResponse;
+import org.forgerock.json.resource.Responses;
+import org.forgerock.services.context.RootContext;
+
+/** Unit test utility methods, including fluent methods for creating JSON objects. */
+public final class TestUtils {
+
+    /**
+     * Returns a {@code Resource} containing the provided JSON content. The ID
+     * and revision will be taken from the "_id" and "_rev" fields respectively.
+     *
+     * @param content
+     *            The JSON content.
+     * @return A {@code Resource} containing the provided JSON content.
+     */
+    public static ResourceResponse asResource(final JsonValue content) {
+        return Responses.newResourceResponse(content.get("_id").asString(), content.get("_rev").asString(), content);
+    }
+
+    /**
+     * Creates a JSON value for the provided object.
+     *
+     * @param object
+     *            The object.
+     * @return The JSON value.
+     */
+    public static JsonValue content(final Object object) {
+        return json(object);
+    }
+
+    /**
+     * Creates a root context to be passed in with client requests.
+     *
+     * @return The root context.
+     */
+    public static RootContext ctx() {
+        return new RootContext();
+    }
+
+    /**
+     * Creates a JSON value for the provided object. This is the same as
+     * {@link #content(Object)} but can yield more readable test data in data
+     * providers.
+     *
+     * @param object
+     *            The object.
+     * @return The JSON value.
+     */
+    public static JsonValue expected(final Object object) {
+        return json(object);
+    }
+
+    /**
+     * Creates a list of JSON pointers from the provided string representations.
+     *
+     * @param fields
+     *            The list of JSON pointer strings.
+     * @return The list of parsed JSON pointers.
+     */
+    public static List<JsonPointer> filter(final String... fields) {
+        final List<JsonPointer> result = new ArrayList<>(fields.length);
+        for (final String field : fields) {
+            result.add(new JsonPointer(field));
+        }
+        return result;
+    }
+
+    private TestUtils() {
+        // Prevent instantiation.
+    }
+
+}
diff --git a/opendj-sdk/opendj-sdk-bom/pom.xml b/opendj-sdk/opendj-sdk-bom/pom.xml
new file mode 100644
index 0000000..7f5d35a
--- /dev/null
+++ b/opendj-sdk/opendj-sdk-bom/pom.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions Copyright [year] [name of copyright owner]".
+
+  Copyright 2011-2016 ForgeRock AS.
+-->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.forgerock</groupId>
+        <artifactId>forgerock-parent</artifactId>
+        <version>2.0.3</version>
+        <relativePath />
+    </parent>
+
+    <groupId>org.forgerock.opendj</groupId>
+    <artifactId>opendj-sdk-bom</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+
+    <packaging>pom</packaging>
+
+    <name>OpenDJ SDK BOM</name>
+    <description>
+        Provides a list of OpenDJ SDK dependencies which are known to be compatible with each other.
+    </description>
+
+    <properties>
+        <opendj.sdk.version>4.0.0-SNAPSHOT</opendj.sdk.version>
+        <i18n-framework.version>1.4.2</i18n-framework.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- ForgeRock BOM -->
+            <dependency>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>forgerock-bom</artifactId>
+                <version>4.1.1</version>
+                <scope>import</scope>
+                <type>pom</type>
+            </dependency>
+
+            <!-- I18N framework -->
+            <dependency>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-core</artifactId>
+                <version>${i18n-framework.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.forgerock.commons</groupId>
+                <artifactId>i18n-slf4j</artifactId>
+                <version>${i18n-framework.version}</version>
+            </dependency>
+
+
+            <!-- OpenDJ SDK -->
+            <dependency>
+                <groupId>org.forgerock.opendj</groupId>
+                <artifactId>opendj-core</artifactId>
+                <version>${opendj.sdk.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.forgerock.opendj</groupId>
+                <artifactId>opendj-cli</artifactId>
+                <version>${opendj.sdk.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.forgerock.opendj</groupId>
+                <artifactId>opendj-grizzly</artifactId>
+                <version>${opendj.sdk.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.forgerock.opendj</groupId>
+                <artifactId>opendj-rest2ldap</artifactId>
+                <version>${opendj.sdk.version}</version>
+            </dependency>
+
+            <!-- Other -->
+            <dependency>
+                <groupId>com.github.stephenc.jcip</groupId>
+                <artifactId>jcip-annotations</artifactId>
+                <version>1.0-1</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

--
Gitblit v1.10.0